Skip to main content

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;
38
39/// Inner structure of `Resource` holding the actual data.
40/// This structure is designed to be shared among `Resource` instances via `Arc`.
41#[derive(Debug, Clone, PartialEq)]
42struct ResourceInner {
43    attrs: HashMap<Key, Value>,
44    schema_url: Option<Cow<'static, str>>,
45}
46
47/// An immutable representation of the entity producing telemetry as attributes.
48/// Utilizes `Arc` for efficient sharing and cloning.
49#[derive(Clone, Debug, PartialEq)]
50pub struct Resource {
51    inner: Arc<ResourceInner>,
52}
53
54impl Resource {
55    /// Creates a [ResourceBuilder] that allows you to configure multiple aspects of the Resource.
56    ///
57    /// This [ResourceBuilder] will include the following [ResourceDetector]s:
58    /// - [SdkProvidedResourceDetector]
59    /// - [TelemetryResourceDetector]
60    /// - [EnvResourceDetector]
61    ///   If you'd like to start from an empty resource, use [Resource::builder_empty].
62    pub fn builder() -> ResourceBuilder {
63        ResourceBuilder {
64            resource: Self::from_detectors(&[
65                Box::new(SdkProvidedResourceDetector),
66                Box::new(TelemetryResourceDetector),
67                Box::new(EnvResourceDetector::new()),
68            ]),
69        }
70    }
71
72    /// Creates a [ResourceBuilder] that allows you to configure multiple aspects of the Resource.
73    ///
74    /// This [ResourceBuilder] will not include any attributes or [ResourceDetector]s by default.
75    pub fn builder_empty() -> ResourceBuilder {
76        ResourceBuilder {
77            resource: Resource::empty(),
78        }
79    }
80
81    /// Creates an empty resource.
82    /// This is the basic constructor that initializes a resource with no attributes and no schema URL.
83    pub(crate) fn empty() -> Self {
84        Resource {
85            inner: Arc::new(ResourceInner {
86                attrs: HashMap::new(),
87                schema_url: None,
88            }),
89        }
90    }
91
92    /// Create a new `Resource` from key value pairs.
93    ///
94    /// Values are de-duplicated by key, and the last key-value pair will be retained
95    pub(crate) fn new<T: IntoIterator<Item = KeyValue>>(kvs: T) -> Self {
96        let mut attrs = HashMap::new();
97        for kv in kvs {
98            attrs.insert(kv.key, kv.value);
99        }
100
101        Resource {
102            inner: Arc::new(ResourceInner {
103                attrs,
104                schema_url: None,
105            }),
106        }
107    }
108
109    /// Create a new `Resource` from a key value pairs and [schema url].
110    ///
111    /// Values are de-duplicated by key, and the first key-value pair with a non-empty string value
112    /// will be retained.
113    ///
114    /// schema_url must be a valid URL using HTTP or HTTPS protocol.
115    ///
116    /// [schema url]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url
117    fn from_schema_url<KV, S>(kvs: KV, schema_url: S) -> Self
118    where
119        KV: IntoIterator<Item = KeyValue>,
120        S: Into<Cow<'static, str>>,
121    {
122        let schema_url_str = schema_url.into();
123        let normalized_schema_url = if schema_url_str.is_empty() {
124            None
125        } else {
126            Some(schema_url_str)
127        };
128        let mut attrs = HashMap::new();
129        for kv in kvs {
130            attrs.insert(kv.key, kv.value);
131        }
132        Resource {
133            inner: Arc::new(ResourceInner {
134                attrs,
135                schema_url: normalized_schema_url,
136            }),
137        }
138    }
139
140    /// Create a new `Resource` from resource detectors.
141    fn from_detectors(detectors: &[Box<dyn ResourceDetector>]) -> Self {
142        let mut resource = Resource::empty();
143        for detector in detectors {
144            let detected_res = detector.detect();
145            // This call ensures that if the Arc is not uniquely owned,
146            // the data is cloned before modification, preserving safety.
147            // If the Arc is uniquely owned, it simply returns a mutable reference to the data.
148            let inner = Arc::make_mut(&mut resource.inner);
149            for (key, value) in detected_res.into_iter() {
150                inner.attrs.insert(Key::new(key.clone()), value.clone());
151            }
152        }
153
154        resource
155    }
156
157    /// Create a new `Resource` by combining two resources.
158    ///
159    /// ### Key value pairs
160    /// Keys from the `other` resource have priority over keys from this resource, even if the
161    /// updated value is empty.
162    ///
163    /// ### [Schema url]
164    /// If both of the resource are not empty. Schema url is determined by the following rules, in order:
165    /// 1. If this resource has a schema url, it will be used.
166    /// 2. If this resource does not have a schema url, and the other resource has a schema url, it will be used.
167    /// 3. If both resources have a schema url and it's the same, it will be used.
168    /// 4. If both resources have a schema url and it's different, the schema url will be empty.
169    /// 5. If both resources do not have a schema url, the schema url will be empty.
170    ///
171    /// [Schema url]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url
172    pub(crate) fn merge<T: Deref<Target = Self>>(&self, other: T) -> Self {
173        if self.is_empty() && self.schema_url().is_none() {
174            return other.clone();
175        }
176        if other.is_empty() && other.schema_url().is_none() {
177            return self.clone();
178        }
179        let mut combined_attrs = self.inner.attrs.clone();
180        for (k, v) in other.inner.attrs.iter() {
181            combined_attrs.insert(k.clone(), v.clone());
182        }
183
184        // Resolve the schema URL according to the precedence rules
185        let combined_schema_url = match (&self.inner.schema_url, &other.inner.schema_url) {
186            // If both resources have a schema URL and it's the same, use it
187            (Some(url1), Some(url2)) if url1 == url2 => Some(url1.clone()),
188            // If both resources have a schema URL but they are not the same, the schema URL will be empty
189            (Some(_), Some(_)) => None,
190            // If this resource does not have a schema URL, and the other resource has a schema URL, it will be used
191            (None, Some(url)) => Some(url.clone()),
192            // If this resource has a schema URL, it will be used (covers case 1 and any other cases where `self` has a schema URL)
193            (Some(url), _) => Some(url.clone()),
194            // If both resources do not have a schema URL, the schema URL will be empty
195            (None, None) => None,
196        };
197        Resource {
198            inner: Arc::new(ResourceInner {
199                attrs: combined_attrs,
200                schema_url: combined_schema_url,
201            }),
202        }
203    }
204
205    /// Return the [schema url] of the resource. If the resource does not have a schema url, return `None`.
206    ///
207    /// [schema url]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url
208    pub fn schema_url(&self) -> Option<&str> {
209        self.inner.schema_url.as_ref().map(|s| s.as_ref())
210    }
211
212    /// Returns the number of attributes for this resource
213    pub fn len(&self) -> usize {
214        self.inner.attrs.len()
215    }
216
217    /// Returns `true` if the resource contains no attributes.
218    pub fn is_empty(&self) -> bool {
219        self.inner.attrs.is_empty()
220    }
221
222    /// Gets an iterator over the attributes of this resource.
223    pub fn iter(&self) -> Iter<'_> {
224        Iter(self.inner.attrs.iter())
225    }
226
227    /// Retrieve the value from resource associate with given key.
228    pub fn get(&self, key: &Key) -> Option<Value> {
229        self.inner.attrs.get(key).cloned()
230    }
231
232    /// Returns a reference to the value for the resource associated with the given key without cloning.
233    pub fn get_ref(&self, key: &Key) -> Option<&Value> {
234        self.inner.attrs.get(key)
235    }
236}
237
238/// An iterator over the entries of a `Resource`.
239#[derive(Debug)]
240pub struct Iter<'a>(hash_map::Iter<'a, Key, Value>);
241
242impl<'a> Iterator for Iter<'a> {
243    type Item = (&'a Key, &'a Value);
244
245    fn next(&mut self) -> Option<Self::Item> {
246        self.0.next()
247    }
248}
249
250impl<'a> IntoIterator for &'a Resource {
251    type Item = (&'a Key, &'a Value);
252    type IntoIter = Iter<'a>;
253
254    fn into_iter(self) -> Self::IntoIter {
255        Iter(self.inner.attrs.iter())
256    }
257}
258
259/// ResourceDetector detects OpenTelemetry resource information
260///
261/// Implementations of this trait can be passed to
262/// the [`ResourceBuilder::with_detectors`] function to generate a Resource from the merged information.
263pub trait ResourceDetector {
264    /// detect returns an initialized Resource based on gathered information.
265    ///
266    /// If source information to construct a Resource is inaccessible, an empty Resource should be returned
267    ///
268    /// If source information to construct a Resource is invalid, for example,
269    /// missing required values. an empty Resource should be returned.
270    fn detect(&self) -> Resource;
271}
272
273/// Builder for [Resource]
274#[derive(Debug)]
275pub struct ResourceBuilder {
276    resource: Resource,
277}
278
279impl ResourceBuilder {
280    /// Add a single [ResourceDetector] to your resource.
281    pub fn with_detector(self, detector: Box<dyn ResourceDetector>) -> Self {
282        self.with_detectors(&[detector])
283    }
284
285    /// Add multiple [ResourceDetector]s to your resource.
286    pub fn with_detectors(mut self, detectors: &[Box<dyn ResourceDetector>]) -> Self {
287        self.resource = self.resource.merge(&Resource::from_detectors(detectors));
288        self
289    }
290
291    /// Add a [KeyValue] to the resource.
292    pub fn with_attribute(self, kv: KeyValue) -> Self {
293        self.with_attributes([kv])
294    }
295
296    /// Add multiple [KeyValue]s to the resource.
297    pub fn with_attributes<T: IntoIterator<Item = KeyValue>>(mut self, kvs: T) -> Self {
298        self.resource = self.resource.merge(&Resource::new(kvs));
299        self
300    }
301
302    /// Add `service.name` resource attribute.
303    pub fn with_service_name(self, name: impl Into<Value>) -> Self {
304        self.with_attribute(KeyValue::new(SERVICE_NAME, name.into()))
305    }
306
307    /// This will merge the provided `schema_url` with the current state of the Resource being built. It
308    /// will use the following rules to determine which `schema_url` should be used.
309    ///
310    /// ### [Schema url]
311    /// Schema url is determined by the following rules, in order:
312    /// 1. If the current builder resource doesn't have a `schema_url`, the provided `schema_url` will be used.
313    /// 2. If the current builder resource has a `schema_url`, and the provided `schema_url` is different from the builder resource, `schema_url` will be empty.
314    /// 3. If the provided `schema_url` is the same as the current builder resource, it will be used.
315    ///
316    /// [Schema url]: https://github.com/open-telemetry/opentelemetry-specification/blob/v1.9.0/specification/schemas/overview.md#schema-url
317    pub fn with_schema_url<KV, S>(mut self, attributes: KV, schema_url: S) -> Self
318    where
319        KV: IntoIterator<Item = KeyValue>,
320        S: Into<Cow<'static, str>>,
321    {
322        self.resource = Resource::from_schema_url(attributes, schema_url).merge(&self.resource);
323        self
324    }
325
326    /// Create a [Resource] with the options provided to the [ResourceBuilder].
327    pub fn build(self) -> Resource {
328        self.resource
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use rstest::rstest;
335
336    use super::*;
337
338    #[rstest]
339    #[case([KeyValue::new("a", ""), KeyValue::new("a", "final")], [(Key::new("a"), Value::from("final"))])]
340    #[case([KeyValue::new("a", "final"), KeyValue::new("a", "")], [(Key::new("a"), Value::from(""))])]
341    fn new_resource(
342        #[case] given_attributes: [KeyValue; 2],
343        #[case] expected_attrs: [(Key, Value); 1],
344    ) {
345        // Arrange
346        let expected = HashMap::from_iter(expected_attrs.into_iter());
347
348        // Act
349        let resource = Resource::builder_empty()
350            .with_attributes(given_attributes)
351            .build();
352        let resource_inner = Arc::try_unwrap(resource.inner).expect("Failed to unwrap Arc");
353
354        // Assert
355        assert_eq!(resource_inner.attrs, expected);
356        assert_eq!(resource_inner.schema_url, None);
357    }
358
359    #[test]
360    fn merge_resource_key_value_pairs() {
361        let resource_a = Resource::builder_empty()
362            .with_attributes([
363                KeyValue::new("a", ""),
364                KeyValue::new("b", "b-value"),
365                KeyValue::new("d", "d-value"),
366            ])
367            .build();
368
369        let resource_b = Resource::builder_empty()
370            .with_attributes([
371                KeyValue::new("a", "a-value"),
372                KeyValue::new("c", "c-value"),
373                KeyValue::new("d", ""),
374            ])
375            .build();
376
377        let mut expected_attrs = HashMap::new();
378        expected_attrs.insert(Key::new("a"), Value::from("a-value"));
379        expected_attrs.insert(Key::new("b"), Value::from("b-value"));
380        expected_attrs.insert(Key::new("c"), Value::from("c-value"));
381        expected_attrs.insert(Key::new("d"), Value::from(""));
382
383        let expected_resource = Resource {
384            inner: Arc::new(ResourceInner {
385                attrs: expected_attrs,
386                schema_url: None, // Assuming schema_url handling if needed
387            }),
388        };
389
390        assert_eq!(resource_a.merge(&resource_b), expected_resource);
391    }
392
393    #[rstest]
394    #[case(Some("http://schema/a"), None, Some("http://schema/a"))]
395    #[case(Some("http://schema/a"), Some("http://schema/b"), None)]
396    #[case(None, Some("http://schema/b"), Some("http://schema/b"))]
397    #[case(
398        Some("http://schema/a"),
399        Some("http://schema/a"),
400        Some("http://schema/a")
401    )]
402    #[case(None, None, None)]
403    fn merge_resource_schema_url(
404        #[case] schema_url_a: Option<&'static str>,
405        #[case] schema_url_b: Option<&'static str>,
406        #[case] expected_schema_url: Option<&'static str>,
407    ) {
408        let resource_a =
409            Resource::from_schema_url([KeyValue::new("key", "")], schema_url_a.unwrap_or(""));
410        let resource_b =
411            Resource::from_schema_url([KeyValue::new("key", "")], schema_url_b.unwrap_or(""));
412
413        let merged_resource = resource_a.merge(&resource_b);
414        let result_schema_url = merged_resource.schema_url();
415
416        assert_eq!(
417            result_schema_url.map(|s| s as &str),
418            expected_schema_url,
419            "Merging schema_url_a {schema_url_a:?} with schema_url_b {schema_url_b:?} did not yield expected result {expected_schema_url:?}"
420        );
421    }
422
423    #[rstest]
424    #[case(vec![], vec![KeyValue::new("key", "b")], Some("http://schema/a"), None, Some("http://schema/a"))]
425    #[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], Some("http://schema/a"), None, Some("http://schema/a"))]
426    #[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], Some("http://schema/a"), None, Some("http://schema/a"))]
427    #[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], Some("http://schema/a"), Some("http://schema/b"), None)]
428    #[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], None, Some("http://schema/b"), Some("http://schema/b"))]
429    fn merge_resource_with_missing_attributes(
430        #[case] key_values_a: Vec<KeyValue>,
431        #[case] key_values_b: Vec<KeyValue>,
432        #[case] schema_url_a: Option<&'static str>,
433        #[case] schema_url_b: Option<&'static str>,
434        #[case] expected_schema_url: Option<&'static str>,
435    ) {
436        let resource = match schema_url_a {
437            Some(schema) => Resource::from_schema_url(key_values_a, schema),
438            None => Resource::new(key_values_a),
439        };
440
441        let other_resource = match schema_url_b {
442            Some(schema) => Resource::builder_empty()
443                .with_schema_url(key_values_b, schema)
444                .build(),
445            None => Resource::new(key_values_b),
446        };
447
448        assert_eq!(
449            resource.merge(&other_resource).schema_url(),
450            expected_schema_url
451        );
452    }
453
454    #[test]
455    fn detect_resource() {
456        temp_env::with_vars(
457            [
458                (
459                    "OTEL_RESOURCE_ATTRIBUTES",
460                    Some("key=value, k = v , a= x, a=z"),
461                ),
462                ("IRRELEVANT", Some("20200810")),
463            ],
464            || {
465                let detector = EnvResourceDetector::new();
466                let resource = Resource::from_detectors(&[Box::new(detector)]);
467                assert_eq!(
468                    resource,
469                    Resource::builder_empty()
470                        .with_attributes([
471                            KeyValue::new("key", "value"),
472                            KeyValue::new("k", "v"),
473                            KeyValue::new("a", "x"),
474                            KeyValue::new("a", "z"),
475                        ])
476                        .build()
477                )
478            },
479        )
480    }
481
482    #[rstest]
483    #[case(Some("http://schema/a"), Some("http://schema/b"), None)]
484    #[case(None, Some("http://schema/b"), Some("http://schema/b"))]
485    #[case(
486        Some("http://schema/a"),
487        Some("http://schema/a"),
488        Some("http://schema/a")
489    )]
490    fn builder_with_schema_url(
491        #[case] schema_url_a: Option<&'static str>,
492        #[case] schema_url_b: Option<&'static str>,
493        #[case] expected_schema_url: Option<&'static str>,
494    ) {
495        let base_builder = if let Some(url) = schema_url_a {
496            ResourceBuilder {
497                resource: Resource::from_schema_url(vec![KeyValue::new("key", "")], url),
498            }
499        } else {
500            ResourceBuilder {
501                resource: Resource::empty(),
502            }
503        };
504
505        let resource = base_builder
506            .with_schema_url(
507                vec![KeyValue::new("key", "")],
508                schema_url_b.expect("should always be Some for this test"),
509            )
510            .build();
511
512        assert_eq!(
513            resource.schema_url().map(|s| s as &str),
514            expected_schema_url,
515            "Merging schema_url_a {schema_url_a:?} with schema_url_b {schema_url_b:?} did not yield expected result {expected_schema_url:?}"
516        );
517    }
518
519    #[test]
520    fn builder_detect_resource() {
521        temp_env::with_vars(
522            [
523                (
524                    "OTEL_RESOURCE_ATTRIBUTES",
525                    Some("key=value, k = v , a= x, a=z"),
526                ),
527                ("IRRELEVANT", Some("20200810")),
528            ],
529            || {
530                let resource = Resource::builder_empty()
531                    .with_detector(Box::new(EnvResourceDetector::new()))
532                    .with_service_name("testing_service")
533                    .with_attribute(KeyValue::new("test1", "test_value"))
534                    .with_attributes([
535                        KeyValue::new("test1", "test_value1"),
536                        KeyValue::new("test2", "test_value2"),
537                    ])
538                    .build();
539
540                assert_eq!(
541                    resource,
542                    Resource::builder_empty()
543                        .with_attributes([
544                            KeyValue::new("key", "value"),
545                            KeyValue::new("test1", "test_value1"),
546                            KeyValue::new("test2", "test_value2"),
547                            KeyValue::new(SERVICE_NAME, "testing_service"),
548                            KeyValue::new("k", "v"),
549                            KeyValue::new("a", "x"),
550                            KeyValue::new("a", "z"),
551                        ])
552                        .build()
553                )
554            },
555        )
556    }
557}