Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 58 additions & 0 deletions opentelemetry-sdk/src/resource/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -230,6 +235,12 @@ impl Resource {
}
}

impl From<Resource> 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>);
Expand Down Expand Up @@ -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()
);
}
}
153 changes: 153 additions & 0 deletions opentelemetry-sdk/src/trace/provider.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<F>(self, f: F) -> Self
Copy link
Contributor

@scottgerring scottgerring Sep 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The type isn't so long ... would it be cleaner inline? Something like

Suggested change
pub fn transform_resource<F>(self, f: F) -> Self
pub fn transform_resource(self, f: impl FnOnce(Option<&Resource>) -> Resource) -> Self {

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add transform_resource to the other signal builders also?

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;
Expand Down Expand Up @@ -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")
);
}
}
Loading