Skip to content
Merged
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
1 change: 1 addition & 0 deletions libdd-data-pipeline/src/otlp/metrics.rs
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ mod tests {
Some(vec!["db.hostname:prod-db-1", "db.name:orders"])
);
assert_eq!(str_at(a, "status.code"), Some(STATUS_CODE_OK));
assert_eq!(a.iter().filter(|kv| kv["key"] == "status.code").count(), 1);
let custom = pts
.iter()
.find(|p| {
Expand Down
4 changes: 3 additions & 1 deletion libdd-data-pipeline/src/trace_exporter/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,8 @@ impl<

let mut header_tags: TracerHeaderTags = self.metadata.borrow().into();

// Process stats computation and drop non-sampled (p0) chunks.
// This must run before the OTLP path so that unsampled spans are not exported.
let client_side_stats = stats::process_traces_for_stats(
&mut traces,
&mut header_tags,
Expand Down Expand Up @@ -840,7 +842,7 @@ impl<
// OTLP path: send sampled traces via OTLP when an OTLP endpoint is configured.
// Unlike the agent path, there is no downstream agent to drop unsampled traces,
// so drop_chunks is always called here regardless of whether stats are enabled.
if let Some(ref config) = self.otlp_config {
if let Some(config) = self.otlp_config.as_ref() {
libdd_trace_utils::span::trace_utils::drop_chunks(&mut traces);
if traces.is_empty() {
return Ok(AgentResponse::Unchanged);
Expand Down
120 changes: 109 additions & 11 deletions libdd-data-pipeline/tests/test_trace_exporter_otlp_export.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,35 +3,58 @@
#[cfg(test)]
mod otlp_export_tests {
use libdd_capabilities_impl::NativeCapabilities;
use libdd_data_pipeline::trace_exporter::TraceExporterBuilder;
use libdd_trace_utils::test_utils::create_test_json_span;
use libdd_data_pipeline::trace_exporter::{
TraceExporterBuilder, TraceExporterInputFormat, TraceExporterOutputFormat,
};
use libdd_trace_utils::span::v05::dict::SharedDict;
use libdd_trace_utils::test_utils::{create_test_json_span, create_test_v05_span};
use serde_json::json;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use tokio::task;

fn get_v04_trace_snapshot_test_payload(name_prefix: &str) -> Vec<u8> {
let mut span_1 = create_test_json_span(1234, 12342, 12341, 1, false);
let mut span_1 = create_test_json_span(1234, 12342, 0, 1, false);
span_1["name"] = json!(format!("{name_prefix}_01"));
span_1["metrics"] = json!({
"_dd_metric1": 1.0,
"_dd_metric2": 2.0
"_dd_metric2": 2.0,
"_sampling_priority_v1": 1.0
});
let mut span_2 = create_test_json_span(1234, 12343, 12341, 1, false);
let mut span_2 = create_test_json_span(1234, 12343, 12342, 1, false);
span_2["name"] = json!(format!("{name_prefix}_02"));
rmp_serde::to_vec_named(&vec![vec![span_1, span_2]]).unwrap()
}

fn get_v05_sampled_trace_payload() -> Vec<u8> {
let mut dict = SharedDict::default();
let span = create_test_v05_span(
1234,
12342,
0,
1,
true,
&mut dict,
Some(vec![("_sampling_priority_v1".to_string(), 1.0)]),
);
rmp_serde::to_vec(&(dict, vec![vec![span]])).unwrap()
}

#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn otlp_export_sends_correct_payload() {
use httpmock::MockServer;

let server = MockServer::start_async().await;
let body_valid = Arc::new(AtomicBool::new(false));
let matcher_flag = body_valid.clone();

// Assert the OTLP request structure using json_body_includes matchers.
// resourceSpans must be present with the expected resource attributes, including
// client-computed-stats, and the redundant header must be set for direct Agent requests.
let mut mock = server
.mock_async(|when, then| {
// Assert the request structure with json_body_includes, then inspect the complete span
// array so a child without its own sampling priority must still carry the chunk-level
// sampled decision.
let mock = server
.mock_async(move |when, then| {
let flag = matcher_flag.clone();
when.method("POST")
.path("/v1/traces")
.header("content-type", "application/json")
Expand All @@ -53,7 +76,36 @@ mod otlp_export_tests {
}]
})
.to_string(),
);
)
.is_true(move |req: &httpmock::prelude::HttpMockRequest| {
let Ok(body) = serde_json::from_slice::<serde_json::Value>(req.body_ref())
else {
return false;
};
let has_attribute = |span: &serde_json::Value, key: &str| {
span["attributes"].as_array().is_some_and(|attributes| {
attributes
.iter()
.any(|attribute| attribute["key"] == key)
})
};
let valid = body["resourceSpans"][0]["scopeSpans"][0]["spans"]
.as_array()
.is_some_and(|spans| {
spans.len() == 2
&& spans.iter().all(|span| span["flags"] == 1)
&& spans.iter().any(|span| {
has_attribute(span, "_sampling_priority_v1")
})
&& spans.iter().any(|span| {
!has_attribute(span, "_sampling_priority_v1")
})
});
if valid {
flag.store(true, Ordering::SeqCst);
}
valid
});
then.status(200).body("{}");
})
.await;
Expand All @@ -72,6 +124,7 @@ mod otlp_export_tests {
.set_env("test_env")
.set_service("test")
.set_runtime_id("test-runtime-id")
.enable_otel_trace_semantics()
.set_client_computed_stats();

let trace_exporter = builder
Expand All @@ -83,6 +136,51 @@ mod otlp_export_tests {
})
.await;

assert!(task_result.is_ok());
assert_eq!(mock.calls_async().await, 1);
assert!(
body_valid.load(Ordering::SeqCst),
"OTLP payload did not set sampled flags on both spans"
);
}

#[cfg_attr(miri, ignore)]
#[tokio::test]
async fn otlp_v05_export_sets_sampled_flag() {
use httpmock::MockServer;

let server = MockServer::start_async().await;
let mut mock = server
.mock_async(|when, then| {
when.method("POST").path("/v1/traces").json_body_includes(
serde_json::json!({
"resourceSpans": [{
"scopeSpans": [{
"spans": [{"flags": 1}]
}]
}]
})
.to_string(),
);
then.status(200).body("{}");
})
.await;

let otlp_endpoint = format!("http://localhost:{}/v1/traces", server.port());
let task_result = task::spawn_blocking(move || {
let mut builder = TraceExporterBuilder::default();
builder
.set_otlp_endpoint(&otlp_endpoint)
.set_input_format(TraceExporterInputFormat::V05)
.set_output_format(TraceExporterOutputFormat::V05);
let trace_exporter = builder
.build::<NativeCapabilities>()
.expect("Unable to build TraceExporter");
let response = trace_exporter.send(&get_v05_sampled_trace_payload());
assert!(response.is_ok(), "OTLP send failed: {:?}", response.err());
})
.await;

assert!(task_result.is_ok());
assert_eq!(mock.calls_async().await, 1);
mock.delete();
Expand Down
Loading
Loading