mod env;
mod telemetry;
mod attributes;
pub(crate) use attributes::*;
pub use env::EnvResourceDetector;
pub use env::SdkProvidedResourceDetector;
pub use telemetry::TelemetryResourceDetector;
use opentelemetry::{Key, KeyValue, Value};
use std::borrow::Cow;
use std::collections::{hash_map, HashMap};
use std::ops::Deref;
use std::sync::Arc;
#[derive(Debug, Clone, PartialEq)]
struct ResourceInner {
attrs: HashMap<Key, Value>,
schema_url: Option<Cow<'static, str>>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Resource {
inner: Arc<ResourceInner>,
}
impl Resource {
pub fn builder() -> ResourceBuilder {
ResourceBuilder {
resource: Self::from_detectors(&[
Box::new(SdkProvidedResourceDetector),
Box::new(TelemetryResourceDetector),
Box::new(EnvResourceDetector::new()),
]),
}
}
pub fn builder_empty() -> ResourceBuilder {
ResourceBuilder {
resource: Resource::empty(),
}
}
pub(crate) fn empty() -> Self {
Resource {
inner: Arc::new(ResourceInner {
attrs: HashMap::new(),
schema_url: None,
}),
}
}
pub(crate) fn new<T: IntoIterator<Item = KeyValue>>(kvs: T) -> Self {
let mut attrs = HashMap::new();
for kv in kvs {
attrs.insert(kv.key, kv.value);
}
Resource {
inner: Arc::new(ResourceInner {
attrs,
schema_url: None,
}),
}
}
fn from_schema_url<KV, S>(kvs: KV, schema_url: S) -> Self
where
KV: IntoIterator<Item = KeyValue>,
S: Into<Cow<'static, str>>,
{
let schema_url_str = schema_url.into();
let normalized_schema_url = if schema_url_str.is_empty() {
None
} else {
Some(schema_url_str)
};
let mut attrs = HashMap::new();
for kv in kvs {
attrs.insert(kv.key, kv.value);
}
Resource {
inner: Arc::new(ResourceInner {
attrs,
schema_url: normalized_schema_url,
}),
}
}
fn from_detectors(detectors: &[Box<dyn ResourceDetector>]) -> Self {
let mut resource = Resource::empty();
for detector in detectors {
let detected_res = detector.detect();
let inner = Arc::make_mut(&mut resource.inner);
for (key, value) in detected_res.into_iter() {
inner.attrs.insert(Key::new(key.clone()), value.clone());
}
}
resource
}
pub(crate) fn merge<T: Deref<Target = Self>>(&self, other: T) -> Self {
if self.is_empty() && self.schema_url().is_none() {
return other.clone();
}
if other.is_empty() && other.schema_url().is_none() {
return self.clone();
}
let mut combined_attrs = self.inner.attrs.clone();
for (k, v) in other.inner.attrs.iter() {
combined_attrs.insert(k.clone(), v.clone());
}
let combined_schema_url = match (&self.inner.schema_url, &other.inner.schema_url) {
(Some(url1), Some(url2)) if url1 == url2 => Some(url1.clone()),
(Some(_), Some(_)) => None,
(None, Some(url)) => Some(url.clone()),
(Some(url), _) => Some(url.clone()),
(None, None) => None,
};
Resource {
inner: Arc::new(ResourceInner {
attrs: combined_attrs,
schema_url: combined_schema_url,
}),
}
}
pub fn schema_url(&self) -> Option<&str> {
self.inner.schema_url.as_ref().map(|s| s.as_ref())
}
pub fn len(&self) -> usize {
self.inner.attrs.len()
}
pub fn is_empty(&self) -> bool {
self.inner.attrs.is_empty()
}
pub fn iter(&self) -> Iter<'_> {
Iter(self.inner.attrs.iter())
}
pub fn get(&self, key: &Key) -> Option<Value> {
self.inner.attrs.get(key).cloned()
}
pub fn get_ref(&self, key: &Key) -> Option<&Value> {
self.inner.attrs.get(key)
}
}
#[derive(Debug)]
pub struct Iter<'a>(hash_map::Iter<'a, Key, Value>);
impl<'a> Iterator for Iter<'a> {
type Item = (&'a Key, &'a Value);
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}
impl<'a> IntoIterator for &'a Resource {
type Item = (&'a Key, &'a Value);
type IntoIter = Iter<'a>;
fn into_iter(self) -> Self::IntoIter {
Iter(self.inner.attrs.iter())
}
}
pub trait ResourceDetector {
fn detect(&self) -> Resource;
}
#[derive(Debug)]
pub struct ResourceBuilder {
resource: Resource,
}
impl ResourceBuilder {
pub fn with_detector(self, detector: Box<dyn ResourceDetector>) -> Self {
self.with_detectors(&[detector])
}
pub fn with_detectors(mut self, detectors: &[Box<dyn ResourceDetector>]) -> Self {
self.resource = self.resource.merge(&Resource::from_detectors(detectors));
self
}
pub fn with_attribute(self, kv: KeyValue) -> Self {
self.with_attributes([kv])
}
pub fn with_attributes<T: IntoIterator<Item = KeyValue>>(mut self, kvs: T) -> Self {
self.resource = self.resource.merge(&Resource::new(kvs));
self
}
pub fn with_service_name(self, name: impl Into<Value>) -> Self {
self.with_attribute(KeyValue::new(SERVICE_NAME, name.into()))
}
pub fn with_schema_url<KV, S>(mut self, attributes: KV, schema_url: S) -> Self
where
KV: IntoIterator<Item = KeyValue>,
S: Into<Cow<'static, str>>,
{
self.resource = Resource::from_schema_url(attributes, schema_url).merge(&self.resource);
self
}
pub fn build(self) -> Resource {
self.resource
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case([KeyValue::new("a", ""), KeyValue::new("a", "final")], [(Key::new("a"), Value::from("final"))])]
#[case([KeyValue::new("a", "final"), KeyValue::new("a", "")], [(Key::new("a"), Value::from(""))])]
fn new_resource(
#[case] given_attributes: [KeyValue; 2],
#[case] expected_attrs: [(Key, Value); 1],
) {
let expected = HashMap::from_iter(expected_attrs.into_iter());
let resource = Resource::builder_empty()
.with_attributes(given_attributes)
.build();
let resource_inner = Arc::try_unwrap(resource.inner).expect("Failed to unwrap Arc");
assert_eq!(resource_inner.attrs, expected);
assert_eq!(resource_inner.schema_url, None);
}
#[test]
fn merge_resource_key_value_pairs() {
let resource_a = Resource::builder_empty()
.with_attributes([
KeyValue::new("a", ""),
KeyValue::new("b", "b-value"),
KeyValue::new("d", "d-value"),
])
.build();
let resource_b = Resource::builder_empty()
.with_attributes([
KeyValue::new("a", "a-value"),
KeyValue::new("c", "c-value"),
KeyValue::new("d", ""),
])
.build();
let mut expected_attrs = HashMap::new();
expected_attrs.insert(Key::new("a"), Value::from("a-value"));
expected_attrs.insert(Key::new("b"), Value::from("b-value"));
expected_attrs.insert(Key::new("c"), Value::from("c-value"));
expected_attrs.insert(Key::new("d"), Value::from(""));
let expected_resource = Resource {
inner: Arc::new(ResourceInner {
attrs: expected_attrs,
schema_url: None, }),
};
assert_eq!(resource_a.merge(&resource_b), expected_resource);
}
#[rstest]
#[case(Some("http://schema/a"), None, Some("http://schema/a"))]
#[case(Some("http://schema/a"), Some("http://schema/b"), None)]
#[case(None, Some("http://schema/b"), Some("http://schema/b"))]
#[case(
Some("http://schema/a"),
Some("http://schema/a"),
Some("http://schema/a")
)]
#[case(None, None, None)]
fn merge_resource_schema_url(
#[case] schema_url_a: Option<&'static str>,
#[case] schema_url_b: Option<&'static str>,
#[case] expected_schema_url: Option<&'static str>,
) {
let resource_a =
Resource::from_schema_url([KeyValue::new("key", "")], schema_url_a.unwrap_or(""));
let resource_b =
Resource::from_schema_url([KeyValue::new("key", "")], schema_url_b.unwrap_or(""));
let merged_resource = resource_a.merge(&resource_b);
let result_schema_url = merged_resource.schema_url();
assert_eq!(
result_schema_url.map(|s| s as &str),
expected_schema_url,
"Merging schema_url_a {schema_url_a:?} with schema_url_b {schema_url_b:?} did not yield expected result {expected_schema_url:?}"
);
}
#[rstest]
#[case(vec![], vec![KeyValue::new("key", "b")], Some("http://schema/a"), None, Some("http://schema/a"))]
#[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], Some("http://schema/a"), None, Some("http://schema/a"))]
#[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], Some("http://schema/a"), None, Some("http://schema/a"))]
#[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], Some("http://schema/a"), Some("http://schema/b"), None)]
#[case(vec![KeyValue::new("key", "a")], vec![KeyValue::new("key", "b")], None, Some("http://schema/b"), Some("http://schema/b"))]
fn merge_resource_with_missing_attributes(
#[case] key_values_a: Vec<KeyValue>,
#[case] key_values_b: Vec<KeyValue>,
#[case] schema_url_a: Option<&'static str>,
#[case] schema_url_b: Option<&'static str>,
#[case] expected_schema_url: Option<&'static str>,
) {
let resource = match schema_url_a {
Some(schema) => Resource::from_schema_url(key_values_a, schema),
None => Resource::new(key_values_a),
};
let other_resource = match schema_url_b {
Some(schema) => Resource::builder_empty()
.with_schema_url(key_values_b, schema)
.build(),
None => Resource::new(key_values_b),
};
assert_eq!(
resource.merge(&other_resource).schema_url(),
expected_schema_url
);
}
#[test]
fn detect_resource() {
temp_env::with_vars(
[
(
"OTEL_RESOURCE_ATTRIBUTES",
Some("key=value, k = v , a= x, a=z"),
),
("IRRELEVANT", Some("20200810")),
],
|| {
let detector = EnvResourceDetector::new();
let resource = Resource::from_detectors(&[Box::new(detector)]);
assert_eq!(
resource,
Resource::builder_empty()
.with_attributes([
KeyValue::new("key", "value"),
KeyValue::new("k", "v"),
KeyValue::new("a", "x"),
KeyValue::new("a", "z"),
])
.build()
)
},
)
}
#[rstest]
#[case(Some("http://schema/a"), Some("http://schema/b"), None)]
#[case(None, Some("http://schema/b"), Some("http://schema/b"))]
#[case(
Some("http://schema/a"),
Some("http://schema/a"),
Some("http://schema/a")
)]
fn builder_with_schema_url(
#[case] schema_url_a: Option<&'static str>,
#[case] schema_url_b: Option<&'static str>,
#[case] expected_schema_url: Option<&'static str>,
) {
let base_builder = if let Some(url) = schema_url_a {
ResourceBuilder {
resource: Resource::from_schema_url(vec![KeyValue::new("key", "")], url),
}
} else {
ResourceBuilder {
resource: Resource::empty(),
}
};
let resource = base_builder
.with_schema_url(
vec![KeyValue::new("key", "")],
schema_url_b.expect("should always be Some for this test"),
)
.build();
assert_eq!(
resource.schema_url().map(|s| s as &str),
expected_schema_url,
"Merging schema_url_a {schema_url_a:?} with schema_url_b {schema_url_b:?} did not yield expected result {expected_schema_url:?}"
);
}
#[test]
fn builder_detect_resource() {
temp_env::with_vars(
[
(
"OTEL_RESOURCE_ATTRIBUTES",
Some("key=value, k = v , a= x, a=z"),
),
("IRRELEVANT", Some("20200810")),
],
|| {
let resource = Resource::builder_empty()
.with_detector(Box::new(EnvResourceDetector::new()))
.with_service_name("testing_service")
.with_attribute(KeyValue::new("test1", "test_value"))
.with_attributes([
KeyValue::new("test1", "test_value1"),
KeyValue::new("test2", "test_value2"),
])
.build();
assert_eq!(
resource,
Resource::builder_empty()
.with_attributes([
KeyValue::new("key", "value"),
KeyValue::new("test1", "test_value1"),
KeyValue::new("test2", "test_value2"),
KeyValue::new(SERVICE_NAME, "testing_service"),
KeyValue::new("k", "v"),
KeyValue::new("a", "x"),
KeyValue::new("a", "z"),
])
.build()
)
},
)
}
}