Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
5190ab4
docs(otlp): design spec for OTLP HTTP/protobuf trace export
bm1549 Jun 12, 2026
946f116
docs(otlp): implementation plan for OTLP HTTP/protobuf trace export
bm1549 Jun 12, 2026
07c7296
feat(trace-protobuf): vendor + generate OTLP trace/collector prost types
bm1549 Jun 12, 2026
6f385ba
feat(trace-utils): add serde->prost OTLP converter
bm1549 Jun 12, 2026
a52e30b
refactor(trace-utils): clarify OTLP converter + add fallback/status t…
bm1549 Jun 12, 2026
4a4846a
feat(trace-utils): add encode_otlp_json/encode_otlp_protobuf
bm1549 Jun 12, 2026
54d8856
feat(data-pipeline): make OtlpProtocol public with FromStr
bm1549 Jun 12, 2026
772be3e
feat(data-pipeline): set OTLP content-type from protocol
bm1549 Jun 12, 2026
46d72a7
feat(data-pipeline): add TraceExporterBuilder::set_otlp_protocol
bm1549 Jun 12, 2026
8f3c38e
feat(data-pipeline): dispatch OTLP encoder by protocol + protobuf test
bm1549 Jun 12, 2026
2633427
refactor(data-pipeline): narrow otlp pub surface + exhaustive content…
bm1549 Jun 12, 2026
e493d8d
feat(data-pipeline-ffi): add ddog_trace_exporter_config_set_otlp_prot…
bm1549 Jun 12, 2026
4a81412
test(data-pipeline-ffi): cover set_otlp_protocol + clarify contract
bm1549 Jun 12, 2026
3091b57
fix(trace-protobuf): disable comments on vendored OTLP trace protos t…
bm1549 Jun 12, 2026
664f16f
docs(data-pipeline): clarify parse_u64/kind fallbacks and unreachable…
bm1549 Jun 12, 2026
9bc5cf2
chore: drop in-repo planning docs (moved to chonk)
bm1549 Jun 12, 2026
a8a305f
fix(data-pipeline): reject unsupported OTLP gRPC at build time
bm1549 Jun 12, 2026
58ba1b8
refactor(data-pipeline): mark OtlpProtocol non_exhaustive
bm1549 Jun 18, 2026
1cb1dfb
fix(data-pipeline-ffi): store parsed OtlpProtocol, drop silent re-parse
bm1549 Jun 18, 2026
b479255
test(trace-utils): extend OTLP parity test to trace_id, status, attri…
bm1549 Jun 18, 2026
55541eb
perf(trace-utils): build OTLP protobuf directly from native spans
bm1549 Jun 18, 2026
421cb6d
fix(trace-utils): explicit OTLP scope fields + clamp negative timestamps
bm1549 Jun 18, 2026
a790182
docs(data-pipeline-ffi): clarify OTLP setter is inert without endpoin…
bm1549 Jun 18, 2026
af79de7
feat(trace-utils): add OTLP/JSON serde serializer over prost types
bm1549 Jun 18, 2026
b21be02
refactor(trace-utils): prost OTLP types as single IR, delete json_types
bm1549 Jun 18, 2026
809914e
refactor(data-pipeline): OtlpWireProtocol encapsulates content-type +…
bm1549 Jun 18, 2026
855f4c1
fix(data-pipeline): reject OTLP gRPC only when an endpoint is configured
bm1549 Jun 19, 2026
5952434
refactor(otlp): crate-private OtlpWireProtocol/json_serializer, drop …
bm1549 Jun 19, 2026
21769ac
test(trace-utils): add OTLP encoder hot-path benchmarks
bm1549 Jun 19, 2026
f60fa27
perf(trace-utils): pre-size OTLP mapper Vecs, allocation-free JSON id…
bm1549 Jun 19, 2026
3431c1d
perf(trace-utils): serialize OTLP/JSON timestamps and ints from a sta…
bm1549 Jun 19, 2026
18f28c5
docs(trace-protobuf): retain OTLP proto doc comments; fence example a…
bm1549 Jun 19, 2026
00261ae
test(data-pipeline): skip live-build OTLP gRPC test under miri
bm1549 Jun 19, 2026
0b430a5
refactor(data-pipeline): drop unsupported gRPC from OtlpProtocol; add…
bm1549 Jun 22, 2026
968602b
Merge origin/main into brian.marks/otlp-http-protobuf-export
bm1549 Jun 22, 2026
f4b0054
fix(trace-utils): carry OTLP span-link flags through the mapper
bm1549 Jun 22, 2026
79706e7
test(trace-utils): trim OTLP benchmarks to the 1x1000 fixture
bm1549 Jun 22, 2026
11de6a0
Merge origin/main: integrate OTLP OTel-semantics (#2091) into the pro…
bm1549 Jun 23, 2026
77b9a73
fix(trace-utils): promote error.message (not just error.msg) to OTLP …
bm1549 Jun 23, 2026
80aad24
Potential fix for pull request finding
bm1549 Jun 23, 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
2 changes: 2 additions & 0 deletions Cargo.lock

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

140 changes: 136 additions & 4 deletions libdd-data-pipeline-ffi/src/trace_exporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use libdd_data_pipeline::trace_exporter::{
TelemetryConfig, TelemetryInstrumentationSessions, TraceExporter as GenericTraceExporter,
TraceExporterInputFormat, TraceExporterOutputFormat,
};
use libdd_data_pipeline::OtlpProtocol;

pub(crate) type TraceExporter = GenericTraceExporter<NativeCapabilities>;

Expand Down Expand Up @@ -83,6 +84,7 @@ pub struct TraceExporterConfig {
connection_timeout: Option<u64>,
shared_runtime: Option<Arc<SharedRuntime>>,
otlp_endpoint: Option<String>,
otlp_protocol: Option<OtlpProtocol>,
}

#[no_mangle]
Expand Down Expand Up @@ -498,12 +500,50 @@ pub unsafe extern "C" fn ddog_trace_exporter_config_set_otlp_endpoint(
)
}

/// Sets the OTLP export protocol. Accepts the OTel-standard values `http/json` (default) or
Comment thread
ekump marked this conversation as resolved.
/// `http/protobuf`; `grpc` is rejected as not yet supported. The host language resolves the value
/// (e.g. from `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL`).
///
/// Has no effect unless an OTLP endpoint is also configured via
/// `ddog_trace_exporter_config_set_otlp_endpoint`; without one, traces are sent to the
/// Datadog agent and this protocol selection is ignored.
///
/// Returns `None` on success, `ErrorCode::InvalidArgument` for a null config or an unaccepted
/// value, and `ErrorCode::InvalidInput` for a non-UTF-8 string.
#[no_mangle]
pub unsafe extern "C" fn ddog_trace_exporter_config_set_otlp_protocol(
config: Option<&mut TraceExporterConfig>,
protocol: CharSlice,
) -> Option<Box<ExporterError>> {
catch_panic!(
if let Some(handle) = config {
let value = match sanitize_string(protocol) {
Ok(s) => s,
Err(e) => return Some(e),
};
// `FromStr` is the single source of truth for string -> OtlpProtocol. It accepts only
// the supported HTTP encodings (`http/json`, `http/protobuf`); `grpc` and any unknown
// value are rejected with an error, so an unsupported protocol can never be stored.
match value.parse::<OtlpProtocol>() {
Ok(p) => {
handle.otlp_protocol = Some(p);
None
}
Err(_) => gen_error!(ErrorCode::InvalidArgument),
}
} 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.
/// When an OTLP endpoint is configured via `TraceExporterConfig`, the exporter sends traces to
/// that endpoint in OTLP over HTTP — JSON or protobuf per the configured protocol — instead of
/// to the Datadog agent. The same payload (e.g. MessagePack) is passed to
/// `ddog_trace_exporter_send`; the library decodes and converts it to OTLP when OTLP is enabled.
///
/// # Arguments
///
Expand Down Expand Up @@ -565,6 +605,9 @@ pub unsafe extern "C" fn ddog_trace_exporter_new(

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

match builder.build() {
Expand Down Expand Up @@ -1283,6 +1326,95 @@ mod tests {
}
}

#[test]
fn config_otlp_protocol_test() {
unsafe {
// Null config → InvalidArgument
let error =
ddog_trace_exporter_config_set_otlp_protocol(None, CharSlice::from("http/json"));
assert_eq!(error.as_ref().unwrap().code, ErrorCode::InvalidArgument);
ddog_trace_exporter_error_free(error);

// "http/json" → success, stored
let mut config = Some(TraceExporterConfig::default());
let error = ddog_trace_exporter_config_set_otlp_protocol(
config.as_mut(),
CharSlice::from("http/json"),
);
assert_eq!(error, None);
assert_eq!(
config.as_ref().unwrap().otlp_protocol,
Some(OtlpProtocol::HttpJson)
);

// "http/protobuf" → success, stored
let mut config = Some(TraceExporterConfig::default());
let error = ddog_trace_exporter_config_set_otlp_protocol(
config.as_mut(),
CharSlice::from("http/protobuf"),
);
assert_eq!(error, None);
assert_eq!(
config.as_ref().unwrap().otlp_protocol,
Some(OtlpProtocol::HttpProtobuf)
);

// "grpc" → InvalidArgument
let mut config = Some(TraceExporterConfig::default());
let error = ddog_trace_exporter_config_set_otlp_protocol(
config.as_mut(),
CharSlice::from("grpc"),
);
assert_eq!(error.as_ref().unwrap().code, ErrorCode::InvalidArgument);
ddog_trace_exporter_error_free(error);

// Garbage value → InvalidArgument
let mut config = Some(TraceExporterConfig::default());
let error = ddog_trace_exporter_config_set_otlp_protocol(
config.as_mut(),
CharSlice::from("nonsense"),
);
assert_eq!(error.as_ref().unwrap().code, ErrorCode::InvalidArgument);
ddog_trace_exporter_error_free(error);

// Non-UTF-8 input → InvalidInput
let mut config = Some(TraceExporterConfig::default());
let invalid: [u8; 2] = [0x80u8, 0xFFu8];
let error = ddog_trace_exporter_config_set_otlp_protocol(
config.as_mut(),
CharSlice::from_bytes(&invalid),
);
assert_eq!(error.as_ref().unwrap().code, ErrorCode::InvalidInput);
ddog_trace_exporter_error_free(error);
}
}

#[test]
fn set_otlp_protocol_stores_parsed_enum() {
use libdd_data_pipeline::OtlpProtocol;
let mut cfg = TraceExporterConfig::default();
let err = unsafe {
ddog_trace_exporter_config_set_otlp_protocol(
Some(&mut cfg),
CharSlice::from("http/protobuf"),
)
};
assert!(err.is_none());
assert_eq!(cfg.otlp_protocol, Some(OtlpProtocol::HttpProtobuf));
}

#[test]
fn set_otlp_protocol_rejects_grpc_and_unknown() {
let mut cfg = TraceExporterConfig::default();
for bad in ["grpc", "nonsense"] {
let err = unsafe {
ddog_trace_exporter_config_set_otlp_protocol(Some(&mut cfg), CharSlice::from(bad))
};
assert!(err.is_some(), "expected error for {bad}");
assert_eq!(cfg.otlp_protocol, None, "{bad} must not be stored");
}
}

#[cfg(all(feature = "catch_panic", panic = "unwind"))]
#[test]
fn catch_panic_test() {
Expand Down
1 change: 1 addition & 0 deletions libdd-data-pipeline/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ libdd-trace-utils = { path = "../libdd-trace-utils", features = [
"test-utils",
] }
httpmock = "0.8.0-alpha.1"
prost = "0.14.1"
rand = "0.8.5"
tempfile = "3.3.0"
tokio = { version = "1.23", features = [
Expand Down
3 changes: 3 additions & 0 deletions libdd-data-pipeline/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@
pub mod agent_info;
mod health_metrics;
pub(crate) mod otlp;
// `OtlpProtocol` (documented on the enum itself) is the only public symbol from the otherwise
// crate-internal `otlp` module; re-exported here so the FFI crate can name it.
pub use otlp::OtlpProtocol;
#[cfg(feature = "telemetry")]
pub(crate) mod telemetry;
#[cfg(not(target_arch = "wasm32"))]
Expand Down
105 changes: 91 additions & 14 deletions libdd-data-pipeline/src/otlp/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,60 @@
use http::HeaderMap;
use std::time::Duration;

/// OTLP trace export protocol. HTTP/JSON is currently supported.
/// OTLP trace export protocol — selects the HTTP body encoding and `Content-Type`.
///
/// Only the HTTP encodings libdatadog actually supports are representable. A `grpc` value (e.g.
/// resolved from the OTel-default `OTEL_EXPORTER_OTLP_PROTOCOL`) is rejected by
/// [`FromStr`](std::str::FromStr) rather than represented here, so an unsupported protocol can
/// never be constructed and silently mishandled downstream.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub(crate) enum OtlpProtocol {
/// HTTP with JSON body (Content-Type: application/json). Default for HTTP.
pub enum OtlpProtocol {
/// HTTP with a JSON body (`Content-Type: application/json`). The default.
#[default]
HttpJson,
/// HTTP with protobuf body. (Not supported yet)
#[allow(dead_code)]
/// HTTP with a protobuf body (`Content-Type: application/x-protobuf`).
HttpProtobuf,
/// gRPC. (Not supported yet)
#[allow(dead_code)]
Grpc,
}

impl std::str::FromStr for OtlpProtocol {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"http/json" => Ok(OtlpProtocol::HttpJson),
"http/protobuf" => Ok(OtlpProtocol::HttpProtobuf),
// gRPC is a valid OTLP protocol in the OTel spec but is not implemented in
// libdatadog. Reject it explicitly so callers get a clean error at the parse
// boundary, rather than constructing an unsupported value that has to be guarded
// against everywhere downstream.
"grpc" => Err("OTLP gRPC export is not supported".to_string()),
other => Err(format!("unknown OTLP protocol: {other}")),
}
}
}

impl OtlpProtocol {
/// The HTTP `Content-Type` for this protocol's body encoding. Crate-internal: the public type
/// is only constructed/selected by callers; encoding is the exporter's job.
pub(crate) fn content_type(&self) -> http::HeaderValue {
match self {
OtlpProtocol::HttpJson => libdd_common::header::APPLICATION_JSON,
OtlpProtocol::HttpProtobuf => libdd_common::header::APPLICATION_PROTOBUF,
}
}

/// Encode the prost OTLP request to this protocol's wire format. Crate-internal so the
/// third-party `serde_json::Error` does not leak into the public API.
pub(crate) fn encode(
&self,
req: &libdd_trace_utils::otlp_encoder::ProtoExportTraceServiceRequest,
) -> Result<Vec<u8>, serde_json::Error> {
match self {
OtlpProtocol::HttpJson => libdd_trace_utils::otlp_encoder::encode_otlp_json(req),
OtlpProtocol::HttpProtobuf => {
Ok(libdd_trace_utils::otlp_encoder::encode_otlp_protobuf(req))
}
}
}
}

/// Default timeout for OTLP export requests.
Expand All @@ -32,15 +74,50 @@ pub struct OtlpTraceConfig {
pub headers: HeaderMap,
/// Request timeout.
pub timeout: Duration,
/// Protocol (for future use; currently only HttpJson is supported).
#[allow(dead_code)]
pub(crate) protocol: OtlpProtocol,
/// When `true`, does not add DD-specific per-span attributes to the OTLP payload.
// These attributes are: (`service.name`, `operation.name`, `resource.name`,
// `span.type`, `error.msg`, `error.message`, `span.kind`)
/// OTLP export protocol (selects body encoding and content-type).
pub protocol: OtlpProtocol,
Comment thread
bm1549 marked this conversation as resolved.
/// When `true`, omit DD-specific per-span attributes (`service.name`, `operation.name`,
/// `resource.name`, `span.type`, `error.*`, `span.kind`) from the OTLP payload.
pub otel_trace_semantics_enabled: bool,
}

#[cfg(test)]
mod tests {
use super::*;
use std::str::FromStr;
#[test]
fn protocol_from_str() {
assert_eq!(
OtlpProtocol::from_str("http/json").unwrap(),
OtlpProtocol::HttpJson
);
assert_eq!(
OtlpProtocol::from_str("http/protobuf").unwrap(),
OtlpProtocol::HttpProtobuf
);
assert!(OtlpProtocol::from_str("nonsense").is_err());
}

#[test]
fn grpc_is_rejected_at_parse() {
// gRPC is unsupported, so it must not parse into a protocol: an unsupported value can
// never be constructed.
assert!(OtlpProtocol::from_str("grpc").is_err());
}

#[test]
fn protocol_content_types() {
assert_eq!(
OtlpProtocol::HttpJson.content_type(),
libdd_common::header::APPLICATION_JSON
);
assert_eq!(
OtlpProtocol::HttpProtobuf.content_type(),
libdd_common::header::APPLICATION_PROTOBUF
);
}
}

/// Parsed OTLP trace-metrics exporter configuration.
#[derive(Clone, Debug)]
pub struct OtlpMetricsConfig {
Expand Down
Loading
Loading