Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
1e86c8d
WIP: OTLP trace export
rachelyangdog Mar 2, 2026
01662c2
linting
rachelyangdog Mar 2, 2026
91a6559
linting on comments
rachelyangdog Mar 2, 2026
293ecd8
fix endpoint and mapping
rachelyangdog Mar 3, 2026
2dd4b57
implement feedback
rachelyangdog Mar 6, 2026
42de22c
lint and private fields
rachelyangdog Mar 6, 2026
f359ff5
send_with_retry and move to libdd_trace_utils
rachelyangdog Mar 10, 2026
9bbc48e
lint + nits
rachelyangdog Mar 10, 2026
db08608
lint
rachelyangdog Mar 10, 2026
f1f7921
config change and sampling update
rachelyangdog Mar 11, 2026
82435cb
Merge branch 'main' into rachel.yang/OTLP-trace-export
rachelyangdog Mar 11, 2026
e1b519b
linting issues
rachelyangdog Mar 16, 2026
8773444
Merge branch 'main' into rachel.yang/OTLP-trace-export
rachelyangdog Mar 18, 2026
c0cfacf
merge refactor and update
rachelyangdog Mar 18, 2026
679110c
add test
rachelyangdog Mar 18, 2026
eeeb68f
more edits
rachelyangdog Mar 18, 2026
381c488
header change
rachelyangdog Mar 19, 2026
ad516d6
edits from review
rachelyangdog Mar 23, 2026
2ab492f
linting
rachelyangdog Mar 23, 2026
9d5e0f6
Merge branch 'main' into rachel.yang/OTLP-trace-export
rachelyangdog Mar 23, 2026
e59b071
lint
rachelyangdog Mar 23, 2026
66e5b43
zach review
rachelyangdog Mar 23, 2026
a662508
fix lint and errors
rachelyangdog Mar 23, 2026
eec9836
revisions
rachelyangdog Mar 24, 2026
e4ca73c
revisions part 2
rachelyangdog Mar 24, 2026
39e639b
lint and cargo
rachelyangdog Mar 24, 2026
1c7f3dd
span name edit
rachelyangdog Mar 24, 2026
b964349
error message update
rachelyangdog Mar 24, 2026
7f9cf75
service.name and scope update
rachelyangdog Mar 25, 2026
1285cd1
update codeowners
rachelyangdog Mar 25, 2026
7a031ef
remove some attributes and refactor
rachelyangdog Mar 25, 2026
2b376c8
linter
rachelyangdog Mar 25, 2026
e8c15c1
refactor tests
rachelyangdog Mar 25, 2026
4acc420
update codeowners
rachelyangdog Mar 25, 2026
1b6d3f4
new attributes
rachelyangdog Mar 26, 2026
8fc9a14
add metastruct and tracestate
rachelyangdog Mar 26, 2026
a167acb
minor nit fixes
rachelyangdog Mar 26, 2026
3b26275
Merge branch 'main' into rachel.yang/OTLP-trace-export
rachelyangdog Mar 26, 2026
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
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -81,3 +81,6 @@ bin_tests/tests/test_the_tests.rs @DataDog/libdatadog-core
bin_tests/src/bin/test_the_tests.rs @DataDog/libdatadog-core
tools/cc_utils/ @DataDog/libdatadog-php
tools/sidecar_mockgen/ @DataDog/libdatadog-php
libdd-data-pipeline/src/otlp/ @DataDog/apm-sdk-capabilities
libdd-data-pipeline/tests/test_trace_exporter_otlp_export.rs @DataDog/apm-sdk-capabilities
libdd-trace-utils/src/otlp_encoder/ @DataDog/apm-sdk-capabilities
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

34 changes: 34 additions & 0 deletions libdd-data-pipeline-ffi/src/trace_exporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ pub struct TraceExporterConfig {
process_tags: Option<String>,
test_session_token: Option<String>,
connection_timeout: Option<u64>,
otlp_endpoint: Option<String>,
}

#[no_mangle]
Expand Down Expand Up @@ -414,8 +415,37 @@ pub unsafe extern "C" fn ddog_trace_exporter_config_set_connection_timeout(
)
}

/// Enables OTLP HTTP/JSON export and sets the endpoint URL.
///
/// When set, traces are sent to this URL in OTLP HTTP/JSON format instead of the Datadog
/// agent. The host language is responsible for resolving the endpoint from its configuration
/// (e.g. `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) before calling this function.
#[no_mangle]
pub unsafe extern "C" fn ddog_trace_exporter_config_set_otlp_endpoint(
config: Option<&mut TraceExporterConfig>,
url: CharSlice,
) -> Option<Box<ExporterError>> {
catch_panic!(
if let Some(handle) = config {
handle.otlp_endpoint = match sanitize_string(url) {
Ok(s) => Some(s),
Err(e) => return Some(e),
};
None
} else {
gen_error!(ErrorCode::InvalidArgument)
},
gen_error!(ErrorCode::Panic)
)
}

/// Create a new TraceExporter instance.
///
/// When an OTLP endpoint is configured via `TraceExporterConfig`, the exporter sends traces in
/// OTLP HTTP/JSON to that endpoint instead of the Datadog agent. The same payload (e.g.
/// MessagePack) is passed to `ddog_trace_exporter_send`; the library decodes and converts to
/// OTLP when OTLP is enabled.
///
/// # Arguments
///
/// * `out_handle` - The handle to write the TraceExporter instance in.
Expand Down Expand Up @@ -467,6 +497,10 @@ pub unsafe extern "C" fn ddog_trace_exporter_new(
builder.enable_health_metrics();
}

if let Some(ref url) = config.otlp_endpoint {
builder.set_otlp_endpoint(url);
}

match builder.build() {
Ok(exporter) => {
out_handle.as_ptr().write(Box::new(exporter));
Expand Down
1 change: 1 addition & 0 deletions libdd-data-pipeline/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

pub mod agent_info;
mod health_metrics;
pub(crate) mod otlp;
mod pausable_worker;
#[allow(missing_docs)]
pub mod stats_exporter;
Expand Down
38 changes: 38 additions & 0 deletions libdd-data-pipeline/src/otlp/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0
Comment thread
rachelyangdog marked this conversation as resolved.

//! OTLP trace export configuration.

use http::HeaderMap;
use std::time::Duration;

/// OTLP trace export protocol. HTTP/JSON is currently supported.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum OtlpProtocol {
/// HTTP with JSON body (Content-Type: application/json). Default for HTTP.
#[default]
HttpJson,
/// HTTP with protobuf body. (Not supported yet)
#[allow(dead_code)]
HttpProtobuf,
/// gRPC. (Not supported yet)
#[allow(dead_code)]
Grpc,
}

/// Default timeout for OTLP export requests.
pub const DEFAULT_OTLP_TIMEOUT: Duration = Duration::from_secs(10);

/// Parsed OTLP trace exporter configuration.
#[derive(Clone, Debug)]
pub struct OtlpTraceConfig {
/// Full URL to POST traces to (e.g. `http://localhost:4318/v1/traces`).
pub endpoint_url: String,
/// Pre-validated HTTP headers to include in each request.
pub headers: HeaderMap,
/// Request timeout.
pub timeout: Duration,
/// Protocol (for future use; currently only HttpJson is supported).
#[allow(dead_code)]
pub(crate) protocol: OtlpProtocol,
}
88 changes: 88 additions & 0 deletions libdd-data-pipeline/src/otlp/exporter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
// SPDX-License-Identifier: Apache-2.0

//! OTLP HTTP/JSON trace exporter.

use super::config::OtlpTraceConfig;
use crate::trace_exporter::error::{InternalErrorKind, RequestError, TraceExporterError};
use libdd_common::{http_common, Endpoint, HttpClient};
use libdd_trace_utils::send_with_retry::{
send_with_retry, RetryBackoffType, RetryStrategy, SendWithRetryError,
};

/// Max total attempts for OTLP export (1 initial + up to 4 retries on transient failures).
const OTLP_MAX_ATTEMPTS: u32 = 5;
/// Initial backoff between retries (milliseconds).
const OTLP_RETRY_DELAY_MS: u64 = 100;

/// Send OTLP trace payload (JSON bytes) to the configured endpoint with retries.
///
/// Uses [`send_with_retry`] for consistent retry behaviour and observability across exporters.
///
/// `test_token` is forwarded as `X-Datadog-Test-Session-Token` when set, enabling snapshot tests
/// against the Datadog test agent's OTLP endpoint.
pub async fn send_otlp_traces_http(
client: &HttpClient,
config: &OtlpTraceConfig,
test_token: Option<&str>,
json_body: Vec<u8>,
) -> Result<(), TraceExporterError> {
Comment thread
rachelyangdog marked this conversation as resolved.
let url = libdd_common::parse_uri(&config.endpoint_url).map_err(|e| {
TraceExporterError::Internal(InternalErrorKind::InvalidWorkerState(format!(
"Invalid OTLP endpoint URL: {}",
e
)))
})?;

let target = Endpoint {
url,
timeout_ms: config.timeout.as_millis() as u64,
..Endpoint::default()
};

let mut headers = config.headers.clone();
headers.insert(
http::header::CONTENT_TYPE,
libdd_common::header::APPLICATION_JSON,
);
if let Some(token) = test_token {
if let Ok(val) = http::HeaderValue::from_str(token) {
headers.insert(
http::HeaderName::from_static("x-datadog-test-session-token"),
val,
);
}
}

let retry_strategy = RetryStrategy::new(
OTLP_MAX_ATTEMPTS,
OTLP_RETRY_DELAY_MS,
RetryBackoffType::Exponential,
None,
);

match send_with_retry(client, &target, json_body, &headers, &retry_strategy).await {
Ok(_) => Ok(()),
Err(e) => Err(map_send_error(e).await),
}
}

async fn map_send_error(err: SendWithRetryError) -> TraceExporterError {
match err {
SendWithRetryError::Http(response, _) => {
let status = response.status();
let body_bytes = http_common::collect_response_bytes(response)
.await
.unwrap_or_default();
let body_str = String::from_utf8_lossy(&body_bytes);
TraceExporterError::Request(RequestError::new(status, &body_str))
}
SendWithRetryError::Timeout(_) => {
TraceExporterError::Io(std::io::Error::from(std::io::ErrorKind::TimedOut))
}
SendWithRetryError::Network(error, _) => TraceExporterError::from(error),
SendWithRetryError::Build(_) => TraceExporterError::Internal(
InternalErrorKind::InvalidWorkerState("Failed to build OTLP request".to_string()),
),
}
}
30 changes: 30 additions & 0 deletions libdd-data-pipeline/src/otlp/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright 2024-Present Datadog, Inc. https://www.datadoghq.com/
Comment thread
rachelyangdog marked this conversation as resolved.
// SPDX-License-Identifier: Apache-2.0

//! OTLP trace export for libdatadog.
//!
//! When an OTLP endpoint is configured via
//! [`crate::trace_exporter::TraceExporterBuilder::set_otlp_endpoint`], the trace exporter sends
//! traces in OTLP HTTP/JSON format to that endpoint instead of the Datadog agent. The host language
//! is responsible for resolving the endpoint from its own configuration (e.g.
//! `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`).
//!
//! ## Sampling
//!
//! The exporter enforces the sampling decision already made by the tracer: unsampled chunks are
//! dropped via `drop_chunks` before export. It does not apply its own sampling policy. The tracer
//! (e.g. dd-trace-py) is responsible for inheriting the sampling decision from the distributed
//! trace context; when no decision is present, the tracer typically uses 100% (always on).
//!
//! ## Partial flush
//!
//! For the POC, partial flush is disabled. The tracer should only invoke the exporter when all
//! spans from a local trace are closed (i.e. send complete trace chunks). This crate does not
//! buffer or flush partially—it exports whatever trace chunks it receives.

pub mod config;
pub mod exporter;

pub use config::OtlpTraceConfig;
pub use exporter::send_otlp_traces_http;
pub use libdd_trace_utils::otlp_encoder::{map_traces_to_otlp, OtlpResourceInfo};
51 changes: 51 additions & 0 deletions libdd-data-pipeline/src/trace_exporter/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
// SPDX-License-Identifier: Apache-2.0

use crate::agent_info::AgentInfoFetcher;
use crate::otlp::config::{OtlpProtocol, DEFAULT_OTLP_TIMEOUT};
use crate::otlp::OtlpTraceConfig;
use crate::pausable_worker::PausableWorker;
use crate::telemetry::TelemetryClientBuilder;
use crate::trace_exporter::agent_response::AgentResponsePayloadVersion;
Expand Down Expand Up @@ -51,6 +53,8 @@ pub struct TraceExporterBuilder {
test_session_token: Option<String>,
agent_rates_payload_version_enabled: bool,
connection_timeout: Option<u64>,
otlp_endpoint: Option<String>,
otlp_headers: Vec<(String, String)>,
}

impl TraceExporterBuilder {
Expand Down Expand Up @@ -223,6 +227,28 @@ impl TraceExporterBuilder {
self
}

/// Enables OTLP HTTP/JSON export and sets the endpoint URL.
///
/// When set, traces are sent to this endpoint in OTLP HTTP/JSON format instead of the
/// Datadog agent. The host language is responsible for resolving the endpoint from its
/// configuration (e.g. `OTEL_EXPORTER_OTLP_TRACES_ENDPOINT`) before calling this method.
///
/// Example: `set_otlp_endpoint("http://localhost:4318/v1/traces")`
pub fn set_otlp_endpoint(&mut self, url: &str) -> &mut Self {
self.otlp_endpoint = Some(url.to_owned());
self
}

/// Sets additional HTTP headers to include in OTLP trace export requests.
///
/// Headers should be provided as key-value pairs. The host language is responsible for
/// resolving headers from its configuration (e.g. `OTEL_EXPORTER_OTLP_TRACES_HEADERS`)
/// before calling this method.
pub fn set_otlp_headers(&mut self, headers: Vec<(String, String)>) -> &mut Self {
self.otlp_headers = headers;
self
}

#[allow(missing_docs)]
pub fn build(self) -> Result<TraceExporter, TraceExporterError> {
if !Self::is_inputs_outputs_formats_compatible(self.input_format, self.output_format) {
Expand Down Expand Up @@ -345,6 +371,31 @@ impl TraceExporterBuilder {
.agent_rates_payload_version_enabled
.then(AgentResponsePayloadVersion::new),
http_client: new_default_client(),
otlp_config: self.otlp_endpoint.map(|url| {
let mut headers = http::HeaderMap::new();
for (key, value) in self.otlp_headers {
match (
http::HeaderName::from_bytes(key.as_bytes()),
http::HeaderValue::from_str(&value),
) {
(Ok(name), Ok(val)) => {
headers.insert(name, val);
}
_ => {
tracing::warn!("Skipping invalid OTLP header: {:?}={:?}", key, value);
}
}
}
OtlpTraceConfig {
endpoint_url: url,
headers,
timeout: self
.connection_timeout
.map(Duration::from_millis)
.unwrap_or(DEFAULT_OTLP_TIMEOUT),
protocol: OtlpProtocol::HttpJson,
}
}),
})
}

Expand Down
Loading
Loading