Skip to content

Commit 7805aa7

Browse files
feat(otel_mapping): unify the otel to dd mapping code (#31)
# What does this PR do? The OTEL to DD span mapping code was duplicated in both the datadog-opentelemetry crate and the sampler crate. This PR extracts this code to a new crate, which both crate import and removes the code duplication in the sampler. To make the code generic enough, this modifies the sampler function to pass a `PreSampledSpan` which bundles attributes and otel resource in one object implementing the same trait as the Span we pass on export. This PR shouldn't modify the behaviour of the code. # Motivation What inspired you to submit this pull request? # Additional Notes Anything else we should know when reviewing?
1 parent cee8b42 commit 7805aa7

File tree

22 files changed

+615
-1597
lines changed

22 files changed

+615
-1597
lines changed

Cargo.lock

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
members = [
66
"datadog-opentelemetry",
77
"datadog-opentelemetry/examples/propagator",
8+
"datadog-opentelemetry-mappings",
89
"dd-trace",
910
"dd-trace-propagation",
1011
"dd-trace-sampling",

LICENSE-3rdparty.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
root_name: datadog-opentelemetry, dd-trace, dd-trace-propagation, dd-trace-sampling
1+
root_name: datadog-opentelemetry, datadog-opentelemetry-mappings, dd-trace, dd-trace-propagation, dd-trace-sampling
22
third_party_libraries:
33
- package_name: addr2line
44
package_version: 0.24.2
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
[package]
5+
name = "datadog-opentelemetry-mappings"
6+
edition.workspace = true
7+
version.workspace = true
8+
license.workspace = true
9+
repository.workspace = true
10+
readme.workspace = true
11+
description.workspace = true
12+
13+
[dependencies]
14+
opentelemetry = { workspace = true }
15+
opentelemetry_sdk = { workspace = true }
16+
opentelemetry-semantic-conventions = { workspace = true }
17+
18+
# Libdatadog dependencies
19+
datadog-trace-utils = { workspace = true }
20+
tinybytes = { workspace = true }
21+
22+
dd-trace = { path = "../dd-trace" }
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
mod sdk_span;
5+
mod transform;
6+
7+
pub use sdk_span::SdkSpan;
8+
pub use transform::otel_util::{
9+
get_dd_key_for_otlp_attribute, get_otel_env, get_otel_operation_name_v2, get_otel_resource_v2,
10+
get_otel_service, get_otel_status_code,
11+
};
12+
pub use transform::{
13+
attribute_keys::{AttributeIndices, AttributeKey},
14+
otel_span_to_dd_span,
15+
otel_util::{OtelSpan, DEFAULT_OTLP_SERVICE_NAME},
16+
};
17+
// Exposed for testing in the sampler
18+
pub use transform::semconv;
19+
pub use transform::semconv_shim;
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
use std::{borrow::Cow, time::SystemTime};
5+
6+
/// A span exported from the OpenTelemetry SDK
7+
///
8+
/// We don't use the `opentelemetry_sdk::trace::SpanData` because it's not
9+
/// constructible so we can't create it and use to write tests on the span conversion
10+
pub struct SdkSpan {
11+
pub span_context: opentelemetry::trace::SpanContext,
12+
pub parent_span_id: opentelemetry::trace::SpanId,
13+
pub span_kind: opentelemetry::trace::SpanKind,
14+
pub name: Cow<'static, str>,
15+
pub start_time: SystemTime,
16+
pub end_time: SystemTime,
17+
pub attributes: Vec<opentelemetry::KeyValue>,
18+
#[allow(dead_code)]
19+
pub dropped_attributes_count: u32,
20+
pub events: Vec<opentelemetry::trace::Event>,
21+
#[allow(dead_code)]
22+
pub dropped_event_count: u32,
23+
pub links: Vec<opentelemetry::trace::Link>,
24+
#[allow(dead_code)]
25+
pub dropped_links_count: u32,
26+
pub status: opentelemetry::trace::Status,
27+
pub instrumentation_scope: opentelemetry::InstrumentationScope,
28+
}
29+
30+
impl SdkSpan {
31+
pub fn from_sdk_span_data(span: opentelemetry_sdk::trace::SpanData) -> Self {
32+
Self {
33+
span_context: span.span_context,
34+
parent_span_id: span.parent_span_id,
35+
span_kind: span.span_kind,
36+
name: span.name,
37+
start_time: span.start_time,
38+
end_time: span.end_time,
39+
attributes: span.attributes,
40+
dropped_attributes_count: span.dropped_attributes_count,
41+
events: span.events.events,
42+
dropped_event_count: span.events.dropped_count,
43+
links: span.links.links,
44+
dropped_links_count: span.links.dropped_count,
45+
status: span.status,
46+
instrumentation_scope: span.instrumentation_scope,
47+
}
48+
}
49+
}

datadog-opentelemetry/src/transform/attribute_keys.rs renamed to datadog-opentelemetry-mappings/src/transform/attribute_keys.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ attribute_key! {
8686
"span.type" => SPAN_TYPE,
8787
"http.response.status_code" => HTTP_RESPONSE_STATUS_CODE,
8888
opentelemetry_semantic_conventions::attribute::HTTP_STATUS_CODE => HTTP_STATUS_CODE,
89+
semconv::attribute::SERVICE_NAME => SERVICE_NAME,
8990
semconv::attribute::SERVICE_VERSION => SERVICE_VERSION,
9091
"deployment.environment.name" => DEPLOYMENT_ENVIRONMENT_NAME,
9192
semconv::attribute::DEPLOYMENT_ENVIRONMENT => DEPLOYMENT_ENVIRONMENT,

datadog-opentelemetry/src/transform/mod.rs renamed to datadog-opentelemetry-mappings/src/transform/mod.rs

Lines changed: 69 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,13 @@
3232
//! The code in attribute_keys.rs loops only once and then stores the offsets at which the
3333
//! attributes are stored, for the set of keys we are interested in.
3434
35-
mod attribute_keys;
36-
mod otel_util;
37-
mod semconv_shim;
35+
pub mod attribute_keys;
36+
pub mod otel_util;
37+
pub mod semconv_shim;
3838

3939
#[cfg(test)]
4040
mod transform_tests;
4141

42-
pub use otel_util::DEFAULT_OTLP_SERVICE_NAME;
43-
4442
use attribute_keys::*;
4543
use otel_util::*;
4644

@@ -53,49 +51,13 @@ use datadog_trace_utils::span::{
5351
};
5452
use opentelemetry::{
5553
trace::{Link, SpanKind},
56-
Key, KeyValue, SpanId,
54+
Key, KeyValue, SpanId, Value,
5755
};
5856
use opentelemetry_sdk::Resource;
59-
use opentelemetry_semantic_conventions as semconv;
57+
pub use opentelemetry_semantic_conventions as semconv;
6058
use tinybytes::BytesString;
6159

62-
use crate::ddtrace_transform::ExportSpan;
63-
64-
struct SpanExtractArgs<'a> {
65-
span: &'a ExportSpan,
66-
span_attrs: AttributeIndices,
67-
}
68-
69-
impl OtelSpan for SpanExtractArgs<'_> {
70-
fn name(&self) -> Cow<'static, str> {
71-
self.span.name.clone()
72-
}
73-
74-
fn span_kind(&self) -> SpanKind {
75-
self.span.span_kind.clone()
76-
}
77-
78-
fn has_attr(&self, attr_key: AttributeKey) -> bool {
79-
self.span_attrs.get(attr_key).is_some()
80-
}
81-
82-
fn get_attr_str_opt(&self, attr_key: AttributeKey) -> Option<Cow<'static, str>> {
83-
let idx = self.span_attrs.get(attr_key)?;
84-
let kv = self.span.attributes.get(idx)?;
85-
Some(Cow::Owned(kv.value.to_string()))
86-
}
87-
88-
fn get_attr_num<T: TryFrom<i64>>(&self, attr_key: AttributeKey) -> Option<T> {
89-
let idx = self.span_attrs.get(attr_key)?;
90-
let kv = self.span.attributes.get(idx)?;
91-
let i = match kv.value {
92-
opentelemetry::Value::I64(i) => i,
93-
opentelemetry::Value::F64(i) if i == i.floor() && i < i64::MAX as f64 => i as i64,
94-
_ => return None,
95-
};
96-
T::try_from(i).ok()
97-
}
98-
}
60+
use crate::sdk_span::SdkSpan;
9961

10062
fn set_meta_otlp(k: BytesString, v: BytesString, dd_span: &mut DdSpan) {
10163
match k.as_str() {
@@ -125,10 +87,10 @@ fn set_meta_otlp_with_semconv_mappings(
12587
dd_span: &mut DdSpan,
12688
) {
12789
let mapped_key = get_dd_key_for_otlp_attribute(k);
128-
if mapped_key.is_empty() {
90+
if mapped_key.as_str().is_empty() {
12991
return;
13092
}
131-
let mapped_key = BytesString::from_cow(mapped_key);
93+
let mapped_key = BytesString::from_cow(mapped_key.into_static_cow());
13294
if is_meta_key(mapped_key.as_ref())
13395
&& !dd_span
13496
.meta
@@ -159,7 +121,7 @@ fn set_metric_otlp(s: &mut DdSpan, k: BytesString, v: f64) {
159121

160122
fn set_metric_otlp_with_semconv_mappings(k: &str, value: f64, dd_span: &mut DdSpan) {
161123
let mapped_key = get_dd_key_for_otlp_attribute(k);
162-
let mapped_key = BytesString::from_cow(mapped_key);
124+
let mapped_key = BytesString::from_cow(mapped_key.into_static_cow());
163125

164126
if !mapped_key.is_empty() {
165127
if is_meta_key(mapped_key.as_str()) && dd_span.metrics.contains_key(&mapped_key) {
@@ -170,11 +132,7 @@ fn set_metric_otlp_with_semconv_mappings(k: &str, value: f64, dd_span: &mut DdSp
170132
}
171133

172134
/// https://github.com/DataDog/datadog-agent/blob/main/pkg/trace/transform/transform.go#L69
173-
fn otel_span_to_dd_span_minimal(
174-
span: &SpanExtractArgs,
175-
res: &Resource,
176-
is_top_level: bool,
177-
) -> DdSpan {
135+
fn otel_span_to_dd_span_minimal(span: &SpanExtractArgs, is_top_level: bool) -> DdSpan {
178136
let (trace_id_lower_half, _) = otel_trace_id_to_dd_id(span.span.span_context.trace_id());
179137
let span_id = otel_span_id_to_dd_id(span.span.span_context.span_id());
180138
let parent_id = otel_span_id_to_dd_id(span.span.parent_span_id);
@@ -221,18 +179,18 @@ fn otel_span_to_dd_span_minimal(
221179
}
222180

223181
if dd_span.service.is_empty() {
224-
dd_span.service = BytesString::from_cow(get_otel_service(res));
182+
dd_span.service = BytesString::from_cow(get_otel_service(span));
225183
}
226184

227185
if dd_span.name.is_empty() {
228186
dd_span.name = BytesString::from_cow(get_otel_operation_name_v2(span));
229187
}
230188

231189
if dd_span.resource.is_empty() {
232-
dd_span.resource = BytesString::from_cow(get_otel_resource_v2(span, res));
190+
dd_span.resource = BytesString::from_cow(get_otel_resource_v2(span));
233191
}
234192
if dd_span.r#type.is_empty() {
235-
dd_span.r#type = BytesString::from_cow(get_otel_span_type(span, res));
193+
dd_span.r#type = BytesString::from_cow(get_otel_span_type(span));
236194
}
237195
let code: u32 = if let Some(http_status_code) = span.get_attr_num(DATADOG_HTTP_STATUS_CODE) {
238196
http_status_code
@@ -438,6 +396,58 @@ fn is_meta_key(key: &str) -> bool {
438396
)
439397
}
440398

399+
struct SpanExtractArgs<'a> {
400+
span: &'a SdkSpan,
401+
resource: &'a Resource,
402+
span_attrs: AttributeIndices,
403+
}
404+
405+
impl<'a> SpanExtractArgs<'a> {
406+
pub fn new(span: &'a SdkSpan, resource: &'a Resource) -> Self {
407+
let span_attrs = AttributeIndices::from_attribute_slice(&span.attributes);
408+
Self {
409+
span,
410+
span_attrs,
411+
resource,
412+
}
413+
}
414+
}
415+
416+
impl OtelSpan for SpanExtractArgs<'_> {
417+
fn name(&self) -> Cow<'static, str> {
418+
self.span.name.clone()
419+
}
420+
421+
fn span_kind(&self) -> SpanKind {
422+
self.span.span_kind.clone()
423+
}
424+
425+
fn has_attr(&self, attr_key: AttributeKey) -> bool {
426+
self.span_attrs.get(attr_key).is_some()
427+
}
428+
429+
fn get_attr_str_opt(&self, attr_key: AttributeKey) -> Option<Cow<'static, str>> {
430+
let idx = self.span_attrs.get(attr_key)?;
431+
let kv = self.span.attributes.get(idx)?;
432+
Some(Cow::Owned(kv.value.to_string()))
433+
}
434+
435+
fn get_attr_num<T: TryFrom<i64>>(&self, attr_key: AttributeKey) -> Option<T> {
436+
let idx = self.span_attrs.get(attr_key)?;
437+
let kv = self.span.attributes.get(idx)?;
438+
let i = match kv.value {
439+
opentelemetry::Value::I64(i) => i,
440+
opentelemetry::Value::F64(i) if i == i.floor() && i < i64::MAX as f64 => i as i64,
441+
_ => return None,
442+
};
443+
T::try_from(i).ok()
444+
}
445+
446+
fn get_res_attribute_opt(&self, attr_key: AttributeKey) -> Option<Value> {
447+
self.resource.get(&Key::from_static_str(attr_key.key()))
448+
}
449+
}
450+
441451
/// Converts an OpenTelemetry span to a Datadog span.
442452
/// https://github.com/DataDog/datadog-agent/blob/d91c1b47da4f5f24559f49be284e547cc847d5e2/pkg/trace/transform/transform.go#L236
443453
///
@@ -448,7 +458,7 @@ fn is_meta_key(key: &str) -> bool {
448458
/// * `enable_otlp_compute_top_level_by_span_kind` => default to true
449459
/// * `IgnoreMissingDatadogFields` => default to false
450460
/// * `disable_operation_and_resource_name_logic_v2` => default to false
451-
pub fn otel_span_to_dd_span(otel_span: ExportSpan, otel_resource: &Resource) -> DdSpan {
461+
pub fn otel_span_to_dd_span(otel_span: SdkSpan, otel_resource: &Resource) -> DdSpan {
452462
// There is a performance otpimization possible here:
453463
// The otlp receiver splits span conversion into two steps
454464
// 1. The minimal fields used by Stats computation
@@ -457,15 +467,11 @@ pub fn otel_span_to_dd_span(otel_span: ExportSpan, otel_resource: &Resource) ->
457467
// If we use CSS we could probably do only 1. if we know the span is going to be dropped before
458468
// being sent...
459469

460-
let span_attrs = AttributeIndices::from_attribute_slice(&otel_span.attributes);
461-
let span_extracted = SpanExtractArgs {
462-
span: &otel_span,
463-
span_attrs,
464-
};
470+
let span_extracted = SpanExtractArgs::new(&otel_span, otel_resource);
465471
let is_top_level = otel_span.parent_span_id == SpanId::INVALID
466472
|| matches!(otel_span.span_kind, SpanKind::Server | SpanKind::Consumer);
467473

468-
let mut dd_span = otel_span_to_dd_span_minimal(&span_extracted, otel_resource, is_top_level);
474+
let mut dd_span = otel_span_to_dd_span_minimal(&span_extracted, is_top_level);
469475

470476
for (dd_semantics_key, meta_key) in DD_SEMANTICS_KEY_TO_META_KEY {
471477
let value = span_extracted.get_attr_str(*dd_semantics_key);
@@ -526,7 +532,7 @@ pub fn otel_span_to_dd_span(otel_span: ExportSpan, otel_resource: &Resource) ->
526532
}
527533

528534
if let hash_map::Entry::Vacant(env_slot) = dd_span.meta.entry(BytesString::from_static("env")) {
529-
let env = get_otel_env(otel_resource);
535+
let env = get_otel_env(&span_extracted);
530536
if !env.is_empty() {
531537
env_slot.insert(BytesString::from_cow(env));
532538
}

0 commit comments

Comments
 (0)