opentelemetry_sdk/resource/
mod.rs

1//! Representations of entities producing telemetry.
2//!
3//! A [Resource] is an immutable representation of the entity producing
4//! telemetry as attributes. For example, a process producing telemetry that is
5//! running in a container on Kubernetes has a Pod name, it is in a namespace
6//! and possibly is part of a Deployment which also has a name. All three of
7//! these attributes can be included in the `Resource`. Note that there are
8//! certain ["standard attributes"] that have prescribed meanings.
9//!
10//! ["standard attributes"]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/resource/semantic_conventions/README.md
11//!
12//! # Resource detectors
13//!
14//! [`ResourceDetector`]s are used to detect resource from runtime or
15//! environmental variables. The following are provided by default with this
16//! SDK.
17//!
18//! - [`EnvResourceDetector`] - detect resource from environmental variables.
19//! - [`TelemetryResourceDetector`] - detect telemetry SDK's information.
20//!
21//! The OS and Process resource detectors are packaged separately in the
22//! [`opentelemetry-resource-detector` crate](https://github.com/open-telemetry/opentelemetry-rust-contrib/tree/main/opentelemetry-resource-detectors).
23mod env;
24mod telemetry;
25
26mod attributes;
27pub(crate) use attributes::*;
28
29pub use env::EnvResourceDetector;
30pub use env::SdkProvidedResourceDetector;
31pub use telemetry::TelemetryResourceDetector;
32
33use opentelemetry::{Key, KeyValue, Value};
34use std::borrow::Cow;
35use std::collections::{hash_map, HashMap};
36use std::ops::Deref;
37use std::sync::Arc;
38use std::time::Duration;
39
40/// Inner structure of `Resource` holding the actual data.
41/// This structure is designed to be shared among `Resource` instances via `Arc`.
42#[derive(Debug, Clone, PartialEq)]
43struct ResourceInner {
44    attrs: HashMap<Key, Value>,
45    schema_url: Option<Cow<'static, str>>,
46}
47
48/// An immutable representation of the entity producing telemetry as attributes.
49/// Utilizes `Arc` for efficient sharing and cloning.
50#[derive(Clone, Debug, PartialEq)]
51pub struct Resource {
52    inner: Arc<ResourceInner>,
53}
54
55impl Default for Resource {
56    fn default() -> Self {
57        Self::from_detectors(
58            Duration::from_secs(0),
59            vec![
60                Box::new(SdkProvidedResourceDetector),
61                Box::new(TelemetryResourceDetector),
62                Box::new(EnvResourceDetector::new()),
63            ],
64        )
65    }
66}
67
68impl Resource {
69    /// Creates an empty resource.
70    /// This is the basic constructor that initializes a resource with no attributes and no schema URL.
71    pub fn empty() -> Self {
72        Resource {
73            inner: Arc::new(ResourceInner {
74                attrs: HashMap::new(),
75                schema_url: None,
76            }),
77        }
78    }
79
80    /// Create a new `Resource` from key value pairs.
81    ///
82    /// Values are de-duplicated by key, and the first key-value pair with a non-empty string value
83    /// will be retained
84    pub fn new<T: IntoIterator<Item = KeyValue>>(kvs: T) -> Self {
85        let mut attrs = HashMap::new();
86        for kv in kvs {
87            attrs.insert(kv.key, kv.value);
88        }
89
90        Resource {
91            inner: Arc::new(ResourceInner {
92                attrs,
93                schema_url: None,
94            }),
95        }
96    }
97
98    /// Create a new `Resource` from a key value pairs and [schema url].
99    ///
100    /// Values are de-duplicated by key, and the first key-value pair with a non-empty string value
101    /// will be retained.
102    ///
103    /// schema_url must be a valid URL using HTTP or HTTPS protocol.
104    ///
105    /// [schema url]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url
106    pub fn from_schema_url<KV, S>(kvs: KV, schema_url: S) -> Self
107    where
108        KV: IntoIterator<Item = KeyValue>,
109        S: Into<Cow<'static, str>>,
110    {
111        let schema_url_str = schema_url.into();
112        let normalized_schema_url = if schema_url_str.is_empty() {
113            None
114        } else {
115            Some(schema_url_str)
116        };
117        let mut attrs = HashMap::new();
118        for kv in kvs {
119            attrs.insert(kv.key, kv.value);
120        }
121        Resource {
122            inner: Arc::new(ResourceInner {
123                attrs,
124                schema_url: normalized_schema_url,
125            }),
126        }
127    }
128
129    /// Create a new `Resource` from resource detectors.
130    ///
131    /// timeout will be applied to each detector.
132    pub fn from_detectors(timeout: Duration, detectors: Vec<Box<dyn ResourceDetector>>) -> Self {
133        let mut resource = Resource::empty();
134        for detector in detectors {
135            let detected_res = detector.detect(timeout);
136            // This call ensures that if the Arc is not uniquely owned,
137            // the data is cloned before modification, preserving safety.
138            // If the Arc is uniquely owned, it simply returns a mutable reference to the data.
139            let inner = Arc::make_mut(&mut resource.inner);
140            for (key, value) in detected_res.into_iter() {
141                inner.attrs.insert(Key::new(key.clone()), value.clone());
142            }
143        }
144
145        resource
146    }
147
148    /// Create a new `Resource` by combining two resources.
149    ///
150    /// ### Key value pairs
151    /// Keys from the `other` resource have priority over keys from this resource, even if the
152    /// updated value is empty.
153    ///
154    /// ### [Schema url]
155    /// If both of the resource are not empty. Schema url is determined by the following rules, in order:
156    /// 1. If this resource has a schema url, it will be used.
157    /// 2. If this resource does not have a schema url, and the other resource has a schema url, it will be used.
158    /// 3. If both resources have a schema url and it's the same, it will be used.
159    /// 4. If both resources have a schema url and it's different, the schema url will be empty.
160    /// 5. If both resources do not have a schema url, the schema url will be empty.
161    ///
162    /// [Schema url]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url
163    pub fn merge<T: Deref<Target = Self>>(&self, other: T) -> Self {
164        if self.is_empty() {
165            return other.clone();
166        }
167        if other.is_empty() {
168            return self.clone();
169        }
170        let mut combined_attrs = self.inner.attrs.clone();
171        for (k, v) in other.inner.attrs.iter() {
172            combined_attrs.insert(k.clone(), v.clone());
173        }
174        // Resolve the schema URL according to the precedence rules
175        let combined_schema_url = match (&self.inner.schema_url, &other.inner.schema_url) {
176            // If both resources have a schema URL and it's the same, use it
177            (Some(url1), Some(url2)) if url1 == url2 => Some(url1.clone()),
178            // If both resources have a schema URL but they are not the same, the schema URL will be empty
179            (Some(_), Some(_)) => None,
180            // If this resource does not have a schema URL, and the other resource has a schema URL, it will be used
181            (None, Some(url)) => Some(url.clone()),
182            // If this resource has a schema URL, it will be used (covers case 1 and any other cases where `self` has a schema URL)
183            (Some(url), _) => Some(url.clone()),
184            // If both resources do not have a schema URL, the schema URL will be empty
185            (None, None) => None,
186        };
187        Resource {
188            inner: Arc::new(ResourceInner {
189                attrs: combined_attrs,
190                schema_url: combined_schema_url,
191            }),
192        }
193    }
194
195    /// Return the [schema url] of the resource. If the resource does not have a schema url, return `None`.
196    ///
197    /// [schema url]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url
198    pub fn schema_url(&self) -> Option<&str> {
199        self.inner.schema_url.as_ref().map(|s| s.as_ref())
200    }
201
202    /// Returns the number of attributes for this resource
203    pub fn len(&self) -> usize {
204        self.inner.attrs.len()
205    }
206
207    /// Returns `true` if the resource contains no attributes.
208    pub fn is_empty(&self) -> bool {
209        self.inner.attrs.is_empty()
210    }
211
212    /// Gets an iterator over the attributes of this resource.
213    pub fn iter(&self) -> Iter<'_> {
214        Iter(self.inner.attrs.iter())
215    }
216
217    /// Retrieve the value from resource associate with given key.
218    pub fn get(&self, key: Key) -> Option<Value> {
219        self.inner.attrs.get(&key).cloned()
220    }
221}
222
223/// An iterator over the entries of a `Resource`.
224#[derive(Debug)]
225pub struct Iter<'a>(hash_map::Iter<'a, Key, Value>);
226
227impl<'a> Iterator for Iter<'a> {
228    type Item = (&'a Key, &'a Value);
229
230    fn next(&mut self) -> Option<Self::Item> {
231        self.0.next()
232    }
233}
234
235impl<'a> IntoIterator for &'a Resource {
236    type Item = (&'a Key, &'a Value);
237    type IntoIter = Iter<'a>;
238
239    fn into_iter(self) -> Self::IntoIter {
240        Iter(self.inner.attrs.iter())
241    }
242}
243
244/// ResourceDetector detects OpenTelemetry resource information
245///
246/// Implementations of this trait can be passed to
247/// the [`Resource::from_detectors`] function to generate a Resource from the merged information.
248pub trait ResourceDetector {
249    /// detect returns an initialized Resource based on gathered information.
250    ///
251    /// timeout is used in case the detection operation takes too much time.
252    ///
253    /// If source information to construct a Resource is inaccessible, an empty Resource should be returned
254    ///
255    /// If source information to construct a Resource is invalid, for example,
256    /// missing required values. an empty Resource should be returned.
257    fn detect(&self, timeout: Duration) -> Resource;
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use std::time;
264
265    #[test]
266    fn new_resource() {
267        let args_with_dupe_keys = vec![KeyValue::new("a", ""), KeyValue::new("a", "final")];
268
269        let mut expected_attrs = HashMap::new();
270        expected_attrs.insert(Key::new("a"), Value::from("final"));
271
272        let resource = Resource::new(args_with_dupe_keys);
273        let resource_inner = Arc::try_unwrap(resource.inner).expect("Failed to unwrap Arc");
274        assert_eq!(resource_inner.attrs, expected_attrs);
275        assert_eq!(resource_inner.schema_url, None);
276    }
277
278    #[test]
279    fn merge_resource_key_value_pairs() {
280        let resource_a = Resource::new(vec![
281            KeyValue::new("a", ""),
282            KeyValue::new("b", "b-value"),
283            KeyValue::new("d", "d-value"),
284        ]);
285
286        let resource_b = Resource::new(vec![
287            KeyValue::new("a", "a-value"),
288            KeyValue::new("c", "c-value"),
289            KeyValue::new("d", ""),
290        ]);
291
292        let mut expected_attrs = HashMap::new();
293        expected_attrs.insert(Key::new("a"), Value::from("a-value"));
294        expected_attrs.insert(Key::new("b"), Value::from("b-value"));
295        expected_attrs.insert(Key::new("c"), Value::from("c-value"));
296        expected_attrs.insert(Key::new("d"), Value::from(""));
297
298        let expected_resource = Resource {
299            inner: Arc::new(ResourceInner {
300                attrs: expected_attrs,
301                schema_url: None, // Assuming schema_url handling if needed
302            }),
303        };
304
305        assert_eq!(resource_a.merge(&resource_b), expected_resource);
306    }
307
308    #[test]
309    fn merge_resource_schema_url() {
310        // if both resources contains key value pairs
311        let test_cases = vec![
312            (Some("http://schema/a"), None, Some("http://schema/a")),
313            (Some("http://schema/a"), Some("http://schema/b"), None),
314            (None, Some("http://schema/b"), Some("http://schema/b")),
315            (
316                Some("http://schema/a"),
317                Some("http://schema/a"),
318                Some("http://schema/a"),
319            ),
320            (None, None, None),
321        ];
322
323        for (schema_url_a, schema_url_b, expected_schema_url) in test_cases.into_iter() {
324            let resource_a = Resource::from_schema_url(
325                vec![KeyValue::new("key", "")],
326                schema_url_a.unwrap_or(""),
327            );
328            let resource_b = Resource::from_schema_url(
329                vec![KeyValue::new("key", "")],
330                schema_url_b.unwrap_or(""),
331            );
332
333            let merged_resource = resource_a.merge(&resource_b);
334            let result_schema_url = merged_resource.schema_url();
335
336            assert_eq!(
337                result_schema_url.map(|s| s as &str),
338                expected_schema_url,
339                "Merging schema_url_a {:?} with schema_url_b {:?} did not yield expected result {:?}",
340                schema_url_a, schema_url_b, expected_schema_url
341            );
342        }
343
344        // if only one resource contains key value pairs
345        let resource = Resource::from_schema_url(vec![], "http://schema/a");
346        let other_resource = Resource::new(vec![KeyValue::new("key", "")]);
347
348        assert_eq!(resource.merge(&other_resource).schema_url(), None);
349    }
350
351    #[test]
352    fn detect_resource() {
353        temp_env::with_vars(
354            [
355                (
356                    "OTEL_RESOURCE_ATTRIBUTES",
357                    Some("key=value, k = v , a= x, a=z"),
358                ),
359                ("IRRELEVANT", Some("20200810")),
360            ],
361            || {
362                let detector = EnvResourceDetector::new();
363                let resource = Resource::from_detectors(
364                    time::Duration::from_secs(5),
365                    vec![Box::new(detector)],
366                );
367                assert_eq!(
368                    resource,
369                    Resource::new(vec![
370                        KeyValue::new("key", "value"),
371                        KeyValue::new("k", "v"),
372                        KeyValue::new("a", "x"),
373                        KeyValue::new("a", "z"),
374                    ])
375                )
376            },
377        )
378    }
379}