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 pub fn get_ref(&self, key: &Key) -> Option<&Value> {
234 self.inner.attrs.get(key)
235 }
236}
237
238#[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
259pub trait ResourceDetector {
264 fn detect(&self) -> Resource;
271}
272
273#[derive(Debug)]
275pub struct ResourceBuilder {
276 resource: Resource,
277}
278
279impl ResourceBuilder {
280 pub fn with_detector(self, detector: Box<dyn ResourceDetector>) -> Self {
282 self.with_detectors(&[detector])
283 }
284
285 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 pub fn with_attribute(self, kv: KeyValue) -> Self {
293 self.with_attributes([kv])
294 }
295
296 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 pub fn with_service_name(self, name: impl Into<Value>) -> Self {
304 self.with_attribute(KeyValue::new(SERVICE_NAME, name.into()))
305 }
306
307 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 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 let expected = HashMap::from_iter(expected_attrs.into_iter());
347
348 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_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, }),
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}