Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -114,8 +114,8 @@ tools/cc_utils/ @DataDog/libdatadog-php
tools/sidecar_mockgen/ @DataDog/libdatadog-php
libdd-data-pipeline/src/otlp/ @DataDog/apm-sdk-capabilities-rust
libdd-data-pipeline/tests/test_trace_exporter_otlp_export.rs @DataDog/apm-sdk-capabilities-rust
libdd-data-pipeline/tests/test_trace_exporter_otlp_grpc.rs @DataDog/apm-sdk-capabilities-rust
libdd-trace-utils/src/otlp_encoder/ @DataDog/apm-sdk-capabilities-rust
datadog-sidecar/src/service/ffe_exposures_flusher.rs @DataDog/libdatadog-php @DataDog/libdatadog-apm @DataDog/feature-flagging-and-experimentation-sdk
datadog-sidecar/src/service/ffe_metrics_flusher.rs @DataDog/libdatadog-php @DataDog/libdatadog-apm @DataDog/feature-flagging-and-experimentation-sdk
.github/workflows/nix.yml @DataDog/nix-guild @DataDog/apm-common-components-core

35 changes: 18 additions & 17 deletions libdd-data-pipeline-ffi/src/trace_exporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,11 +495,12 @@ pub unsafe extern "C" fn ddog_trace_exporter_config_set_shared_runtime(
)
}

/// Enables OTLP HTTP/JSON export and sets the endpoint URL.
/// Enables OTLP trace 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.
/// When set, traces are sent to this URL using the protocol selected by
/// `ddog_trace_exporter_config_set_otlp_protocol` 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>,
Expand All @@ -519,9 +520,10 @@ 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
/// `http/protobuf`; `grpc` is rejected as not yet supported. The host language resolves the value
/// Sets the OTLP export protocol. Accepts the OTel-standard values `http/json` (default),
/// `http/protobuf`, or `grpc`; unknown values are rejected. The host language resolves the value
/// (e.g. from `OTEL_EXPORTER_OTLP_TRACES_PROTOCOL`).
/// The `grpc` protocol currently supports plaintext `http://` endpoints only.
///
/// 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
Expand All @@ -540,9 +542,6 @@ pub unsafe extern "C" fn ddog_trace_exporter_config_set_otlp_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);
Expand Down Expand Up @@ -754,9 +753,9 @@ pub unsafe extern "C" fn ddog_trace_exporter_config_set_output_to_log(

/// Create a new TraceExporter instance.
///
/// 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
/// When an OTLP endpoint is configured via `TraceExporterConfig`, the exporter sends traces using
/// the configured `http/json`, `http/protobuf`, or `grpc` protocol instead of 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 @@ -1626,14 +1625,16 @@ mod tests {
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);
assert_eq!(error, None);
assert_eq!(
config.as_ref().unwrap().otlp_protocol,
Some(OtlpProtocol::Grpc)
);

// Garbage value → InvalidArgument
let mut config = Some(TraceExporterConfig::default());
Expand Down Expand Up @@ -1714,9 +1715,9 @@ mod tests {
}

#[test]
fn set_otlp_protocol_rejects_grpc_and_unknown() {
fn set_otlp_protocol_rejects_unknown() {
let mut cfg = TraceExporterConfig::default();
for bad in ["grpc", "nonsense"] {
for bad in ["nonsense", "grcp"] {
let err = unsafe {
ddog_trace_exporter_config_set_otlp_protocol(Some(&mut cfg), CharSlice::from(bad))
};
Expand Down
4 changes: 2 additions & 2 deletions libdd-data-pipeline/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,8 +57,10 @@ libdd-capabilities-impl = { version = "4.0.0", path = "../libdd-capabilities-imp
# (hyper/tokio/socket2) does not build for wasm32, so the whole gRPC path is gated off.
tonic = { version = "0.14", default-features = false }
prost = "0.14.1"
h2 = "0.4"
hyper = { workspace = true, features = ["client", "http2"] }
hyper-util = { workspace = true, features = ["tokio"] }
rand = "0.8.5"

[target.'cfg(target_arch = "wasm32")'.dependencies]
getrandom = { version = "0.2", features = ["js"] }
Expand Down Expand Up @@ -87,7 +89,6 @@ libdd-trace-utils = { path = "../libdd-trace-utils", features = [
] }
httpmock = "0.8.0-alpha.1"
prost = "0.14.1"
rand = "0.8.5"
tempfile.workspace = true
tokio = { version = "1.23", features = [
"rt",
Expand All @@ -97,7 +98,6 @@ tokio = { version = "1.23", features = [
duplicate = "2.0.1"

[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
h2 = "0.4"
zstd = { version = "0.13", default-features = false }

[target.'cfg(target_arch = "wasm32")'.dev-dependencies]
Expand Down
53 changes: 19 additions & 34 deletions libdd-data-pipeline/src/otlp/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,16 @@
use http::HeaderMap;
use std::time::Duration;

/// 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.
/// OTLP trace export protocol: selects the wire transport and body encoding.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OtlpProtocol {
/// HTTP with a JSON body (`Content-Type: application/json`). The default.
#[default]
HttpJson,
/// HTTP with a protobuf body (`Content-Type: application/x-protobuf`).
HttpProtobuf,
/// gRPC over HTTP/2.
Grpc,
}

impl std::str::FromStr for OtlpProtocol {
Expand All @@ -27,37 +24,31 @@ impl std::str::FromStr for OtlpProtocol {
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()),
"grpc" => Ok(OtlpProtocol::Grpc),
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 {
pub(crate) fn content_type(&self) -> Option<http::HeaderValue> {
match self {
OtlpProtocol::HttpJson => libdd_common::header::APPLICATION_JSON,
OtlpProtocol::HttpProtobuf => libdd_common::header::APPLICATION_PROTOBUF,
OtlpProtocol::HttpJson => Some(libdd_common::header::APPLICATION_JSON),
OtlpProtocol::HttpProtobuf => Some(libdd_common::header::APPLICATION_PROTOBUF),
OtlpProtocol::Grpc => None,
}
}

/// 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> {
) -> Option<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))
}
OtlpProtocol::HttpJson => Some(libdd_trace_utils::otlp_encoder::encode_otlp_json(req)),
OtlpProtocol::HttpProtobuf => Some(Ok(
libdd_trace_utils::otlp_encoder::encode_otlp_protobuf(req),
)),
OtlpProtocol::Grpc => None,
}
}
}
Expand Down Expand Up @@ -86,8 +77,7 @@ pub struct OtlpTraceConfig {
}

/// Per-request OTLP gRPC trace exporter configuration.
// Not yet wired to the trace exporter's send loop; exercised by tests only.
#[allow(dead_code)]
#[cfg(not(target_arch = "wasm32"))]
#[derive(Clone, Debug)]
pub struct OtlpGrpcTraceConfig {
/// Custom key-value pairs forwarded as gRPC request metadata.
Expand All @@ -112,26 +102,21 @@ mod tests {
OtlpProtocol::from_str("http/protobuf").unwrap(),
OtlpProtocol::HttpProtobuf
);
assert_eq!(OtlpProtocol::from_str("grpc").unwrap(), OtlpProtocol::Grpc);
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
Some(libdd_common::header::APPLICATION_JSON)
);
assert_eq!(
OtlpProtocol::HttpProtobuf.content_type(),
libdd_common::header::APPLICATION_PROTOBUF
Some(libdd_common::header::APPLICATION_PROTOBUF)
);
assert_eq!(OtlpProtocol::Grpc.content_type(), None);
}
}

Expand Down
9 changes: 7 additions & 2 deletions libdd-data-pipeline/src/otlp/exporter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ use std::time::Duration;
pub(crate) const OTLP_MAX_RETRIES: u32 = 4;
/// No retries on shutdown to avoid a long backoff in the shutdown window.
pub(crate) const OTLP_SHUTDOWN_MAX_RETRIES: u32 = 0;
const OTLP_RETRY_DELAY_MS: u64 = 100;
pub(crate) const OTLP_RETRY_DELAY_MS: u64 = 100;

/// POST an OTLP HTTP payload to `endpoint_url` with the given `content_type` (callers pass JSON or
/// protobuf); `test_token` enables snapshot tests.
Expand Down Expand Up @@ -125,13 +125,18 @@ pub async fn send_otlp_traces_http<C: HttpClientCapability + SleepCapability>(
test_token: Option<&str>,
body: Vec<u8>,
) -> Result<(), TraceExporterError> {
let content_type = config.protocol.content_type().ok_or_else(|| {
TraceExporterError::Internal(InternalErrorKind::InvalidWorkerState(
"OTLP gRPC protocol cannot be sent over the HTTP export path".to_string(),
))
})?;
send_otlp_http(
capabilities,
&config.endpoint_url,
&config.headers,
config.timeout,
test_token,
config.protocol.content_type(),
content_type,
body,
OTLP_MAX_RETRIES,
)
Expand Down
Loading
Loading