1mod 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#[derive(Debug, Clone, PartialEq)]
42struct ResourceInner {
43    attrs: HashMap<Key, Value>,
44    schema_url: Option<Cow<'static, str>>,
45}
46
47#[derive(Clone, Debug, PartialEq)]
50pub struct Resource {
51    inner: Arc<ResourceInner>,
52}
53
54impl Resource {
55    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    pub fn builder_empty() -> ResourceBuilder {
76        ResourceBuilder {
77            resource: Resource::empty(),
78        }
79    }
80
81    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    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    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    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            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    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        let combined_schema_url = match (&self.inner.schema_url, &other.inner.schema_url) {
186            (Some(url1), Some(url2)) if url1 == url2 => Some(url1.clone()),
188            (Some(_), Some(_)) => None,
190            (None, Some(url)) => Some(url.clone()),
192            (Some(url), _) => Some(url.clone()),
194            (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    pub fn schema_url(&self) -> Option<&str> {
209        self.inner.schema_url.as_ref().map(|s| s.as_ref())
210    }
211
212    pub fn len(&self) -> usize {
214        self.inner.attrs.len()
215    }
216
217    pub fn is_empty(&self) -> bool {
219        self.inner.attrs.is_empty()
220    }
221
222    pub fn iter(&self) -> Iter<'_> {
224        Iter(self.inner.attrs.iter())
225    }
226
227    pub fn get(&self, key: &Key) -> Option<Value> {
229        self.inner.attrs.get(key).cloned()
230    }
231}
232
233#[derive(Debug)]
235pub struct Iter<'a>(hash_map::Iter<'a, Key, Value>);
236
237impl<'a> Iterator for Iter<'a> {
238    type Item = (&'a Key, &'a Value);
239
240    fn next(&mut self) -> Option<Self::Item> {
241        self.0.next()
242    }
243}
244
245impl<'a> IntoIterator for &'a Resource {
246    type Item = (&'a Key, &'a Value);
247    type IntoIter = Iter<'a>;
248
249    fn into_iter(self) -> Self::IntoIter {
250        Iter(self.inner.attrs.iter())
251    }
252}
253
254pub trait ResourceDetector {
259    fn detect(&self) -> Resource;
266}
267
268#[derive(Debug)]
270pub struct ResourceBuilder {
271    resource: Resource,
272}
273
274impl ResourceBuilder {
275    pub fn with_detector(self, detector: Box<dyn ResourceDetector>) -> Self {
277        self.with_detectors(&[detector])
278    }
279
280    pub fn with_detectors(mut self, detectors: &[Box<dyn ResourceDetector>]) -> Self {
282        self.resource = self.resource.merge(&Resource::from_detectors(detectors));
283        self
284    }
285
286    pub fn with_attribute(self, kv: KeyValue) -> Self {
288        self.with_attributes([kv])
289    }
290
291    pub fn with_attributes<T: IntoIterator<Item = KeyValue>>(mut self, kvs: T) -> Self {
293        self.resource = self.resource.merge(&Resource::new(kvs));
294        self
295    }
296
297    pub fn with_service_name(self, name: impl Into<Value>) -> Self {
299        self.with_attribute(KeyValue::new(SERVICE_NAME, name.into()))
300    }
301
302    pub fn with_schema_url<KV, S>(mut self, attributes: KV, schema_url: S) -> Self
313    where
314        KV: IntoIterator<Item = KeyValue>,
315        S: Into<Cow<'static, str>>,
316    {
317        self.resource = Resource::from_schema_url(attributes, schema_url).merge(&self.resource);
318        self
319    }
320
321    pub fn build(self) -> Resource {
323        self.resource
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use rstest::rstest;
330
331    use super::*;
332
333    #[rstest]
334    #[case([KeyValue::new("a", ""), KeyValue::new("a", "final")], [(Key::new("a"), Value::from("final"))])]
335    #[case([KeyValue::new("a", "final"), KeyValue::new("a", "")], [(Key::new("a"), Value::from(""))])]
336    fn new_resource(
337        #[case] given_attributes: [KeyValue; 2],
338        #[case] expected_attrs: [(Key, Value); 1],
339    ) {
340        let expected = HashMap::from_iter(expected_attrs.into_iter());
342
343        let resource = Resource::builder_empty()
345            .with_attributes(given_attributes)
346            .build();
347        let resource_inner = Arc::try_unwrap(resource.inner).expect("Failed to unwrap Arc");
348
349        assert_eq!(resource_inner.attrs, expected);
351        assert_eq!(resource_inner.schema_url, None);
352    }
353
354    #[test]
355    fn merge_resource_key_value_pairs() {
356        let resource_a = Resource::builder_empty()
357            .with_attributes([
358                KeyValue::new("a", ""),
359                KeyValue::new("b", "b-value"),
360                KeyValue::new("d", "d-value"),
361            ])
362            .build();
363
364        let resource_b = Resource::builder_empty()
365            .with_attributes([
366                KeyValue::new("a", "a-value"),
367                KeyValue::new("c", "c-value"),
368                KeyValue::new("d", ""),
369            ])
370            .build();
371
372        let mut expected_attrs = HashMap::new();
373        expected_attrs.insert(Key::new("a"), Value::from("a-value"));
374        expected_attrs.insert(Key::new("b"), Value::from("b-value"));
375        expected_attrs.insert(Key::new("c"), Value::from("c-value"));
376        expected_attrs.insert(Key::new("d"), Value::from(""));
377
378        let expected_resource = Resource {
379            inner: Arc::new(ResourceInner {
380                attrs: expected_attrs,
381                schema_url: None, }),
383        };
384
385        assert_eq!(resource_a.merge(&resource_b), expected_resource);
386    }
387
388    #[rstest]
389    #[case(Some("http://schema/a"), None, Some("http://schema/a"))]
390    #[case(Some("http://schema/a"), Some("http://schema/b"), None)]
391    #[case(None, Some("http://schema/b"), Some("http://schema/b"))]
392    #[case(
393        Some("http://schema/a"),
394        Some("http://schema/a"),
395        Some("http://schema/a")
396    )]
397    #[case(None, None, None)]
398    fn merge_resource_schema_url(
399        #[case] schema_url_a: Option<&'static str>,
400        #[case] schema_url_b: Option<&'static str>,
401        #[case] expected_schema_url: Option<&'static str>,
402    ) {
403        let resource_a =
404            Resource::from_schema_url([KeyValue::new("key", "")], schema_url_a.unwrap_or(""));
405        let resource_b =
406            Resource::from_schema_url([KeyValue::new("key", "")], schema_url_b.unwrap_or(""));
407
408        let merged_resource = resource_a.merge(&resource_b);
409        let result_schema_url = merged_resource.schema_url();
410
411        assert_eq!(
412            result_schema_url.map(|s| s as &str),
413            expected_schema_url,
414            "Merging schema_url_a {:?} with schema_url_b {:?} did not yield expected result {:?}",
415            schema_url_a,
416            schema_url_b,
417            expected_schema_url
418        );
419    }
420
421    #[rstest]
422    #[case(vec![], vec![KeyValue::new("key", "b")], Some("http://schema/a"), None, Some("http://schema/a"))]
423    #[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], Some("http://schema/a"), None, Some("http://schema/a"))]
424    #[case(vec![KeyValue::new("key", "a")], 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"), Some("http://schema/b"), None)]
426    #[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], None, Some("http://schema/b"), Some("http://schema/b"))]
427    fn merge_resource_with_missing_attributes(
428        #[case] key_values_a: Vec<KeyValue>,
429        #[case] key_values_b: Vec<KeyValue>,
430        #[case] schema_url_a: Option<&'static str>,
431        #[case] schema_url_b: Option<&'static str>,
432        #[case] expected_schema_url: Option<&'static str>,
433    ) {
434        let resource = match schema_url_a {
435            Some(schema) => Resource::from_schema_url(key_values_a, schema),
436            None => Resource::new(key_values_a),
437        };
438
439        let other_resource = match schema_url_b {
440            Some(schema) => Resource::builder_empty()
441                .with_schema_url(key_values_b, schema)
442                .build(),
443            None => Resource::new(key_values_b),
444        };
445
446        assert_eq!(
447            resource.merge(&other_resource).schema_url(),
448            expected_schema_url
449        );
450    }
451
452    #[test]
453    fn detect_resource() {
454        temp_env::with_vars(
455            [
456                (
457                    "OTEL_RESOURCE_ATTRIBUTES",
458                    Some("key=value, k = v , a= x, a=z"),
459                ),
460                ("IRRELEVANT", Some("20200810")),
461            ],
462            || {
463                let detector = EnvResourceDetector::new();
464                let resource = Resource::from_detectors(&[Box::new(detector)]);
465                assert_eq!(
466                    resource,
467                    Resource::builder_empty()
468                        .with_attributes([
469                            KeyValue::new("key", "value"),
470                            KeyValue::new("k", "v"),
471                            KeyValue::new("a", "x"),
472                            KeyValue::new("a", "z"),
473                        ])
474                        .build()
475                )
476            },
477        )
478    }
479
480    #[rstest]
481    #[case(Some("http://schema/a"), Some("http://schema/b"), None)]
482    #[case(None, Some("http://schema/b"), Some("http://schema/b"))]
483    #[case(
484        Some("http://schema/a"),
485        Some("http://schema/a"),
486        Some("http://schema/a")
487    )]
488    fn builder_with_schema_url(
489        #[case] schema_url_a: Option<&'static str>,
490        #[case] schema_url_b: Option<&'static str>,
491        #[case] expected_schema_url: Option<&'static str>,
492    ) {
493        let base_builder = if let Some(url) = schema_url_a {
494            ResourceBuilder {
495                resource: Resource::from_schema_url(vec![KeyValue::new("key", "")], url),
496            }
497        } else {
498            ResourceBuilder {
499                resource: Resource::empty(),
500            }
501        };
502
503        let resource = base_builder
504            .with_schema_url(
505                vec![KeyValue::new("key", "")],
506                schema_url_b.expect("should always be Some for this test"),
507            )
508            .build();
509
510        assert_eq!(
511            resource.schema_url().map(|s| s as &str),
512            expected_schema_url,
513            "Merging schema_url_a {:?} with schema_url_b {:?} did not yield expected result {:?}",
514            schema_url_a,
515            schema_url_b,
516            expected_schema_url
517        );
518    }
519
520    #[test]
521    fn builder_detect_resource() {
522        temp_env::with_vars(
523            [
524                (
525                    "OTEL_RESOURCE_ATTRIBUTES",
526                    Some("key=value, k = v , a= x, a=z"),
527                ),
528                ("IRRELEVANT", Some("20200810")),
529            ],
530            || {
531                let resource = Resource::builder_empty()
532                    .with_detector(Box::new(EnvResourceDetector::new()))
533                    .with_service_name("testing_service")
534                    .with_attribute(KeyValue::new("test1", "test_value"))
535                    .with_attributes([
536                        KeyValue::new("test1", "test_value1"),
537                        KeyValue::new("test2", "test_value2"),
538                    ])
539                    .build();
540
541                assert_eq!(
542                    resource,
543                    Resource::builder_empty()
544                        .with_attributes([
545                            KeyValue::new("key", "value"),
546                            KeyValue::new("test1", "test_value1"),
547                            KeyValue::new("test2", "test_value2"),
548                            KeyValue::new(SERVICE_NAME, "testing_service"),
549                            KeyValue::new("k", "v"),
550                            KeyValue::new("a", "x"),
551                            KeyValue::new("a", "z"),
552                        ])
553                        .build()
554                )
555            },
556        )
557    }
558}