Skip to content

Commit 9b42048

Browse files
feat(trace-export): map DD span resource to OTLP resource.name attribute (#1811)
# What does this PR do? Adds `resource.name` as a per-span OTLP attribute when mapping Datadog spans to OTLP. The Datadog span resource field now maps to two places: - OtlpSpan.name (existing — the OTLP span name) - span.attributes["resource.name"] (new) This follows the same conditional pattern as operation.name and span.type: the attribute is only emitted when the field is non-empty. # Motivation Spec update to the OTLP trace export RFC: the Datadog resource field should be preserved as a resource.name attribute so downstream consumers can reconstruct the original Datadog resource name independently of the OTLP span name. # Additional Notes This builds on top of feat(otel): add support for OTLP trace export (#1641). The dropped_attributes_count accounting in map_attributes is updated to include the new resource.name slot. # How to test the change? Describe here in detail how the change can be validated. Co-authored-by: rachel.yang <rachel.yang@datadoghq.com>
1 parent 7824b52 commit 9b42048

1 file changed

Lines changed: 62 additions & 0 deletions

File tree

libdd-trace-utils/src/otlp_encoder/mapper.rs

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,14 @@ fn map_attributes<T: TraceData>(span: &Span<T>, resource_service: &str) -> (Vec<
299299
value: AnyValue::StringValue(span_type.to_string()),
300300
});
301301
}
302+
let resource_name = span.resource.borrow();
303+
let has_resource_name = !resource_name.is_empty();
304+
if has_resource_name {
305+
attrs.push(KeyValue {
306+
key: "resource.name".to_string(),
307+
value: AnyValue::StringValue(resource_name.to_string()),
308+
});
309+
}
302310
for (k, v) in span.meta.iter() {
303311
if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
304312
break;
@@ -334,6 +342,7 @@ fn map_attributes<T: TraceData>(span: &Span<T>, resource_service: &str) -> (Vec<
334342
let total = (if has_per_span_service { 1 } else { 0 })
335343
+ (if has_operation_name { 1 } else { 0 })
336344
+ (if has_span_type { 1 } else { 0 })
345+
+ (if has_resource_name { 1 } else { 0 })
337346
+ span.meta.len()
338347
+ span.metrics.len()
339348
+ span.meta_struct.len();
@@ -574,6 +583,59 @@ mod tests {
574583
assert_eq!(kv["value"]["stringValue"], "grpc");
575584
}
576585

586+
#[test]
587+
fn test_resource_name_attribute() {
588+
let resource_info = OtlpResourceInfo::default();
589+
let span: Span<BytesData> = Span {
590+
trace_id: 1,
591+
span_id: 2,
592+
name: libdd_tinybytes::BytesString::from_static("s"),
593+
resource: libdd_tinybytes::BytesString::from_static("GET /api/users"),
594+
start: 0,
595+
duration: 1,
596+
..Default::default()
597+
};
598+
let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
599+
let json = serde_json::to_value(&req).unwrap();
600+
let otlp_span = &json["resourceSpans"][0]["scopeSpans"][0]["spans"][0];
601+
// resource maps to the OTLP span name
602+
assert_eq!(otlp_span["name"], "GET /api/users");
603+
// resource also maps to the resource.name attribute
604+
let kv = otlp_span["attributes"]
605+
.as_array()
606+
.unwrap()
607+
.iter()
608+
.find(|a| a["key"] == "resource.name")
609+
.expect("resource.name attribute not found");
610+
assert_eq!(kv["value"]["stringValue"], "GET /api/users");
611+
}
612+
613+
#[test]
614+
fn test_empty_resource_name_not_emitted() {
615+
// A span with no resource set should not emit a resource.name attribute.
616+
// In practice DD spans always have a resource, but the mapper is defensive about
617+
// empty fields from the wire.
618+
let resource_info = OtlpResourceInfo::default();
619+
let span: Span<BytesData> = Span {
620+
trace_id: 1,
621+
span_id: 2,
622+
name: libdd_tinybytes::BytesString::from_static("s"),
623+
// resource is empty (default)
624+
start: 0,
625+
duration: 1,
626+
..Default::default()
627+
};
628+
let req = map_traces_to_otlp(vec![vec![span]], &resource_info);
629+
let json = serde_json::to_value(&req).unwrap();
630+
let attrs = json["resourceSpans"][0]["scopeSpans"][0]["spans"][0]["attributes"]
631+
.as_array()
632+
.unwrap();
633+
assert!(
634+
!attrs.iter().any(|a| a["key"] == "resource.name"),
635+
"resource.name should not be emitted when resource is empty"
636+
);
637+
}
638+
577639
#[test]
578640
fn test_per_span_service_name_attribute() {
579641
// When span.service differs from the resource-level service, service.name is emitted

0 commit comments

Comments
 (0)