Skip to main content

opentelemetry_sdk/resource/
env.rs

1//! Environment variables resource detector
2//!
3//! Implementation of `ResourceDetector` to extract a `Resource` from environment
4//! variables.
5use crate::resource::{Resource, ResourceDetector};
6use opentelemetry::{Key, KeyValue, Value};
7use std::env;
8
9const OTEL_RESOURCE_ATTRIBUTES: &str = "OTEL_RESOURCE_ATTRIBUTES";
10const OTEL_SERVICE_NAME: &str = "OTEL_SERVICE_NAME";
11
12/// EnvResourceDetector extract resource from environment variable
13/// `OTEL_RESOURCE_ATTRIBUTES`. See [OpenTelemetry Resource
14/// Spec](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable)
15/// for details.
16#[derive(Debug)]
17pub struct EnvResourceDetector {
18    _private: (),
19}
20
21impl ResourceDetector for EnvResourceDetector {
22    fn detect(&self) -> Resource {
23        match env::var(OTEL_RESOURCE_ATTRIBUTES) {
24            Ok(s) if !s.is_empty() => construct_otel_resources(s),
25            Ok(_) | Err(_) => Resource::empty(), // return empty resource
26        }
27    }
28}
29
30impl EnvResourceDetector {
31    /// Create `EnvResourceDetector` instance.
32    pub fn new() -> Self {
33        EnvResourceDetector { _private: () }
34    }
35}
36
37impl Default for EnvResourceDetector {
38    fn default() -> Self {
39        EnvResourceDetector::new()
40    }
41}
42
43/// Extract key value pairs and construct a resource from resources string like
44/// key1=value1,key2=value2,...
45fn construct_otel_resources(s: String) -> Resource {
46    Resource::builder_empty()
47        .with_attributes(s.split_terminator(',').filter_map(|entry| {
48            let parts = match entry.split_once('=') {
49                Some(p) => p,
50                None => return None,
51            };
52            let key = parts.0.trim();
53            let value = parts.1.trim();
54
55            Some(KeyValue::new(key.to_owned(), value.to_owned()))
56        }))
57        .build()
58}
59
60/// There are attributes which MUST be provided by the SDK as specified in
61/// [the Resource SDK specification]. This detector detects those attributes and
62/// if the attribute cannot be detected, it uses the default value.
63///
64/// This detector will first try `OTEL_SERVICE_NAME` env. If it's not available,
65/// then it will check the `OTEL_RESOURCE_ATTRIBUTES` env and see if it contains
66/// `service.name` resource. If it's also not available, it will use `unknown_service`.
67///
68/// If users want to set an empty service name, they can provide
69/// a resource with empty value and `service.name` key.
70///
71/// [the Resource SDK specification]:https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#sdk-provided-resource-attributes
72#[derive(Debug)]
73pub struct SdkProvidedResourceDetector;
74
75fn default_service_name() -> Value {
76    // Fallback to unknown_service:<process.executable.name> per spec.
77    // https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#sdk-provided-resource-attributes
78    if cfg!(miri) {
79        return "unknown_service".into();
80    }
81
82    env::current_exe()
83        .ok()
84        .and_then(|path| {
85            path.file_name()
86                .and_then(|name| name.to_str())
87                .map(|name| format!("unknown_service:{}", name))
88        })
89        .unwrap_or_else(|| "unknown_service".to_string())
90        .into()
91}
92
93impl ResourceDetector for SdkProvidedResourceDetector {
94    fn detect(&self) -> Resource {
95        Resource::builder_empty()
96            .with_attributes([KeyValue::new(
97                super::SERVICE_NAME,
98                env::var(OTEL_SERVICE_NAME)
99                    .ok()
100                    .filter(|s| !s.is_empty())
101                    .map(Value::from)
102                    .or_else(|| {
103                        EnvResourceDetector::new()
104                            .detect()
105                            .get(&Key::new(super::SERVICE_NAME))
106                    })
107                    .unwrap_or_else(default_service_name),
108            )])
109            .build()
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use crate::resource::env::{
116        SdkProvidedResourceDetector, OTEL_RESOURCE_ATTRIBUTES, OTEL_SERVICE_NAME,
117    };
118    use crate::resource::{EnvResourceDetector, Resource, ResourceDetector};
119    use opentelemetry::{Key, KeyValue, Value};
120
121    #[test]
122    fn test_read_from_env() {
123        temp_env::with_vars(
124            [
125                (
126                    "OTEL_RESOURCE_ATTRIBUTES",
127                    Some("key=value, k = v , a= x, a=z,base64=SGVsbG8sIFdvcmxkIQ=="),
128                ),
129                ("IRRELEVANT", Some("20200810")),
130            ],
131            || {
132                let detector = EnvResourceDetector::new();
133                let resource = detector.detect();
134                assert_eq!(
135                    resource,
136                    Resource::builder_empty()
137                        .with_attributes([
138                            KeyValue::new("key", "value"),
139                            KeyValue::new("k", "v"),
140                            KeyValue::new("a", "x"),
141                            KeyValue::new("a", "z"),
142                            KeyValue::new("base64", "SGVsbG8sIFdvcmxkIQ=="), // base64('Hello, World!')
143                        ])
144                        .build()
145                );
146            },
147        );
148
149        let detector = EnvResourceDetector::new();
150        let resource = detector.detect();
151        assert!(resource.is_empty());
152    }
153
154    #[test]
155    fn test_sdk_provided_resource_detector() {
156        // Ensure no env var set - should fallback to unknown_service:<executable_name>
157        // For cargo tests, the executable name is typically <crate_name>-<hash>
158        let no_env = SdkProvidedResourceDetector.detect();
159        let service_name = no_env
160            .get(&Key::from_static_str(crate::resource::SERVICE_NAME))
161            .map(|v| v.to_string())
162            .unwrap();
163
164        if cfg!(miri) {
165            assert_eq!(service_name, "unknown_service");
166        } else {
167            assert!(
168                service_name.starts_with("unknown_service:opentelemetry_sdk-"),
169                "Expected service name to start with 'unknown_service:opentelemetry_sdk-', got: {}",
170                service_name
171            );
172        }
173
174        temp_env::with_var(OTEL_SERVICE_NAME, Some("test service"), || {
175            let with_service = SdkProvidedResourceDetector.detect();
176            assert_eq!(
177                with_service.get(&Key::from_static_str(crate::resource::SERVICE_NAME)),
178                Some(Value::from("test service")),
179            )
180        });
181
182        temp_env::with_var(
183            OTEL_RESOURCE_ATTRIBUTES,
184            Some("service.name=test service1"),
185            || {
186                let with_service = SdkProvidedResourceDetector.detect();
187                assert_eq!(
188                    with_service.get(&Key::from_static_str(crate::resource::SERVICE_NAME)),
189                    Some(Value::from("test service1")),
190                )
191            },
192        );
193
194        // OTEL_SERVICE_NAME takes priority
195        temp_env::with_vars(
196            [
197                (OTEL_SERVICE_NAME, Some("test service")),
198                (OTEL_RESOURCE_ATTRIBUTES, Some("service.name=test service3")),
199            ],
200            || {
201                let with_service = SdkProvidedResourceDetector.detect();
202                assert_eq!(
203                    with_service.get(&Key::from_static_str(crate::resource::SERVICE_NAME)),
204                    Some(Value::from("test service"))
205                );
206            },
207        );
208    }
209}