use super::OtlpResourceInfo;
use crate::span::v04::{Span, SpanEvent, SpanLink};
use crate::span::{TraceData, SPAN_LINK_FLAGS_SET_SENTINEL};
use std::borrow::Borrow;
use libdd_trace_protobuf::opentelemetry::proto::collector::trace::v1::ExportTraceServiceRequest as ProtoReq;
use libdd_trace_protobuf::opentelemetry::proto::common::v1::{
any_value::Value as ProtoValue, AnyValue as ProtoAnyValue, ArrayValue as ProtoArrayValue,
InstrumentationScope as ProtoScope, KeyValue as ProtoKeyValue,
};
use libdd_trace_protobuf::opentelemetry::proto::resource::v1::Resource as ProtoResource;
use libdd_trace_protobuf::opentelemetry::proto::trace::v1::{
span::{Event as ProtoEvent, Link as ProtoLink},
ResourceSpans as ProtoResourceSpans, ScopeSpans as ProtoScopeSpans, Span as ProtoSpan,
Status as ProtoStatus,
};
pub(crate) const MAX_ATTRIBUTES_PER_SPAN: usize = 128;
mod span_kind {
pub const UNSPECIFIED: i32 = 0;
pub const INTERNAL: i32 = 1;
pub const SERVER: i32 = 2;
pub const CLIENT: i32 = 3;
pub const PRODUCER: i32 = 4;
pub const CONSUMER: i32 = 5;
}
pub mod status_code {
pub const UNSET: i32 = 0;
pub const ERROR: i32 = 2;
}
fn span_status<T: TraceData>(span: &Span<T>) -> (i32, Option<String>) {
if span.error != 0 {
let message = span
.meta
.get("error.msg")
.or_else(|| span.meta.get("error.message"))
.map(|v| v.borrow().to_string());
(status_code::ERROR, message)
} else {
(status_code::UNSET, None)
}
}
fn span_kind<T: TraceData>(span: &Span<T>) -> i32 {
span.meta
.get("span.kind")
.map(|v| tag_to_otlp_kind(v.borrow()))
.unwrap_or_else(|| dd_type_to_otlp_kind(span.r#type.borrow()))
}
fn chunk_trace_id_high<T: TraceData>(chunk: &[Span<T>]) -> u64 {
chunk
.iter()
.find_map(|s| {
let high = (s.trace_id >> 64) as u64;
if high != 0 {
return Some(high);
}
s.meta
.get("_dd.p.tid")
.and_then(|v| u64::from_str_radix(v.borrow(), 16).ok())
})
.unwrap_or(0)
}
fn span_sampling_flags<T: TraceData>(span: &Span<T>) -> u32 {
span.metrics
.get("_sampling_priority_v1")
.is_some_and(|priority| *priority >= 1.0)
.into()
}
fn chunk_sampling_flags<T: TraceData>(chunk: &[Span<T>]) -> u32 {
chunk
.iter()
.find_map(|span| span.metrics.get("_sampling_priority_v1"))
.is_some_and(|priority| *priority > 0.0)
.into()
}
fn tag_to_otlp_kind(t: &str) -> i32 {
if t.eq_ignore_ascii_case("server") {
span_kind::SERVER
} else if t.eq_ignore_ascii_case("client") {
span_kind::CLIENT
} else if t.eq_ignore_ascii_case("producer") {
span_kind::PRODUCER
} else if t.eq_ignore_ascii_case("consumer") {
span_kind::CONSUMER
} else if t.eq_ignore_ascii_case("internal") {
span_kind::INTERNAL
} else {
span_kind::UNSPECIFIED
}
}
fn dd_type_to_otlp_kind(t: &str) -> i32 {
if t.eq_ignore_ascii_case("server")
|| t.eq_ignore_ascii_case("web")
|| t.eq_ignore_ascii_case("http")
{
span_kind::SERVER
} else if t.eq_ignore_ascii_case("client") {
span_kind::CLIENT
} else if t.eq_ignore_ascii_case("producer") {
span_kind::PRODUCER
} else if t.eq_ignore_ascii_case("consumer") {
span_kind::CONSUMER
} else {
span_kind::INTERNAL
}
}
fn proto_kv(key: String, value: ProtoValue) -> ProtoKeyValue {
ProtoKeyValue {
key,
value: Some(ProtoAnyValue { value: Some(value) }),
key_ref: 0,
}
}
fn collect_span_attributes<T: TraceData>(
span: &Span<T>,
resource_service: &str,
otel_trace_semantics_enabled: bool,
) -> (Vec<ProtoKeyValue>, usize) {
let capacity = (4 + span.meta.len() + span.metrics.len() + span.meta_struct.len())
.min(MAX_ATTRIBUTES_PER_SPAN);
let mut attrs: Vec<ProtoKeyValue> = Vec::with_capacity(capacity);
let span_service = span.service.borrow();
let has_per_span_service = !span_service.is_empty() && span_service != resource_service;
if has_per_span_service && !otel_trace_semantics_enabled {
attrs.push(proto_kv(
"service.name".to_string(),
ProtoValue::StringValue(span_service.to_string()),
));
}
let operation_name = span.name.borrow();
let has_operation_name = !operation_name.is_empty();
if has_operation_name && !otel_trace_semantics_enabled {
attrs.push(proto_kv(
"operation.name".to_string(),
ProtoValue::StringValue(operation_name.to_string()),
));
}
let span_type = span.r#type.borrow();
let has_span_type = !span_type.is_empty();
if has_span_type && !otel_trace_semantics_enabled {
attrs.push(proto_kv(
"span.type".to_string(),
ProtoValue::StringValue(span_type.to_string()),
));
}
let resource_name = span.resource.borrow();
let has_resource_name = !resource_name.is_empty();
if has_resource_name && !otel_trace_semantics_enabled {
attrs.push(proto_kv(
"resource.name".to_string(),
ProtoValue::StringValue(resource_name.to_string()),
));
}
for (k, v) in span.meta.iter() {
if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
break;
}
let key = k.borrow();
if otel_trace_semantics_enabled
&& (key == "error.msg" || key == "error.message" || key == "span.kind")
{
continue;
}
attrs.push(proto_kv(
key.to_string(),
ProtoValue::StringValue(v.borrow().to_string()),
));
}
for (k, v) in span.metrics.iter() {
if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
break;
}
let value = if v.fract() == 0.0 && (*v >= i64::MIN as f64 && *v <= i64::MAX as f64) {
ProtoValue::IntValue(*v as i64)
} else {
ProtoValue::DoubleValue(*v)
};
attrs.push(proto_kv(k.borrow().to_string(), value));
}
for (k, v) in span.meta_struct.iter() {
if attrs.len() >= MAX_ATTRIBUTES_PER_SPAN {
break;
}
attrs.push(proto_kv(
k.borrow().to_string(),
ProtoValue::BytesValue(v.borrow().to_vec()),
));
}
let excluded_compat_tags = if otel_trace_semantics_enabled {
span.meta.contains_key("error.msg") as usize
+ span.meta.contains_key("error.message") as usize
+ span.meta.contains_key("span.kind") as usize
} else {
0
};
let promoted = if otel_trace_semantics_enabled {
0
} else {
(has_per_span_service as usize)
+ (has_operation_name as usize)
+ (has_span_type as usize)
+ (has_resource_name as usize)
};
let total = promoted
+ (span.meta.len() - excluded_compat_tags)
+ span.metrics.len()
+ span.meta_struct.len();
let dropped = total.saturating_sub(attrs.len());
(attrs, dropped)
}
fn event_attr_value<T: TraceData>(av: &crate::span::v04::AttributeArrayValue<T>) -> ProtoValue {
use crate::span::v04::AttributeArrayValue;
match av {
AttributeArrayValue::String(s) => ProtoValue::StringValue(s.borrow().to_string()),
AttributeArrayValue::Boolean(b) => ProtoValue::BoolValue(*b),
AttributeArrayValue::Integer(i) => ProtoValue::IntValue(*i),
AttributeArrayValue::Double(d) => ProtoValue::DoubleValue(*d),
}
}
fn collect_event_attributes<T: TraceData>(ev: &SpanEvent<T>) -> Vec<ProtoKeyValue> {
use crate::span::v04::AttributeAnyValue;
ev.attributes
.iter()
.map(|(k, v)| {
let value = match v {
AttributeAnyValue::SingleValue(av) => event_attr_value(av),
AttributeAnyValue::Array(items) => ProtoValue::ArrayValue(ProtoArrayValue {
values: items
.iter()
.map(|it| ProtoAnyValue {
value: Some(event_attr_value(it)),
})
.collect(),
}),
};
proto_kv(k.borrow().to_string(), value)
})
.collect()
}
pub fn map_traces_to_otlp<T: TraceData>(
trace_chunks: Vec<Vec<Span<T>>>,
resource_info: &OtlpResourceInfo,
otel_trace_semantics_enabled: bool,
) -> ProtoReq {
let resource = build_resource(resource_info);
let total_spans: usize = trace_chunks.iter().map(|chunk| chunk.len()).sum();
let mut all_spans: Vec<ProtoSpan> = Vec::with_capacity(total_spans);
for chunk in &trace_chunks {
let high = chunk_trace_id_high(chunk);
let chunk_flags = otel_trace_semantics_enabled.then(|| chunk_sampling_flags(chunk));
for span in chunk {
let flags = chunk_flags.unwrap_or_else(|| span_sampling_flags(span));
all_spans.push(map_span(
span,
&resource_info.service,
high,
flags,
otel_trace_semantics_enabled,
));
}
}
ProtoReq {
resource_spans: vec![ProtoResourceSpans {
resource: Some(resource),
scope_spans: vec![ProtoScopeSpans {
scope: Some(ProtoScope {
name: resource_info.instrumentation_scope_name.clone(),
version: resource_info.instrumentation_scope_version.clone(),
attributes: Vec::new(),
dropped_attributes_count: 0,
}),
spans: all_spans,
schema_url: String::new(),
}],
schema_url: String::new(),
}],
}
}
fn build_resource(resource_info: &OtlpResourceInfo) -> ProtoResource {
fn push_str_attr(attrs: &mut Vec<ProtoKeyValue>, k: &str, v: &str) {
if !v.is_empty() {
attrs.push(proto_kv(
k.to_string(),
ProtoValue::StringValue(v.to_string()),
));
}
}
let mut attributes = Vec::new();
push_str_attr(&mut attributes, "service.name", &resource_info.service);
push_str_attr(
&mut attributes,
"deployment.environment.name",
&resource_info.env,
);
push_str_attr(
&mut attributes,
"service.version",
&resource_info.app_version,
);
attributes.push(proto_kv(
"telemetry.sdk.name".to_string(),
ProtoValue::StringValue("datadog".to_string()),
));
push_str_attr(
&mut attributes,
"telemetry.sdk.language",
&resource_info.language,
);
push_str_attr(
&mut attributes,
"telemetry.sdk.version",
&resource_info.tracer_version,
);
push_str_attr(&mut attributes, "runtime-id", &resource_info.runtime_id);
if resource_info.client_computed_stats {
push_str_attr(&mut attributes, "_dd.stats_computed", "true");
}
ProtoResource {
attributes,
dropped_attributes_count: 0,
entity_refs: Vec::new(),
}
}
fn map_span<T: TraceData>(
span: &Span<T>,
resource_service: &str,
chunk_trace_id_high: u64,
flags: u32,
otel_trace_semantics_enabled: bool,
) -> ProtoSpan {
let trace_id_128 = ((chunk_trace_id_high as u128) << 64) | (span.trace_id as u64 as u128);
let parent_span_id = if span.parent_id != 0 {
span.parent_id.to_be_bytes().to_vec()
} else {
Vec::new()
};
let (attributes, dropped_attributes_count) =
collect_span_attributes(span, resource_service, otel_trace_semantics_enabled);
let (code, message) = span_status(span);
let trace_state = span
.meta
.get("tracestate")
.map(|v| v.borrow().to_string())
.filter(|s| !s.is_empty())
.unwrap_or_default();
let links = span.span_links.iter().map(map_span_link).collect();
let (events, dropped_events_count) = map_span_events(&span.span_events);
ProtoSpan {
trace_id: trace_id_128.to_be_bytes().to_vec(),
span_id: span.span_id.to_be_bytes().to_vec(),
trace_state,
parent_span_id,
flags,
name: span.resource.borrow().to_string(),
kind: span_kind(span),
start_time_unix_nano: span.start.max(0) as u64,
end_time_unix_nano: (span.start + span.duration).max(0) as u64,
attributes,
dropped_attributes_count: dropped_attributes_count as u32,
events,
dropped_events_count: dropped_events_count as u32,
links,
dropped_links_count: 0,
status: Some(ProtoStatus {
message: message.unwrap_or_default(),
code,
}),
}
}
fn map_span_link<T: TraceData>(link: &SpanLink<T>) -> ProtoLink {
let trace_id_128 = ((link.trace_id_high as u128) << 64) | (link.trace_id as u128);
ProtoLink {
trace_id: trace_id_128.to_be_bytes().to_vec(),
span_id: link.span_id.to_be_bytes().to_vec(),
trace_state: {
let ts = link.tracestate.borrow();
if ts.is_empty() {
String::new()
} else {
ts.to_string()
}
},
attributes: link
.attributes
.iter()
.map(|(k, v)| {
proto_kv(
k.borrow().to_string(),
ProtoValue::StringValue(v.borrow().to_string()),
)
})
.collect(),
dropped_attributes_count: 0,
flags: link.flags & !SPAN_LINK_FLAGS_SET_SENTINEL,
}
}
fn map_span_events<T: TraceData>(events: &[SpanEvent<T>]) -> (Vec<ProtoEvent>, usize) {
const MAX_EVENTS_PER_SPAN: usize = 128;
let mut out = Vec::with_capacity(events.len().min(MAX_EVENTS_PER_SPAN));
for ev in events.iter().take(MAX_EVENTS_PER_SPAN) {
out.push(ProtoEvent {
time_unix_nano: ev.time_unix_nano,
name: ev.name.borrow().to_string(),
attributes: collect_event_attributes(ev),
dropped_attributes_count: 0,
});
}
let dropped = events.len().saturating_sub(out.len());
(out, dropped)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::otlp_encoder::OtlpResourceInfo;
use crate::span::BytesData;
#[test]
fn maps_native_span_to_prost_ir() {
use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
let resource_info = OtlpResourceInfo::default();
let mut span: Span<BytesData> = Span {
trace_id: 0xD269B633813FC60C_u128,
span_id: 0xEEE19B7EC3C1B174,
parent_id: 0xEEE19B7EC3C1B173,
name: libdd_tinybytes::BytesString::from_static("op"),
resource: libdd_tinybytes::BytesString::from_static("res"),
r#type: libdd_tinybytes::BytesString::from_static("web"),
start: 1544712660000000000,
duration: 1000000000,
error: 1,
..Default::default()
};
span.meta.insert(
"error.msg".into(),
libdd_tinybytes::BytesString::from_static("boom"),
);
span.metrics
.insert(libdd_tinybytes::BytesString::from_static("count"), 42.0);
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
assert_eq!(s.trace_id, 0xD269B633813FC60C_u128.to_be_bytes().to_vec());
assert_eq!(s.span_id, 0xEEE19B7EC3C1B174u64.to_be_bytes().to_vec());
assert_eq!(
s.parent_span_id,
0xEEE19B7EC3C1B173u64.to_be_bytes().to_vec()
);
assert_eq!(s.name, "res");
assert_eq!(s.kind, 2); assert_eq!(s.start_time_unix_nano, 1544712660000000000);
assert_eq!(s.end_time_unix_nano, 1544712661000000000);
let st = s.status.as_ref().unwrap();
assert_eq!(st.code, 2);
assert_eq!(st.message, "boom");
let count = s.attributes.iter().find(|a| a.key == "count").unwrap();
assert!(matches!(
count.value.as_ref().unwrap().value,
Some(PV::IntValue(42))
));
}
#[test]
fn instrumentation_scope_from_resource_info() {
let resource_info = OtlpResourceInfo {
instrumentation_scope_name: "dd-trace-js".to_string(),
instrumentation_scope_version: "7.0.0-pre".to_string(),
..Default::default()
};
let span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("s"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let scope = req.resource_spans[0].scope_spans[0].scope.as_ref().unwrap();
assert_eq!(scope.name, "dd-trace-js");
assert_eq!(scope.version, "7.0.0-pre");
}
#[test]
fn proto_span_uses_raw_id_bytes_and_native_timestamps() {
let resource_info = OtlpResourceInfo {
service: "svc".to_string(),
..Default::default()
};
let span: Span<BytesData> = Span {
trace_id: 0x5b8efff798038103_d269b633813fc60c_u128,
span_id: 0xEEE19B7EC3C1B174,
parent_id: 0xEEE19B7EC3C1B173,
name: libdd_tinybytes::BytesString::from_static("op"),
resource: libdd_tinybytes::BytesString::from_static("res"),
r#type: libdd_tinybytes::BytesString::from_static("web"),
start: 1544712660000000000,
duration: 1000000000,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
assert_eq!(
s.trace_id,
0x5b8efff798038103_d269b633813fc60c_u128
.to_be_bytes()
.to_vec()
);
assert_eq!(s.span_id, 0xEEE19B7EC3C1B174u64.to_be_bytes().to_vec());
assert_eq!(
s.parent_span_id,
0xEEE19B7EC3C1B173u64.to_be_bytes().to_vec()
);
assert_eq!(s.start_time_unix_nano, 1544712660000000000);
assert_eq!(s.end_time_unix_nano, 1544712661000000000);
assert_eq!(s.name, "res");
assert_eq!(s.kind, span_kind::SERVER);
}
#[test]
fn negative_start_clamps_to_zero() {
let resource_info = OtlpResourceInfo {
service: "svc".to_string(),
..Default::default()
};
let span: Span<BytesData> = Span {
trace_id: 1,
span_id: 1,
start: -1,
duration: 0,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
assert_eq!(
s.start_time_unix_nano, 0,
"negative start must clamp to 0, not wrap"
);
assert_eq!(
s.end_time_unix_nano, 0,
"negative start+duration must clamp to 0, not wrap"
);
}
#[test]
fn status_error_message_from_meta() {
let resource_info = OtlpResourceInfo::default();
let mut span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("err_span"),
start: 0,
duration: 1,
error: 1,
..Default::default()
};
span.meta.insert(
libdd_tinybytes::BytesString::from_static("error.msg"),
libdd_tinybytes::BytesString::from_static("something broke"),
);
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
let status = s.status.as_ref().unwrap();
assert_eq!(status.code, status_code::ERROR);
assert_eq!(status.message, "something broke");
}
#[test]
fn metrics_as_int_or_double() {
use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
let resource_info = OtlpResourceInfo::default();
let mut span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("m"),
start: 0,
duration: 1,
..Default::default()
};
span.metrics
.insert(libdd_tinybytes::BytesString::from_static("count"), 42.0);
span.metrics.insert(
libdd_tinybytes::BytesString::from_static("rate"),
std::f64::consts::PI,
);
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
let count = s.attributes.iter().find(|a| a.key == "count").unwrap();
assert!(matches!(
count.value.as_ref().unwrap().value,
Some(PV::IntValue(42))
));
let rate = s.attributes.iter().find(|a| a.key == "rate").unwrap();
match rate.value.as_ref().unwrap().value {
Some(PV::DoubleValue(d)) => assert!((d - std::f64::consts::PI).abs() < 1e-9),
ref other => panic!("expected double, got {other:?}"),
}
}
#[test]
fn trace_id_128_from_dd_p_tid() {
let resource_info = OtlpResourceInfo::default();
let mut span: Span<BytesData> = Span {
trace_id: 0xD269B633813FC60C_u128, span_id: 1,
name: libdd_tinybytes::BytesString::from_static("s"),
start: 0,
duration: 1,
..Default::default()
};
span.meta.insert(
"_dd.p.tid".into(),
libdd_tinybytes::BytesString::from_static("5b8efff798038103"),
);
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
assert_eq!(
s.trace_id,
0x5b8efff798038103_d269b633813fc60c_u128
.to_be_bytes()
.to_vec()
);
}
#[test]
fn trace_id_128_from_native_span_field() {
let resource_info = OtlpResourceInfo::default();
let full: u128 = 0x5b8efff798038103_d269b633813fc60c_u128;
let root: Span<BytesData> = Span {
trace_id: full,
span_id: 1,
name: libdd_tinybytes::BytesString::from_static("root"),
start: 0,
duration: 1,
..Default::default()
};
let child: Span<BytesData> = Span {
trace_id: 0xD269B633813FC60C_u128,
span_id: 2,
parent_id: 1,
name: libdd_tinybytes::BytesString::from_static("child"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![root, child]], &resource_info, false);
let spans = &req.resource_spans[0].scope_spans[0].spans;
let expected = full.to_be_bytes().to_vec();
assert_eq!(spans[0].trace_id, expected);
assert_eq!(spans[1].trace_id, expected);
}
#[test]
fn trace_id_128_without_dd_p_tid_defaults_high_to_zero() {
let resource_info = OtlpResourceInfo::default();
let span: Span<BytesData> = Span {
trace_id: 0xD269B633813FC60C_u128,
span_id: 1,
name: libdd_tinybytes::BytesString::from_static("s"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
assert_eq!(s.trace_id, 0xD269B633813FC60C_u128.to_be_bytes().to_vec());
}
#[test]
fn trace_id_128_propagated_to_chunk_children() {
let resource_info = OtlpResourceInfo::default();
let low: u128 = 0xD269B633813FC60C_u128;
let mut root: Span<BytesData> = Span {
trace_id: low,
span_id: 1,
name: libdd_tinybytes::BytesString::from_static("root"),
start: 0,
duration: 1,
..Default::default()
};
root.meta.insert(
"_dd.p.tid".into(),
libdd_tinybytes::BytesString::from_static("5b8efff798038103"),
);
let child_a: Span<BytesData> = Span {
trace_id: low,
span_id: 2,
parent_id: 1,
name: libdd_tinybytes::BytesString::from_static("child_a"),
start: 0,
duration: 1,
..Default::default()
};
let child_b: Span<BytesData> = Span {
trace_id: low,
span_id: 3,
parent_id: 1,
name: libdd_tinybytes::BytesString::from_static("child_b"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![root, child_a, child_b]], &resource_info, false);
let spans = &req.resource_spans[0].scope_spans[0].spans;
assert_eq!(spans.len(), 3);
let expected = 0x5b8efff798038103_d269b633813fc60c_u128
.to_be_bytes()
.to_vec();
for s in spans {
assert_eq!(s.trace_id, expected);
}
}
#[test]
fn trace_id_128_isolation_across_chunks() {
let resource_info = OtlpResourceInfo::default();
let low_a: u128 = 0x1111111111111111_u128;
let low_b: u128 = 0x2222222222222222_u128;
let mut root_a: Span<BytesData> = Span {
trace_id: low_a,
span_id: 1,
name: libdd_tinybytes::BytesString::from_static("root_a"),
start: 0,
duration: 1,
..Default::default()
};
root_a.meta.insert(
"_dd.p.tid".into(),
libdd_tinybytes::BytesString::from_static("aaaaaaaaaaaaaaaa"),
);
let child_a: Span<BytesData> = Span {
trace_id: low_a,
span_id: 2,
parent_id: 1,
name: libdd_tinybytes::BytesString::from_static("child_a"),
start: 0,
duration: 1,
..Default::default()
};
let mut root_b: Span<BytesData> = Span {
trace_id: low_b,
span_id: 3,
name: libdd_tinybytes::BytesString::from_static("root_b"),
start: 0,
duration: 1,
..Default::default()
};
root_b.meta.insert(
"_dd.p.tid".into(),
libdd_tinybytes::BytesString::from_static("bbbbbbbbbbbbbbbb"),
);
let child_b: Span<BytesData> = Span {
trace_id: low_b,
span_id: 4,
parent_id: 3,
name: libdd_tinybytes::BytesString::from_static("child_b"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(
vec![vec![root_a, child_a], vec![root_b, child_b]],
&resource_info,
false,
);
let spans = &req.resource_spans[0].scope_spans[0].spans;
assert_eq!(spans.len(), 4);
let expect_a = 0xaaaaaaaaaaaaaaaa_1111111111111111_u128
.to_be_bytes()
.to_vec();
let expect_b = 0xbbbbbbbbbbbbbbbb_2222222222222222_u128
.to_be_bytes()
.to_vec();
assert_eq!(spans[0].trace_id, expect_a);
assert_eq!(spans[1].trace_id, expect_a);
assert_eq!(spans[2].trace_id, expect_b);
assert_eq!(spans[3].trace_id, expect_b);
}
#[test]
fn chunk_with_malformed_dd_p_tid_on_root_falls_back() {
let resource_info = OtlpResourceInfo::default();
let low: u128 = 0xD269B633813FC60C_u128;
let mut root: Span<BytesData> = Span {
trace_id: low,
span_id: 1,
name: libdd_tinybytes::BytesString::from_static("root"),
start: 0,
duration: 1,
..Default::default()
};
root.meta.insert(
"_dd.p.tid".into(),
libdd_tinybytes::BytesString::from_static("not-hex"),
);
let child_no_tag: Span<BytesData> = Span {
trace_id: low,
span_id: 2,
parent_id: 1,
name: libdd_tinybytes::BytesString::from_static("child_no_tag"),
start: 0,
duration: 1,
..Default::default()
};
let mut child_valid: Span<BytesData> = Span {
trace_id: low,
span_id: 3,
parent_id: 1,
name: libdd_tinybytes::BytesString::from_static("child_valid"),
start: 0,
duration: 1,
..Default::default()
};
child_valid.meta.insert(
"_dd.p.tid".into(),
libdd_tinybytes::BytesString::from_static("dddddddddddddddd"),
);
let req = map_traces_to_otlp(
vec![vec![root, child_no_tag, child_valid]],
&resource_info,
false,
);
let spans = &req.resource_spans[0].scope_spans[0].spans;
let expected = 0xdddddddddddddddd_d269b633813fc60c_u128
.to_be_bytes()
.to_vec();
assert_eq!(spans[0].trace_id, expected);
assert_eq!(spans[1].trace_id, expected);
assert_eq!(spans[2].trace_id, expected);
}
#[test]
fn test_stats_computed_resource_attr_set_when_enabled() {
let resource_info = OtlpResourceInfo {
client_computed_stats: true,
..Default::default()
};
let span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("s"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let resource_attrs = &req.resource_spans[0].resource.as_ref().unwrap().attributes;
let kv = resource_attrs
.iter()
.find(|a| a.key == "_dd.stats_computed")
.expect("_dd.stats_computed must be present when client_computed_stats=true");
let val = match kv.value.as_ref().and_then(|v| v.value.as_ref()) {
Some(ProtoValue::StringValue(s)) => s.as_str(),
other => panic!("expected stringValue, got {other:?}"),
};
assert_eq!(val, "true");
}
#[test]
fn test_stats_computed_resource_attr_absent_when_disabled() {
let resource_info = OtlpResourceInfo {
client_computed_stats: false,
..Default::default()
};
let span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("s"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let resource_attrs = &req.resource_spans[0].resource.as_ref().unwrap().attributes;
assert!(
!resource_attrs.iter().any(|a| a.key == "_dd.stats_computed"),
"_dd.stats_computed must not be emitted when client_computed_stats=false"
);
}
#[test]
fn span_link_flags_are_carried() {
let mut span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("s"),
start: 0,
duration: 1,
..Default::default()
};
span.span_links.push(SpanLink {
trace_id: 0x11,
span_id: 0x22,
flags: 1,
..Default::default()
});
let req = map_traces_to_otlp(vec![vec![span]], &OtlpResourceInfo::default(), false);
let link = &req.resource_spans[0].scope_spans[0].spans[0].links[0];
assert_eq!(
link.flags, 1,
"OTLP Link.flags must carry the span link's flags"
);
}
#[test]
fn span_link_flags_sentinel_bit_masked() {
fn mapped_flags(flags: u32) -> u32 {
let mut span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("s"),
start: 0,
duration: 1,
..Default::default()
};
span.span_links.push(SpanLink {
trace_id: 0x11,
span_id: 0x22,
flags,
..Default::default()
});
let req = map_traces_to_otlp(vec![vec![span]], &OtlpResourceInfo::default(), false);
req.resource_spans[0].scope_spans[0].spans[0].links[0].flags
}
assert_eq!(
mapped_flags(0x8000_0001),
1,
"OTLP Link.flags must not carry the internal sentinel bit"
);
assert_eq!(
mapped_flags(0x8000_0000),
0,
"an explicit drop decision must still map to flags: 0"
);
}
#[test]
fn test_otel_trace_semantics_enabled() {
let resource_info = OtlpResourceInfo {
service: "resource-svc".to_string(),
..Default::default()
};
let mut span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("http.request"),
service: libdd_tinybytes::BytesString::from_static("span-svc"),
resource: libdd_tinybytes::BytesString::from_static("GET /api/users"),
r#type: libdd_tinybytes::BytesString::from_static("web"),
start: 0,
duration: 1,
..Default::default()
};
span.meta.insert(
libdd_tinybytes::BytesString::from_static("span.kind"),
libdd_tinybytes::BytesString::from_static("client"),
);
span.meta.insert(
libdd_tinybytes::BytesString::from_static("http.method"),
libdd_tinybytes::BytesString::from_static("GET"),
);
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, true);
let attrs = &req.resource_spans[0].scope_spans[0].spans[0].attributes;
let keys: Vec<&str> = attrs.iter().map(|kv| kv.key.as_str()).collect();
for omitted in [
"service.name",
"operation.name",
"resource.name",
"span.type",
"span.kind",
] {
assert!(
!keys.contains(&omitted),
"OTel-semantics must omit {omitted}"
);
}
assert!(
keys.contains(&"http.method"),
"OTel-standard meta tags must remain"
);
}
#[test]
fn error_message_promoted_to_status_under_otel_semantics() {
let resource_info = OtlpResourceInfo::default();
let mut span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("op"),
start: 0,
duration: 1,
error: 1,
..Default::default()
};
span.meta.insert(
libdd_tinybytes::BytesString::from_static("error.message"),
libdd_tinybytes::BytesString::from_static("boom"),
);
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, true);
let otlp_span = &req.resource_spans[0].scope_spans[0].spans[0];
let status = otlp_span
.status
.as_ref()
.expect("status present on error span");
assert_eq!(status.code, status_code::ERROR);
assert_eq!(
status.message, "boom",
"error.message must be promoted to the OTLP Status message"
);
assert!(
!otlp_span
.attributes
.iter()
.any(|kv| kv.key == "error.message"),
"error.message compat attr must be omitted under OTel-semantics"
);
}
#[test]
fn empty_chunk_does_not_panic() {
let resource_info = OtlpResourceInfo::default();
let empty: Vec<Vec<Span<BytesData>>> = vec![vec![]];
let req = map_traces_to_otlp(empty, &resource_info, false);
let spans = &req.resource_spans[0].scope_spans[0].spans;
assert!(spans.is_empty());
}
#[test]
fn tracestate_from_meta() {
let resource_info = OtlpResourceInfo::default();
let mut span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("s"),
start: 0,
duration: 1,
..Default::default()
};
span.meta.insert(
"tracestate".into(),
libdd_tinybytes::BytesString::from_static("vendor1=abc,rojo=00f067"),
);
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
assert_eq!(s.trace_state, "vendor1=abc,rojo=00f067");
}
#[test]
fn meta_struct_as_bytes_value() {
use libdd_tinybytes::Bytes;
use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
let resource_info = OtlpResourceInfo::default();
let mut span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("s"),
start: 0,
duration: 1,
..Default::default()
};
span.meta_struct
.insert("my_key".into(), Bytes::from(vec![1u8, 2, 3]));
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
let kv = s
.attributes
.iter()
.find(|a| a.key == "my_key")
.expect("my_key attribute not found");
match kv.value.as_ref().unwrap().value {
Some(PV::BytesValue(ref b)) => assert_eq!(b, &vec![1u8, 2, 3]),
ref other => panic!("expected bytes, got {other:?}"),
}
}
#[test]
fn operation_name_attribute() {
use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
let resource_info = OtlpResourceInfo::default();
let span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("my.operation"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
let kv = s
.attributes
.iter()
.find(|a| a.key == "operation.name")
.expect("operation.name attribute not found");
match kv.value.as_ref().unwrap().value {
Some(PV::StringValue(ref v)) => assert_eq!(v, "my.operation"),
ref other => panic!("expected string, got {other:?}"),
}
}
#[test]
fn span_type_attribute() {
use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
let resource_info = OtlpResourceInfo::default();
let span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("s"),
r#type: libdd_tinybytes::BytesString::from_static("grpc"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
let kv = s
.attributes
.iter()
.find(|a| a.key == "span.type")
.expect("span.type attribute not found");
match kv.value.as_ref().unwrap().value {
Some(PV::StringValue(ref v)) => assert_eq!(v, "grpc"),
ref other => panic!("expected string, got {other:?}"),
}
}
#[test]
fn resource_name_attribute_and_span_name() {
use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
let resource_info = OtlpResourceInfo::default();
let span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("s"),
resource: libdd_tinybytes::BytesString::from_static("GET /api/users"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
assert_eq!(s.name, "GET /api/users");
let kv = s
.attributes
.iter()
.find(|a| a.key == "resource.name")
.expect("resource.name attribute not found");
match kv.value.as_ref().unwrap().value {
Some(PV::StringValue(ref v)) => assert_eq!(v, "GET /api/users"),
ref other => panic!("expected string, got {other:?}"),
}
}
#[test]
fn empty_resource_name_not_emitted() {
let resource_info = OtlpResourceInfo::default();
let span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("s"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
assert!(
!s.attributes.iter().any(|a| a.key == "resource.name"),
"resource.name should not be emitted when resource is empty"
);
}
#[test]
fn per_span_service_name_attribute() {
use libdd_trace_protobuf::opentelemetry::proto::common::v1::any_value::Value as PV;
let resource_info = OtlpResourceInfo {
service: "resource-svc".to_string(),
..Default::default()
};
let span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("s"),
service: libdd_tinybytes::BytesString::from_static("span-svc"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
let kv = s
.attributes
.iter()
.find(|a| a.key == "service.name")
.expect("service.name attribute not found");
match kv.value.as_ref().unwrap().value {
Some(PV::StringValue(ref v)) => assert_eq!(v, "span-svc"),
ref other => panic!("expected string, got {other:?}"),
}
}
#[test]
fn unsampled_span_flags_zero() {
let resource_info = OtlpResourceInfo::default();
let mut span: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("s"),
start: 0,
duration: 1,
..Default::default()
};
span.metrics.insert("_sampling_priority_v1".into(), 0.0);
let req = map_traces_to_otlp(vec![vec![span]], &resource_info, false);
let s = &req.resource_spans[0].scope_spans[0].spans[0];
assert_eq!(s.flags, 0);
}
#[test]
fn trace_without_sampling_priority_leaves_sampled_flag_unset() {
let resource_info = OtlpResourceInfo::default();
let root: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("root"),
start: 0,
duration: 1,
..Default::default()
};
let child: Span<BytesData> = Span {
trace_id: 1,
span_id: 3,
parent_id: 2,
name: libdd_tinybytes::BytesString::from_static("child"),
start: 0,
duration: 1,
..Default::default()
};
let req = map_traces_to_otlp(vec![vec![root, child]], &resource_info, true);
let spans = &req.resource_spans[0].scope_spans[0].spans;
assert!(spans.iter().all(|span| span.flags == 0));
}
#[test]
fn sampled_trace_sets_sampled_flag_on_every_span() {
let resource_info = OtlpResourceInfo::default();
let root: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("root"),
start: 0,
duration: 1,
..Default::default()
};
let mut child: Span<BytesData> = Span {
trace_id: 1,
span_id: 3,
parent_id: 2,
name: libdd_tinybytes::BytesString::from_static("child"),
start: 0,
duration: 1,
..Default::default()
};
child.metrics.insert("_sampling_priority_v1".into(), 1.0);
let req = map_traces_to_otlp(vec![vec![root, child]], &resource_info, true);
let spans = &req.resource_spans[0].scope_spans[0].spans;
assert!(spans.iter().all(|span| span.flags == 1));
}
#[test]
fn first_priority_sets_sampled_flag_on_every_span() {
let resource_info = OtlpResourceInfo::default();
let mut root: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("root"),
start: 0,
duration: 1,
..Default::default()
};
root.metrics.insert("_sampling_priority_v1".into(), -1.0);
let mut child: Span<BytesData> = Span {
trace_id: 1,
span_id: 3,
parent_id: 2,
name: libdd_tinybytes::BytesString::from_static("child"),
start: 0,
duration: 1,
..Default::default()
};
child.metrics.insert("_sampling_priority_v1".into(), 1.0);
let req = map_traces_to_otlp(vec![vec![child, root]], &resource_info, true);
let spans = &req.resource_spans[0].scope_spans[0].spans;
assert!(spans.iter().all(|span| span.flags == 1));
}
#[test]
fn first_unsampled_priority_clears_sampled_flag_on_every_span() {
let resource_info = OtlpResourceInfo::default();
let mut first: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("first"),
start: 0,
duration: 1,
..Default::default()
};
first.metrics.insert("_sampling_priority_v1".into(), -1.0);
let mut second: Span<BytesData> = Span {
trace_id: 1,
span_id: 3,
parent_id: 2,
name: libdd_tinybytes::BytesString::from_static("second"),
start: 0,
duration: 1,
..Default::default()
};
second.metrics.insert("_sampling_priority_v1".into(), 1.0);
let req = map_traces_to_otlp(vec![vec![first, second]], &resource_info, true);
let spans = &req.resource_spans[0].scope_spans[0].spans;
assert!(spans.iter().all(|span| span.flags == 0));
}
#[test]
fn semantics_disabled_preserves_per_span_sampling_flags() {
let resource_info = OtlpResourceInfo::default();
let root: Span<BytesData> = Span {
trace_id: 1,
span_id: 2,
name: libdd_tinybytes::BytesString::from_static("root"),
start: 0,
duration: 1,
..Default::default()
};
let mut child: Span<BytesData> = Span {
trace_id: 1,
span_id: 3,
parent_id: 2,
name: libdd_tinybytes::BytesString::from_static("child"),
start: 0,
duration: 1,
..Default::default()
};
child.metrics.insert("_sampling_priority_v1".into(), 1.0);
let mut fractional: Span<BytesData> = Span {
trace_id: 1,
span_id: 4,
parent_id: 2,
name: libdd_tinybytes::BytesString::from_static("fractional"),
start: 0,
duration: 1,
..Default::default()
};
fractional
.metrics
.insert("_sampling_priority_v1".into(), 0.5);
let req = map_traces_to_otlp(vec![vec![root, child, fractional]], &resource_info, false);
let spans = &req.resource_spans[0].scope_spans[0].spans;
assert_eq!(spans[0].flags, 0);
assert_eq!(spans[1].flags, 1);
assert_eq!(spans[2].flags, 0);
}
}