diff --git a/opentelemetry-sdk/src/resource/mod.rs b/opentelemetry-sdk/src/resource/mod.rs index 054a472ea3..ded5826559 100644 --- a/opentelemetry-sdk/src/resource/mod.rs +++ b/opentelemetry-sdk/src/resource/mod.rs @@ -78,6 +78,11 @@ impl Resource { } } + /// Returns a [ResourceBuilder] initialized from this resource. + pub fn into_builder(self) -> ResourceBuilder { + ResourceBuilder { resource: self } + } + /// Creates an empty resource. /// This is the basic constructor that initializes a resource with no attributes and no schema URL. pub(crate) fn empty() -> Self { @@ -230,6 +235,12 @@ impl Resource { } } +impl From for ResourceBuilder { + fn from(resource: Resource) -> Self { + resource.into_builder() + } +} + /// An iterator over the entries of a `Resource`. #[derive(Debug)] pub struct Iter<'a>(hash_map::Iter<'a, Key, Value>); @@ -549,4 +560,51 @@ mod tests { }, ) } + + #[test] + fn into_builder() { + // Create a resource with some attributes and schema URL + let original_resource = Resource::from_schema_url( + [ + KeyValue::new("service.name", "test-service"), + KeyValue::new("service.version", "1.0.0"), + KeyValue::new("environment", "test"), + ], + "http://example.com/schema", + ); + + // Get a builder from the resource + let builder: ResourceBuilder = original_resource.clone().into(); + let rebuilt_resource = builder.build(); + + // The rebuilt resource should be identical to the original + assert_eq!(original_resource, rebuilt_resource); + assert_eq!( + original_resource.schema_url(), + rebuilt_resource.schema_url() + ); + assert_eq!(original_resource.len(), rebuilt_resource.len()); + + // Verify we can modify the builder and get a different resource + let modified_resource = original_resource + .clone() + .into_builder() + .with_attribute(KeyValue::new("new_key", "new_value")) + .build(); + + // The modified resource should have the original attributes plus the new one + assert_eq!(modified_resource.len(), original_resource.len() + 1); + assert_eq!( + modified_resource.get(&Key::new("new_key")), + Some(Value::from("new_value")) + ); + assert_eq!( + modified_resource.get(&Key::new("service.name")), + Some(Value::from("test-service")) + ); + assert_eq!( + modified_resource.schema_url(), + original_resource.schema_url() + ); + } } diff --git a/opentelemetry-sdk/src/trace/provider.rs b/opentelemetry-sdk/src/trace/provider.rs index 2b05f89aea..933a527a99 100644 --- a/opentelemetry-sdk/src/trace/provider.rs +++ b/opentelemetry-sdk/src/trace/provider.rs @@ -422,6 +422,25 @@ impl TracerProviderBuilder { TracerProviderBuilder { resource, ..self } } + /// Transforms the current [Resource] in this builder using a function. + /// + /// The transformed [Resource] represents the entity producing telemetry and + /// is associated with all [Tracer]s the [SdkTracerProvider] will create. + /// + /// By default, if this option is not used, the default [Resource] will be used. + /// + /// *Note*: Calls to this method are not additive, the returned resource will + /// completely replace the existing [Resource]. + /// + /// [Tracer]: opentelemetry::trace::Tracer + pub fn transform_resource(self, f: F) -> Self + where + F: FnOnce(Option<&Resource>) -> Resource, + { + let resource = Some(f(self.resource.as_ref())); + TracerProviderBuilder { resource, ..self } + } + /// Create a new provider from this configuration. pub fn build(self) -> SdkTracerProvider { let mut config = self.config; @@ -882,4 +901,138 @@ mod tests { // Verify that shutdown was only called once, even after drop assert_eq!(shutdown_count.load(Ordering::SeqCst), 1); } + + #[test] + fn transform_resource_with_none() { + // Test transform_resource when no resource is set initially + let provider = super::SdkTracerProvider::builder() + .transform_resource(|existing| { + assert!(existing.is_none()); + Resource::new(vec![KeyValue::new("transformed", "value")]) + }) + .build(); + + assert_eq!( + provider.config().resource.get(&Key::new("transformed")), + Some(Value::from("value")) + ); + } + + #[test] + fn transform_resource_with_existing() { + // Test transform_resource when a resource already exists + let provider = super::SdkTracerProvider::builder() + .with_resource(Resource::new(vec![KeyValue::new("original", "value1")])) + .transform_resource(|existing| { + assert!(existing.is_some()); + let existing_resource = existing.unwrap(); + assert_eq!( + existing_resource.get(&Key::new("original")), + Some(Value::from("value1")) + ); + + // Create a new resource that merges with the existing one + existing_resource + .merge(&Resource::new(vec![KeyValue::new("transformed", "value2")])) + }) + .build(); + + // Both original and transformed attributes should be present + assert_eq!( + provider.config().resource.get(&Key::new("original")), + Some(Value::from("value1")) + ); + assert_eq!( + provider.config().resource.get(&Key::new("transformed")), + Some(Value::from("value2")) + ); + } + + #[test] + fn transform_resource_replace_existing() { + // Test transform_resource that completely replaces the existing resource + let provider = super::SdkTracerProvider::builder() + .with_resource(Resource::new(vec![KeyValue::new("original", "value1")])) + .transform_resource(|_existing| { + // Ignore existing and create a completely new resource + Resource::new(vec![KeyValue::new("replaced", "new_value")]) + }) + .build(); + + // Only the new resource should be present + assert_eq!(provider.config().resource.get(&Key::new("original")), None); + assert_eq!( + provider.config().resource.get(&Key::new("replaced")), + Some(Value::from("new_value")) + ); + } + + #[test] + fn transform_resource_multiple_calls() { + // Test multiple calls to transform_resource + let provider = super::SdkTracerProvider::builder() + .with_resource(Resource::new(vec![KeyValue::new("initial", "value")])) + .transform_resource(|existing| { + existing.unwrap().merge(&Resource::new(vec![KeyValue::new( + "first_transform", + "value1", + )])) + }) + .transform_resource(|existing| { + existing.unwrap().merge(&Resource::new(vec![KeyValue::new( + "second_transform", + "value2", + )])) + }) + .build(); + + // All attributes should be present + assert_eq!( + provider.config().resource.get(&Key::new("initial")), + Some(Value::from("value")) + ); + assert_eq!( + provider.config().resource.get(&Key::new("first_transform")), + Some(Value::from("value1")) + ); + assert_eq!( + provider + .config() + .resource + .get(&Key::new("second_transform")), + Some(Value::from("value2")) + ); + } + + #[test] + fn transform_resource_with_schema_url() { + // Test transform_resource preserving schema URL + let provider = super::SdkTracerProvider::builder() + .with_resource( + Resource::builder_empty() + .with_schema_url( + vec![KeyValue::new("original", "value")], + "http://example.com/schema", + ) + .build(), + ) + .transform_resource(|existing| { + let existing_resource = existing.unwrap(); + existing_resource.merge(&Resource::new(vec![KeyValue::new("added", "new_value")])) + }) + .build(); + + assert_eq!( + provider.config().resource.get(&Key::new("original")), + Some(Value::from("value")) + ); + assert_eq!( + provider.config().resource.get(&Key::new("added")), + Some(Value::from("new_value")) + ); + assert_eq!( + provider.config().resource.schema_url(), + Some("http://example.com/schema") + ); + } }