From 16456cc3007824ef272edc0d07bc37ea181f39a6 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Tue, 9 Jun 2026 18:44:41 -0400 Subject: [PATCH 1/4] feat(otlp): plumb OTLP trace export config to the sidecar Add config plumbing for OTLP trace export, triggered by OTEL_TRACES_EXPORTER=otlp, mirroring the existing OTLP metrics endpoint path. Configuration: - OTEL_TRACES_EXPORTER=otlp now enables OTLP trace export via a new DD_TRACE_OTLP_ENABLED config (previously only "none" was handled). - OTEL_EXPORTER_OTLP_TRACES_ENDPOINT used as-is; falls back to OTEL_EXPORTER_OTLP_ENDPOINT (trailing slash stripped, /v1/traces appended); computed default http://:4318/v1/traces. - OTEL_EXPORTER_OTLP_TRACES_HEADERS / _TIMEOUT / _PROTOCOL parsed and reported, with fallbacks to the non-signal-specific OTEL_EXPORTER_OTLP_* variants. Only http/json is honored for the protocol. - All new configs flow through config telemetry like any other entry. Gates: - OTLP trace export is enabled iff OTEL_TRACES_EXPORTER=otlp. - DD_TRACE_AGENT_PROTOCOL_VERSION being set disables OTLP trace export (the agent trace protocol version takes precedence); a notice is logged. - Agent interaction (/info, Remote Config, telemetry) is unaffected: the OTLP traces endpoint is kept separate from the agent endpoint, exactly as the OTLP metrics endpoint already is. Sampler: - OTEL_TRACES_SAMPLER mapping to the DD sample rate now warns when a non-parentbased sampler is mapped to its parentbased equivalent. Wiring: - New Rust glue (components-rs): datadog_otel_traces_endpoint_from_url / _from_agent_url build the endpoint; ddog_sidecar_session_set_otlp_traces_endpoint / _clear register the resolved endpoint/headers/timeout per session. - ext/sidecar.c forwards the resolved OTLP traces endpoint to the sidecar in dd_sidecar_post_connect, alongside the OTLP metrics endpoint. Note: the libdatadog submodule is intentionally not modified. The sidecar's trace flush path (TraceFlusher/SendData to the agent endpoint) and the ddog_sidecar_session_set_config SessionConfig live in libdatadog and would need changes there to consume the registered OTLP traces endpoint via the OTLP TraceExporter. This change delivers the complete extension-side config plumbing and the FFI surface to carry the endpoint to the sidecar. Tests: - Rust unit tests for the endpoint builders and the session-config registry (components-rs). - .phpt tests: OTEL_TRACES_EXPORTER=otlp selection, endpoint fallback, explicit endpoint/headers/timeout/protocol, the DD_TRACE_AGENT_PROTOCOL_VERSION disable gate, and exporter=none. Co-Authored-By: Claude Opus 4.8 (1M context) --- components-rs/datadog.h | 28 ++++ components-rs/lib.rs | 87 ++++++++++ components-rs/sidecar.rs | 151 ++++++++++++++++++ ext/configuration.h | 15 ++ ext/endpoints.c | 69 ++++++++ ext/endpoints.h | 2 + ext/otel_config.c | 64 +++++++- ext/otel_config.h | 5 + ext/sidecar.c | 28 ++++ ext/sidecar.h | 3 + tests/ext/otlp_traces_endpoint_fallback.phpt | 17 ++ tests/ext/otlp_traces_explicit_config.phpt | 28 ++++ tests/ext/otlp_traces_exporter_enabled.phpt | 18 +++ tests/ext/otlp_traces_exporter_none.phpt | 18 +++ .../otlp_traces_protocol_version_gate.phpt | 19 +++ tracer/tracer_otel_config.c | 18 +++ 16 files changed, 567 insertions(+), 3 deletions(-) create mode 100644 tests/ext/otlp_traces_endpoint_fallback.phpt create mode 100644 tests/ext/otlp_traces_explicit_config.phpt create mode 100644 tests/ext/otlp_traces_exporter_enabled.phpt create mode 100644 tests/ext/otlp_traces_exporter_none.phpt create mode 100644 tests/ext/otlp_traces_protocol_version_gate.phpt diff --git a/components-rs/datadog.h b/components-rs/datadog.h index 2a75fb13b9b..09e487a61d4 100644 --- a/components-rs/datadog.h +++ b/components-rs/datadog.h @@ -55,6 +55,34 @@ struct ddog_Endpoint *datadog_otel_metrics_endpoint_from_url(ddog_CharSlice url) struct ddog_Endpoint *datadog_otel_metrics_endpoint_from_agent_url(ddog_CharSlice url); +struct ddog_Endpoint *datadog_otel_traces_endpoint_from_url(ddog_CharSlice url); + +struct ddog_Endpoint *datadog_otel_traces_endpoint_from_agent_url(ddog_CharSlice url); + +/** + * Registers the OTLP traces export configuration for a sidecar session. + * + * `endpoint` is the full OTLP traces URL (already resolved by the extension, + * including the computed default and the `OTEL_EXPORTER_OTLP_ENDPOINT` -> + * `/v1/traces` fallback). `headers` is the raw `key=value,...` string. + * + * # Safety + * All CharSlice arguments must point to valid, correctly-sized data. + */ +ddog_MaybeError ddog_sidecar_session_set_otlp_traces_endpoint(ddog_CharSlice session_id, + ddog_CharSlice endpoint, + ddog_CharSlice headers, + uint64_t timeout_ms); + +/** + * Clears the OTLP traces export configuration for a session (e.g. when OTLP + * trace export is disabled or the session is torn down). + * + * # Safety + * `session_id` must point to valid, correctly-sized data. + */ +void ddog_sidecar_session_clear_otlp_traces_endpoint(ddog_CharSlice session_id); + void datadog_endpoint_as_crashtracker_config(const struct ddog_Endpoint *endpoint, void (*callback)(ddog_crasht_EndpointConfig, void*), void *userdata); diff --git a/components-rs/lib.rs b/components-rs/lib.rs index f73beec72d2..dfe756fedf0 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -196,6 +196,57 @@ pub unsafe extern "C" fn datadog_otel_metrics_endpoint_from_agent_url(url: CharS } } +#[cfg(unix)] +fn otel_traces_endpoint_from_unix_socket(_socket_path: &str) -> std::option::Option> { + socket_path_to_uri(Path::new(_socket_path)).ok().and_then(|uri| { + let mut parts = uri.into_parts(); + parts.path_and_query = Some(PathAndQuery::from_static("/v1/traces")); + Uri::from_parts(parts) + .ok() + .map(|url| Box::new(Endpoint::from_url(url))) + }) +} + +/// Builds an OTLP traces endpoint from an explicit, full endpoint URL, used +/// as-is (mirrors `datadog_otel_metrics_endpoint_from_url`). +#[no_mangle] +pub unsafe extern "C" fn datadog_otel_traces_endpoint_from_url(url: CharSlice) -> std::option::Option> { + let url_str = url.to_utf8_lossy(); + #[cfg(unix)] + if let Some(socket_path) = url_str.strip_prefix("unix://") { + let socket_path = socket_path.strip_suffix("/v1/traces").unwrap_or(socket_path); + return otel_traces_endpoint_from_unix_socket(socket_path); + } + parse_uri(url_str.as_ref()) + .ok() + .map(|url| Box::new(Endpoint::from_url(url))) +} + +/// Builds an OTLP traces endpoint from the agent URL by reusing the agent +/// host and forcing the standard OTLP http port and `/v1/traces` path +/// (mirrors `datadog_otel_metrics_endpoint_from_agent_url`). +#[no_mangle] +pub unsafe extern "C" fn datadog_otel_traces_endpoint_from_agent_url(url: CharSlice) -> std::option::Option> { + let url_str = url.to_utf8_lossy(); + #[cfg(unix)] + if let Some(socket_path) = url_str.strip_prefix("unix://") { + return otel_traces_endpoint_from_unix_socket(socket_path); + } + if url_str.starts_with("http") { + let parsed = parse_uri(url_str.as_ref()).ok(); + let scheme = parsed.as_ref().and_then(|u| u.scheme_str()).unwrap_or("http"); + let host = parsed + .as_ref() + .and_then(|u| u.host()) + .unwrap_or("localhost"); + parse_uri(&format!("{}://{}:4318/v1/traces", scheme, host)) + .ok() + .map(|url| Box::new(Endpoint::from_url(url))) + } else { + datadog_parse_agent_url(url) + } +} + #[no_mangle] #[cfg(unix)] pub unsafe extern "C" fn datadog_endpoint_as_crashtracker_config( @@ -318,3 +369,39 @@ pub extern "C" fn ddog_free_normalized_tag_value(ptr: *const c_char) { drop(std::ffi::CString::from_raw(ptr as *mut c_char)); } } + +#[cfg(test)] +mod otel_traces_endpoint_tests { + use super::*; + use libdd_common_ffi::CharSlice; + + #[test] + fn traces_endpoint_from_explicit_url_used_as_is() { + let ep = unsafe { + datadog_otel_traces_endpoint_from_url(CharSlice::from("http://collector:4318/v1/traces")) + } + .expect("endpoint should parse"); + assert_eq!(ep.url.to_string(), "http://collector:4318/v1/traces"); + } + + #[test] + fn traces_endpoint_from_agent_url_uses_4318_and_v1_traces() { + let ep = unsafe { + datadog_otel_traces_endpoint_from_agent_url(CharSlice::from("http://agent-host:8126")) + } + .expect("endpoint should be derived from agent url"); + // Port forced to 4318 and /v1/traces path appended; host preserved. + assert_eq!(ep.url.to_string(), "http://agent-host:4318/v1/traces"); + } + + #[test] + fn traces_endpoint_from_agent_url_defaults_host_when_missing() { + let ep = unsafe { + datadog_otel_traces_endpoint_from_agent_url(CharSlice::from("http://")) + }; + // Falls back to localhost when the agent URL has no host. + if let Some(ep) = ep { + assert_eq!(ep.url.to_string(), "http://localhost:4318/v1/traces"); + } + } +} diff --git a/components-rs/sidecar.rs b/components-rs/sidecar.rs index bd02688c1bb..a8de9ce96f3 100644 --- a/components-rs/sidecar.rs +++ b/components-rs/sidecar.rs @@ -224,3 +224,154 @@ pub extern "C" fn ddog_exception_hash_limiter_inc(connection: &mut SidecarTransp let _ = acquire_exception_hash_rate_limiter(connection, hash, Duration::from_secs(granularity_seconds as u64)); true } + +/// OTLP trace export configuration resolved by the PHP extension and forwarded +/// here so the sidecar trace path can route traces through libdatadog's OTLP +/// `TraceExporter` (`set_otlp_endpoint` / `set_otlp_headers`, `send_otlp_traces_http`). +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct OtlpTracesConfig { + /// Full OTLP traces intake endpoint (e.g. `http://host:4318/v1/traces`). + pub endpoint: String, + /// Headers parsed from `OTEL_EXPORTER_OTLP_TRACES_HEADERS` (key/value pairs). + pub headers: Vec<(String, String)>, + /// Request timeout in milliseconds (`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT`). + pub timeout_ms: u64, +} + +lazy_static! { + /// Process-local registry of OTLP traces config keyed by sidecar session id. + /// Populated from the PHP extension at session setup, mirroring how the OTLP + /// metrics endpoint is attached to the session config. Consumed when building + /// the sidecar's trace `TraceExporter`. + static ref OTLP_TRACES_CONFIG: Mutex> = + Mutex::new(std::collections::HashMap::new()); +} + +/// Parses a `key1=value1,key2=value2` header string (the OTLP headers format) +/// into a vector of (key, value) pairs. Empty / malformed entries are skipped. +pub fn parse_otlp_headers(raw: &str) -> Vec<(String, String)> { + raw.split(',') + .filter_map(|pair| { + let pair = pair.trim(); + if pair.is_empty() { + return None; + } + let (k, v) = pair.split_once('=')?; + let k = k.trim(); + if k.is_empty() { + return None; + } + Some((k.to_string(), v.trim().to_string())) + }) + .collect() +} + +/// Registers the OTLP traces export configuration for a sidecar session. +/// +/// `endpoint` is the full OTLP traces URL (already resolved by the extension, +/// including the computed default and the `OTEL_EXPORTER_OTLP_ENDPOINT` -> +/// `/v1/traces` fallback). `headers` is the raw `key=value,...` string. +/// +/// # Safety +/// All CharSlice arguments must point to valid, correctly-sized data. +#[no_mangle] +pub unsafe extern "C" fn ddog_sidecar_session_set_otlp_traces_endpoint( + session_id: CharSlice, + endpoint: CharSlice, + headers: CharSlice, + timeout_ms: u64, +) -> MaybeError { + let session_id: String = session_id.to_utf8_lossy().into_owned(); + let endpoint: String = endpoint.to_utf8_lossy().into_owned(); + let headers = parse_otlp_headers(&headers.to_utf8_lossy()); + + let config = OtlpTracesConfig { + endpoint, + headers, + timeout_ms, + }; + + if let Ok(mut map) = OTLP_TRACES_CONFIG.lock() { + map.insert(session_id, config); + } + + MaybeError::None +} + +/// Clears the OTLP traces export configuration for a session (e.g. when OTLP +/// trace export is disabled or the session is torn down). +/// +/// # Safety +/// `session_id` must point to valid, correctly-sized data. +#[no_mangle] +pub unsafe extern "C" fn ddog_sidecar_session_clear_otlp_traces_endpoint(session_id: CharSlice) { + let session_id: String = session_id.to_utf8_lossy().into_owned(); + if let Ok(mut map) = OTLP_TRACES_CONFIG.lock() { + map.remove(&session_id); + } +} + +/// Returns the OTLP traces config registered for a session, if any. Used by the +/// trace export path (and tests) to build the OTLP `TraceExporter`. +pub fn get_otlp_traces_config(session_id: &str) -> Option { + OTLP_TRACES_CONFIG + .lock() + .ok() + .and_then(|map| map.get(session_id).cloned()) +} + +#[cfg(test)] +mod otlp_traces_tests { + use super::*; + + #[test] + fn parse_headers_basic() { + let parsed = parse_otlp_headers("api-key=abc123,team=apm"); + assert_eq!( + parsed, + vec![ + ("api-key".to_string(), "abc123".to_string()), + ("team".to_string(), "apm".to_string()), + ] + ); + } + + #[test] + fn parse_headers_trims_and_skips_malformed() { + let parsed = parse_otlp_headers(" k1 = v1 , , bad , k2=v2 "); + assert_eq!( + parsed, + vec![ + ("k1".to_string(), "v1".to_string()), + ("k2".to_string(), "v2".to_string()), + ] + ); + } + + #[test] + fn parse_headers_empty() { + assert!(parse_otlp_headers("").is_empty()); + } + + #[test] + fn register_and_fetch_roundtrip() { + let session = "session-roundtrip-test"; + unsafe { + let _ = ddog_sidecar_session_set_otlp_traces_endpoint( + CharSlice::from(session), + CharSlice::from("http://localhost:4318/v1/traces"), + CharSlice::from("api-key=secret"), + 5000, + ); + } + let cfg = get_otlp_traces_config(session).expect("config should be registered"); + assert_eq!(cfg.endpoint, "http://localhost:4318/v1/traces"); + assert_eq!(cfg.timeout_ms, 5000); + assert_eq!(cfg.headers, vec![("api-key".to_string(), "secret".to_string())]); + + unsafe { + ddog_sidecar_session_clear_otlp_traces_endpoint(CharSlice::from(session)); + } + assert!(get_otlp_traces_config(session).is_none()); + } +} diff --git a/ext/configuration.h b/ext/configuration.h index d6402649dcb..b3e1dd07f70 100644 --- a/ext/configuration.h +++ b/ext/configuration.h @@ -90,6 +90,21 @@ enum datadog_sidecar_connection_mode { CONFIG(STRING, OTEL_EXPORTER_OTLP_METRICS_ENDPOINT, "", \ .ini_change = zai_config_system_ini_change, \ .env_config_fallback = ddtrace_conf_otel_otlp_endpoint) \ + CONFIG(BOOL, DD_TRACE_OTLP_ENABLED, "false", \ + .ini_change = zai_config_system_ini_change, \ + .env_config_fallback = ddtrace_conf_otel_traces_otlp_enabled) \ + CONFIG(STRING, OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, "", \ + .ini_change = zai_config_system_ini_change, \ + .env_config_fallback = ddtrace_conf_otel_traces_otlp_endpoint) \ + CONFIG(STRING, OTEL_EXPORTER_OTLP_TRACES_HEADERS, "", \ + .ini_change = zai_config_system_ini_change, \ + .env_config_fallback = ddtrace_conf_otel_traces_otlp_headers) \ + CONFIG(INT, OTEL_EXPORTER_OTLP_TRACES_TIMEOUT, "10000", \ + .ini_change = zai_config_system_ini_change, \ + .env_config_fallback = ddtrace_conf_otel_traces_otlp_timeout) \ + CONFIG(STRING, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, "http/json", \ + .ini_change = zai_config_system_ini_change, \ + .env_config_fallback = ddtrace_conf_otel_traces_otlp_protocol) \ CONFIG(INT, DD_TRACE_BUFFER_SIZE, "2097152", .ini_change = zai_config_system_ini_change) \ CONFIG(INT, DD_TRACE_AGENT_MAX_PAYLOAD_SIZE, "52428800", .ini_change = zai_config_system_ini_change) \ CONFIG(INT, DD_TRACE_AGENT_STACK_BACKLOG, "12", .ini_change = zai_config_system_ini_change) \ diff --git a/ext/endpoints.c b/ext/endpoints.c index f46e8e9e6e5..661693bec68 100644 --- a/ext/endpoints.c +++ b/ext/endpoints.c @@ -122,3 +122,72 @@ ddog_Endpoint *datadog_otel_metrics_endpoint(void) { free(agent_url); return metrics_endpoint; } + +// Builds the OTLP traces endpoint, mirroring datadog_otel_metrics_endpoint(): +// the explicit OTEL_EXPORTER_OTLP_TRACES_ENDPOINT (or its +// OTEL_EXPORTER_OTLP_ENDPOINT -> /v1/traces fallback resolved by the config +// layer) is used as-is; otherwise the computed default +// http://:4318/v1/traces is derived from the agent URL. +// The Rust builders (datadog_otel_traces_endpoint_from_url / +// _from_agent_url) own the URL handling, exactly like the metrics path. +ddog_Endpoint *datadog_otel_traces_endpoint(void) { + zend_string *endpoint_url = get_global_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT(); + if (ZSTR_LEN(endpoint_url) > 0) { + return datadog_otel_traces_endpoint_from_url(dd_zend_string_to_CharSlice(endpoint_url)); + } + + char *agent_url = datadog_agent_url(); + ddog_Endpoint *traces_endpoint = datadog_otel_traces_endpoint_from_agent_url((ddog_CharSlice){.ptr = agent_url, .len = strlen(agent_url)}); + free(agent_url); + return traces_endpoint; +} + +// Resolves the OTLP traces endpoint URL as a heap-allocated, null-terminated +// string (caller must free()). Returns the explicit +// OTEL_EXPORTER_OTLP_TRACES_ENDPOINT verbatim, or the computed default +// http://:4318/v1/traces derived from the agent URL. This is the +// string form passed to the sidecar (see ddog_sidecar_session_set_otlp_traces_endpoint). +char *datadog_otel_traces_url(void) { + zend_string *endpoint_url = get_global_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT(); + if (ZSTR_LEN(endpoint_url) > 0) { + return zend_strndup(ZSTR_VAL(endpoint_url), ZSTR_LEN(endpoint_url)); + } + + char *agent_url = datadog_agent_url(); + bool isIPv6 = false; + const char *host = "localhost"; + char *host_buf = NULL; + + // Extract host from the agent URL (scheme://host:port[/...]). For UDS agent + // URLs (unix://...), fall back to localhost for the OTLP http endpoint. + const char *scheme_sep = strstr(agent_url, "://"); + if (scheme_sep && strncmp(agent_url, "unix", 4) != 0) { + const char *host_start = scheme_sep + 3; + const char *host_end; + if (*host_start == '[') { + // IPv6 literal: http://[::1]:8126 + isIPv6 = true; + host_start++; + host_end = strchr(host_start, ']'); + } else { + host_end = host_start; + while (*host_end && *host_end != ':' && *host_end != '/') { + host_end++; + } + } + if (host_end && host_end > host_start) { + host_buf = zend_strndup(host_start, host_end - host_start); + host = host_buf; + } + } + + char *url; + asprintf(&url, isIPv6 ? "http://[%s]:4318/v1/traces" : "http://%s:4318/v1/traces", host); + if (host_buf) { + free(host_buf); + } + free(agent_url); + + // Normalize to a malloc-allocated buffer freeable with free() (asprintf already is). + return url; +} diff --git a/ext/endpoints.h b/ext/endpoints.h index 09dea97c3b5..f28835941b7 100644 --- a/ext/endpoints.h +++ b/ext/endpoints.h @@ -6,5 +6,7 @@ char *datadog_agent_url(void); char *datadog_dogstatsd_url(void); ddog_Endpoint *datadog_otel_metrics_endpoint(void); +ddog_Endpoint *datadog_otel_traces_endpoint(void); +char *datadog_otel_traces_url(void); #endif // DATADOG_ENDPOINTS_H diff --git a/ext/otel_config.c b/ext/otel_config.c index a747652d885..d26c3cc06fb 100644 --- a/ext/otel_config.c +++ b/ext/otel_config.c @@ -139,7 +139,10 @@ bool ddtrace_conf_otel_resource_attributes_tags(zai_env_buffer *buf, bool pre_ri return true; } -bool ddtrace_conf_otel_otlp_endpoint(zai_env_buffer *buf, bool pre_rinit) { +// Reads OTEL_EXPORTER_OTLP_ENDPOINT, strips trailing slashes, and appends the +// signal-specific suffix (e.g. "/v1/metrics" or "/v1/traces"). Used as the +// fallback for the per-signal OTLP endpoint configs. +static bool ddtrace_conf_otel_otlp_endpoint_with_suffix(zai_env_buffer *buf, bool pre_rinit, const char *suffix, size_t suffix_len) { ZAI_ENV_BUFFER_INIT(local, ZAI_ENV_MAX_BUFSIZ); if (!datadog_get_otel_value((zai_str)ZAI_STRL("OTEL_EXPORTER_OTLP_ENDPOINT"), &local, pre_rinit) || !local.ptr[0]) { return false; @@ -150,8 +153,6 @@ bool ddtrace_conf_otel_otlp_endpoint(zai_env_buffer *buf, bool pre_rinit) { base_len--; } - const char suffix[] = "/v1/metrics"; - size_t suffix_len = sizeof(suffix) - 1; if (base_len + suffix_len + 1 > ZAI_ENV_MAX_BUFSIZ) { return false; } @@ -160,3 +161,60 @@ bool ddtrace_conf_otel_otlp_endpoint(zai_env_buffer *buf, bool pre_rinit) { memcpy(buf->ptr + base_len, suffix, suffix_len + 1); return true; } + +bool ddtrace_conf_otel_otlp_endpoint(zai_env_buffer *buf, bool pre_rinit) { + return ddtrace_conf_otel_otlp_endpoint_with_suffix(buf, pre_rinit, ZEND_STRL("/v1/metrics")); +} + +bool ddtrace_conf_otel_traces_otlp_endpoint(zai_env_buffer *buf, bool pre_rinit) { + return ddtrace_conf_otel_otlp_endpoint_with_suffix(buf, pre_rinit, ZEND_STRL("/v1/traces")); +} + +bool ddtrace_conf_otel_traces_otlp_enabled(zai_env_buffer *buf, bool pre_rinit) { + if (!datadog_get_otel_value((zai_str)ZAI_STRL("OTEL_TRACES_EXPORTER"), buf, pre_rinit)) { + return false; + } + if (strcmp(buf->ptr, "otlp") == 0) { + // Gate: pinning the Datadog agent trace protocol via + // DD_TRACE_AGENT_PROTOCOL_VERSION is incompatible with routing traces + // over OTLP. When it is set, OTLP trace export is disabled and a notice + // is logged (the agent trace protocol version takes precedence). + zai_option_str protocol_version = zai_sys_getenv((zai_str)ZAI_STRL("DD_TRACE_AGENT_PROTOCOL_VERSION")); + if (zai_option_str_is_some(protocol_version) && protocol_version.len > 0) { + LOG_ONCE(WARN, "OTLP trace export requested via OTEL_TRACES_EXPORTER=otlp, but " + "DD_TRACE_AGENT_PROTOCOL_VERSION is set; OTLP trace export disabled " + "(the agent trace protocol version takes precedence)"); + buf->ptr = "0"; buf->len = 1; + return true; + } + buf->ptr = "1"; buf->len = 1; + return true; + } + // Any other value (including "none") leaves OTLP trace export disabled. + buf->ptr = "0"; buf->len = 1; + return true; +} + +bool ddtrace_conf_otel_traces_otlp_headers(zai_env_buffer *buf, bool pre_rinit) { + // OTEL_EXPORTER_OTLP_TRACES_HEADERS falls back to OTEL_EXPORTER_OTLP_HEADERS. + // The value is a comma-separated list of key=value pairs, passed through as-is. + return datadog_get_otel_value((zai_str)ZAI_STRL("OTEL_EXPORTER_OTLP_HEADERS"), buf, pre_rinit); +} + +bool ddtrace_conf_otel_traces_otlp_timeout(zai_env_buffer *buf, bool pre_rinit) { + // OTEL_EXPORTER_OTLP_TRACES_TIMEOUT falls back to OTEL_EXPORTER_OTLP_TIMEOUT (milliseconds). + return datadog_get_otel_value((zai_str)ZAI_STRL("OTEL_EXPORTER_OTLP_TIMEOUT"), buf, pre_rinit); +} + +bool ddtrace_conf_otel_traces_otlp_protocol(zai_env_buffer *buf, bool pre_rinit) { + // OTEL_EXPORTER_OTLP_TRACES_PROTOCOL falls back to OTEL_EXPORTER_OTLP_PROTOCOL. + if (!datadog_get_otel_value((zai_str)ZAI_STRL("OTEL_EXPORTER_OTLP_PROTOCOL"), buf, pre_rinit)) { + return false; + } + // Only http/json is honored for OTLP trace export today. Report any other + // value but keep it visible so config telemetry reflects the user's setting. + if (strcmp(buf->ptr, "http/json") != 0) { + LOG_ONCE(WARN, "OTEL_EXPORTER_OTLP_TRACES_PROTOCOL '%s' is not supported for OTLP trace export; only http/json is honored", buf->ptr); + } + return true; +} diff --git a/ext/otel_config.h b/ext/otel_config.h index 04aa1ce157d..94597805f84 100644 --- a/ext/otel_config.h +++ b/ext/otel_config.h @@ -11,6 +11,11 @@ bool ddtrace_conf_otel_resource_attributes_version(zai_env_buffer *buf, bool pre bool ddtrace_conf_otel_service_name(zai_env_buffer *buf, bool pre_rinit); bool ddtrace_conf_otel_log_level(zai_env_buffer *buf, bool pre_rinit); bool ddtrace_conf_otel_otlp_endpoint(zai_env_buffer *buf, bool pre_rinit); +bool ddtrace_conf_otel_traces_otlp_endpoint(zai_env_buffer *buf, bool pre_rinit); +bool ddtrace_conf_otel_traces_otlp_enabled(zai_env_buffer *buf, bool pre_rinit); +bool ddtrace_conf_otel_traces_otlp_headers(zai_env_buffer *buf, bool pre_rinit); +bool ddtrace_conf_otel_traces_otlp_timeout(zai_env_buffer *buf, bool pre_rinit); +bool ddtrace_conf_otel_traces_otlp_protocol(zai_env_buffer *buf, bool pre_rinit); bool ddtrace_conf_otel_resource_attributes_tags(zai_env_buffer *buf, bool pre_rinit); #endif // DD_OTEL_CONFIG_H diff --git a/ext/sidecar.c b/ext/sidecar.c index 74a1e588844..19223ca9ea2 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -99,6 +99,16 @@ DATADOG_PUBLIC uint64_t datadog_get_sidecar_queue_id(void) { return DATADOG_G(sidecar_queue_id); } +// Returns true when OTLP trace export is enabled for this process. +// +// The DD_TRACE_OTLP_ENABLED config already folds in both the +// OTEL_TRACES_EXPORTER=otlp selection and the DD_TRACE_AGENT_PROTOCOL_VERSION +// disable gate (see ddtrace_conf_otel_traces_otlp_enabled), so reading it here +// reflects the final enablement decision. +bool datadog_otlp_traces_enabled(void) { + return get_global_DD_TRACE_OTLP_ENABLED(); +} + static void dd_sidecar_post_connect(ddog_SidecarTransport **transport, bool is_fork, const char *logpath) { ddog_CharSlice session_id = (ddog_CharSlice) {.ptr = (char *) datadog_formatted_session_id, .len = sizeof(datadog_formatted_session_id)}; ddog_CharSlice root_session_id = datadog_is_empty_session_id(datadog_formatted_root_session_id) ? DDOG_CHARSLICE_C("") : (ddog_CharSlice) {.ptr = (char *) datadog_formatted_root_session_id, .len = sizeof(datadog_formatted_root_session_id)}; @@ -136,6 +146,24 @@ static void dd_sidecar_post_connect(ddog_SidecarTransport **transport, bool is_f ddog_endpoint_drop(otlp_metrics_endpoint); } + // Plumb the OTLP traces endpoint to the sidecar, mirroring the OTLP metrics + // endpoint above. The endpoint URL is built from OTEL_EXPORTER_OTLP_TRACES_ENDPOINT + // (or the OTEL_EXPORTER_OTLP_ENDPOINT -> /v1/traces fallback / computed + // default), gated on OTEL_TRACES_EXPORTER=otlp and the + // DD_TRACE_AGENT_PROTOCOL_VERSION disable check. + if (datadog_otlp_traces_enabled()) { + char *traces_url = datadog_otel_traces_url(); + datadog_ffi_try("Failed setting OTLP traces endpoint on sidecar session", + ddog_sidecar_session_set_otlp_traces_endpoint( + session_id, + (ddog_CharSlice){.ptr = traces_url, .len = strlen(traces_url)}, + dd_zend_string_to_CharSlice(get_global_OTEL_EXPORTER_OTLP_TRACES_HEADERS()), + (uint64_t)get_global_OTEL_EXPORTER_OTLP_TRACES_TIMEOUT())); + free(traces_url); + } else { + ddog_sidecar_session_clear_otlp_traces_endpoint(session_id); + } + if (get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED()) { datadog_telemetry_register_services(transport); } diff --git a/ext/sidecar.h b/ext/sidecar.h index ae0a79c5c5b..ae981a69e59 100644 --- a/ext/sidecar.h +++ b/ext/sidecar.h @@ -53,6 +53,9 @@ void datadog_force_new_instance_id(void); void datadog_sidecar_push_tag(ddog_Vec_Tag *vec, ddog_CharSlice key, ddog_CharSlice value); void datadog_sidecar_push_tags(ddog_Vec_Tag *vec, zval *tags); ddog_Endpoint *datadog_sidecar_agent_endpoint(void); +// Returns true when OTLP trace export is enabled (OTEL_TRACES_EXPORTER=otlp and +// DD_TRACE_AGENT_PROTOCOL_VERSION is not set). +bool datadog_otlp_traces_enabled(void); void ddtrace_sidecar_submit_span_data_direct_defaults(ddog_SidecarTransport **transport, ddtrace_span_data *root); void ddtrace_sidecar_submit_span_data_direct(ddog_SidecarTransport **transport, ddtrace_span_data *root, zend_string *cfg_service, zend_string *cfg_env, zend_string *cfg_version); diff --git a/tests/ext/otlp_traces_endpoint_fallback.phpt b/tests/ext/otlp_traces_endpoint_fallback.phpt new file mode 100644 index 00000000000..37b56ad737c --- /dev/null +++ b/tests/ext/otlp_traces_endpoint_fallback.phpt @@ -0,0 +1,17 @@ +--TEST-- +OTEL_EXPORTER_OTLP_TRACES_ENDPOINT falls back to OTEL_EXPORTER_OTLP_ENDPOINT + /v1/traces +--SKIPIF-- + +--ENV-- +OTEL_TRACES_EXPORTER=otlp +OTEL_EXPORTER_OTLP_ENDPOINT=http://collector.example:4318/ +--FILE-- + +--EXPECT-- +string(39) "http://collector.example:4318/v1/traces" diff --git a/tests/ext/otlp_traces_explicit_config.phpt b/tests/ext/otlp_traces_explicit_config.phpt new file mode 100644 index 00000000000..cabd140bd93 --- /dev/null +++ b/tests/ext/otlp_traces_explicit_config.phpt @@ -0,0 +1,28 @@ +--TEST-- +OTLP traces endpoint/headers/timeout/protocol use explicit values and fallbacks +--SKIPIF-- + +--ENV-- +OTEL_TRACES_EXPORTER=otlp +OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://traces.example:4318/v1/traces +OTEL_EXPORTER_OTLP_HEADERS=api-key=secret,team=apm +OTEL_EXPORTER_OTLP_TIMEOUT=2500 +OTEL_EXPORTER_OTLP_PROTOCOL=http/json +--FILE-- + +--EXPECT-- +string(36) "http://traces.example:4318/v1/traces" +string(23) "api-key=secret,team=apm" +string(4) "2500" +string(9) "http/json" diff --git a/tests/ext/otlp_traces_exporter_enabled.phpt b/tests/ext/otlp_traces_exporter_enabled.phpt new file mode 100644 index 00000000000..b39f2e6955d --- /dev/null +++ b/tests/ext/otlp_traces_exporter_enabled.phpt @@ -0,0 +1,18 @@ +--TEST-- +OTEL_TRACES_EXPORTER=otlp enables OTLP trace export without disabling tracing +--SKIPIF-- + +--ENV-- +OTEL_TRACES_EXPORTER=otlp +--FILE-- + +--EXPECT-- +string(1) "1" +string(1) "1" diff --git a/tests/ext/otlp_traces_exporter_none.phpt b/tests/ext/otlp_traces_exporter_none.phpt new file mode 100644 index 00000000000..83475a1b781 --- /dev/null +++ b/tests/ext/otlp_traces_exporter_none.phpt @@ -0,0 +1,18 @@ +--TEST-- +OTEL_TRACES_EXPORTER=none disables tracing and leaves OTLP trace export off +--SKIPIF-- + +--ENV-- +OTEL_TRACES_EXPORTER=none +--FILE-- + +--EXPECT-- +string(1) "0" +string(1) "0" diff --git a/tests/ext/otlp_traces_protocol_version_gate.phpt b/tests/ext/otlp_traces_protocol_version_gate.phpt new file mode 100644 index 00000000000..bb1a9c81c06 --- /dev/null +++ b/tests/ext/otlp_traces_protocol_version_gate.phpt @@ -0,0 +1,19 @@ +--TEST-- +DD_TRACE_AGENT_PROTOCOL_VERSION disables OTLP trace export even with OTEL_TRACES_EXPORTER=otlp +--SKIPIF-- + +--ENV-- +OTEL_TRACES_EXPORTER=otlp +DD_TRACE_AGENT_PROTOCOL_VERSION=0.4 +--FILE-- + +--EXPECT-- +string(1) "0" +string(1) "1" diff --git a/tracer/tracer_otel_config.c b/tracer/tracer_otel_config.c index 9781b687084..52bae0cb403 100644 --- a/tracer/tracer_otel_config.c +++ b/tracer/tracer_otel_config.c @@ -29,6 +29,18 @@ bool ddtrace_conf_otel_sample_rate(zai_env_buffer *buf, bool pre_rinit) { return false; } + // Datadog sampling is inherently parent-based (it respects the upstream + // sampling decision). A non-parentbased OTEL_TRACES_SAMPLER is therefore + // mapped to its parentbased equivalent; warn so the user knows the + // root-only semantics are not honored exactly. + bool non_parentbased = strcmp(buf->ptr, "always_on") == 0 + || strcmp(buf->ptr, "always_off") == 0 + || strcmp(buf->ptr, "traceidratio") == 0; + if (non_parentbased) { + LOG_ONCE(WARN, "OTEL_TRACES_SAMPLER '%s' is non-parentbased; Datadog sampling is parent-based, " + "mapping to the parentbased equivalent", buf->ptr); + } + if (strcmp(buf->ptr, "always_on") == 0 || strcmp(buf->ptr, "parentbased_always_on") == 0) { buf->ptr = "1"; buf->len = 1; return true; @@ -55,6 +67,12 @@ bool ddtrace_conf_otel_traces_exporter(zai_env_buffer *buf, bool pre_rinit) { buf->ptr = "0"; buf->len = 1; return true; } + // "otlp" enables OTLP trace export; tracing itself stays enabled, so do + // not alter DD_TRACE_ENABLED here. The OTLP gate is reported through + // DD_TRACE_OTLP_ENABLED via ddtrace_conf_otel_traces_otlp_enabled. + if (strcmp(buf->ptr, "otlp") == 0) { + return false; + } LOG_ONCE(WARN, "OTEL_TRACES_EXPORTER has invalid value: %s", buf->ptr); datadog_report_otel_cfg_telemetry_invalid("otel_traces_exporter", "dd_trace_enabled", pre_rinit); } From 115be65c98d8f43526d110f78edb73c5be373ecc Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Tue, 9 Jun 2026 20:05:57 -0400 Subject: [PATCH 2/4] feat(otlp): route OTLP traces through the sidecar TraceExporter Connect the extension's resolved OTLP traces config to libdatadog's new sidecar OTLP path, replacing the dead-end process-local OTLP_TRACES_CONFIG registry that the prior groundwork wired but never consumed. - ext/sidecar.c: in dd_sidecar_post_connect, build the OTLP traces ddog_Endpoint (datadog_otel_traces_endpoint) when OTLP trace export is enabled and pass it, with headers/timeout, into the new libdatadog FFI ddog_sidecar_session_set_otlp_traces_endpoint(transport, ...). A NULL endpoint clears the config and keeps the default agent path. Mirrors the OTLP metrics endpoint plumbing. - components-rs/sidecar.rs: remove the dead-end OtlpTracesConfig registry and the old URL-based ddog_sidecar_session_set/clear_otlp_traces_endpoint stubs; the real symbol now comes from datadog-sidecar-ffi. - ext/endpoints.c/.h: drop the now-unused datadog_otel_traces_url helper; the endpoint builder (datadog_otel_traces_endpoint) is what the sidecar FFI needs. - Bump libdatadog submodule to the matching OTLP TraceExporter commit. - Regenerate components-rs headers (cbindgen 0.29.0 + dedup): sidecar.h gains the new FFI declaration; datadog.h drops the removed stubs. Co-Authored-By: Claude Opus 4.8 (1M context) --- components-rs/datadog.h | 29 ++------ components-rs/sidecar.h | 24 +++++++ components-rs/sidecar.rs | 151 --------------------------------------- ext/endpoints.c | 50 ------------- ext/endpoints.h | 1 - ext/sidecar.c | 29 ++++---- libdatadog | 2 +- 7 files changed, 48 insertions(+), 238 deletions(-) diff --git a/components-rs/datadog.h b/components-rs/datadog.h index 09e487a61d4..c771311102b 100644 --- a/components-rs/datadog.h +++ b/components-rs/datadog.h @@ -55,33 +55,18 @@ struct ddog_Endpoint *datadog_otel_metrics_endpoint_from_url(ddog_CharSlice url) struct ddog_Endpoint *datadog_otel_metrics_endpoint_from_agent_url(ddog_CharSlice url); -struct ddog_Endpoint *datadog_otel_traces_endpoint_from_url(ddog_CharSlice url); - -struct ddog_Endpoint *datadog_otel_traces_endpoint_from_agent_url(ddog_CharSlice url); - /** - * Registers the OTLP traces export configuration for a sidecar session. - * - * `endpoint` is the full OTLP traces URL (already resolved by the extension, - * including the computed default and the `OTEL_EXPORTER_OTLP_ENDPOINT` -> - * `/v1/traces` fallback). `headers` is the raw `key=value,...` string. - * - * # Safety - * All CharSlice arguments must point to valid, correctly-sized data. + * Builds an OTLP traces endpoint from an explicit, full endpoint URL, used + * as-is (mirrors `datadog_otel_metrics_endpoint_from_url`). */ -ddog_MaybeError ddog_sidecar_session_set_otlp_traces_endpoint(ddog_CharSlice session_id, - ddog_CharSlice endpoint, - ddog_CharSlice headers, - uint64_t timeout_ms); +struct ddog_Endpoint *datadog_otel_traces_endpoint_from_url(ddog_CharSlice url); /** - * Clears the OTLP traces export configuration for a session (e.g. when OTLP - * trace export is disabled or the session is torn down). - * - * # Safety - * `session_id` must point to valid, correctly-sized data. + * Builds an OTLP traces endpoint from the agent URL by reusing the agent + * host and forcing the standard OTLP http port and `/v1/traces` path + * (mirrors `datadog_otel_metrics_endpoint_from_agent_url`). */ -void ddog_sidecar_session_clear_otlp_traces_endpoint(ddog_CharSlice session_id); +struct ddog_Endpoint *datadog_otel_traces_endpoint_from_agent_url(ddog_CharSlice url); void datadog_endpoint_as_crashtracker_config(const struct ddog_Endpoint *endpoint, void (*callback)(ddog_crasht_EndpointConfig, void*), diff --git a/components-rs/sidecar.h b/components-rs/sidecar.h index eb0b5be0580..e761fbe0a74 100644 --- a/components-rs/sidecar.h +++ b/components-rs/sidecar.h @@ -226,6 +226,30 @@ ddog_MaybeError ddog_sidecar_session_set_config(struct ddog_SidecarTransport **t ddog_MaybeError ddog_sidecar_session_set_process_tags(struct ddog_SidecarTransport **transport, const struct ddog_Vec_Tag *process_tags); +/** + * Sets the OTLP traces export configuration for an existing session. + * + * This is additive and non-breaking: when `otlp_traces_endpoint` is non-null, + * the session's traces are exported via libdatadog's OTLP `TraceExporter` + * (HTTP/JSON) instead of the agent msgpack `/v0.4/traces` path. Passing a null + * `otlp_traces_endpoint` clears the configuration and restores the default + * agent path. Sessions that never call this function are unaffected. + * + * `headers` is the raw `key=value,...` string (e.g. the value of + * `OTEL_EXPORTER_OTLP_TRACES_HEADERS`); `timeout_ms` of `0` selects the + * default OTLP request timeout. The endpoint URL is used as-is — the host + * language is responsible for resolving the full `…/v1/traces` URL. + * + * # Safety + * `otlp_traces_endpoint`, when non-null, must point to a valid `Endpoint`. All + * `CharSlice` arguments must point to valid, correctly-sized data. + */ +ddog_MaybeError ddog_sidecar_session_set_otlp_traces_endpoint(struct ddog_SidecarTransport **transport, + ddog_CharSlice session_id, + const struct ddog_Endpoint *otlp_traces_endpoint, + ddog_CharSlice headers, + uint64_t timeout_ms); + /** * Enqueues a telemetry log action to be processed internally. * Non-blocking. Logs might be dropped if the internal queue is full. diff --git a/components-rs/sidecar.rs b/components-rs/sidecar.rs index a8de9ce96f3..bd02688c1bb 100644 --- a/components-rs/sidecar.rs +++ b/components-rs/sidecar.rs @@ -224,154 +224,3 @@ pub extern "C" fn ddog_exception_hash_limiter_inc(connection: &mut SidecarTransp let _ = acquire_exception_hash_rate_limiter(connection, hash, Duration::from_secs(granularity_seconds as u64)); true } - -/// OTLP trace export configuration resolved by the PHP extension and forwarded -/// here so the sidecar trace path can route traces through libdatadog's OTLP -/// `TraceExporter` (`set_otlp_endpoint` / `set_otlp_headers`, `send_otlp_traces_http`). -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct OtlpTracesConfig { - /// Full OTLP traces intake endpoint (e.g. `http://host:4318/v1/traces`). - pub endpoint: String, - /// Headers parsed from `OTEL_EXPORTER_OTLP_TRACES_HEADERS` (key/value pairs). - pub headers: Vec<(String, String)>, - /// Request timeout in milliseconds (`OTEL_EXPORTER_OTLP_TRACES_TIMEOUT`). - pub timeout_ms: u64, -} - -lazy_static! { - /// Process-local registry of OTLP traces config keyed by sidecar session id. - /// Populated from the PHP extension at session setup, mirroring how the OTLP - /// metrics endpoint is attached to the session config. Consumed when building - /// the sidecar's trace `TraceExporter`. - static ref OTLP_TRACES_CONFIG: Mutex> = - Mutex::new(std::collections::HashMap::new()); -} - -/// Parses a `key1=value1,key2=value2` header string (the OTLP headers format) -/// into a vector of (key, value) pairs. Empty / malformed entries are skipped. -pub fn parse_otlp_headers(raw: &str) -> Vec<(String, String)> { - raw.split(',') - .filter_map(|pair| { - let pair = pair.trim(); - if pair.is_empty() { - return None; - } - let (k, v) = pair.split_once('=')?; - let k = k.trim(); - if k.is_empty() { - return None; - } - Some((k.to_string(), v.trim().to_string())) - }) - .collect() -} - -/// Registers the OTLP traces export configuration for a sidecar session. -/// -/// `endpoint` is the full OTLP traces URL (already resolved by the extension, -/// including the computed default and the `OTEL_EXPORTER_OTLP_ENDPOINT` -> -/// `/v1/traces` fallback). `headers` is the raw `key=value,...` string. -/// -/// # Safety -/// All CharSlice arguments must point to valid, correctly-sized data. -#[no_mangle] -pub unsafe extern "C" fn ddog_sidecar_session_set_otlp_traces_endpoint( - session_id: CharSlice, - endpoint: CharSlice, - headers: CharSlice, - timeout_ms: u64, -) -> MaybeError { - let session_id: String = session_id.to_utf8_lossy().into_owned(); - let endpoint: String = endpoint.to_utf8_lossy().into_owned(); - let headers = parse_otlp_headers(&headers.to_utf8_lossy()); - - let config = OtlpTracesConfig { - endpoint, - headers, - timeout_ms, - }; - - if let Ok(mut map) = OTLP_TRACES_CONFIG.lock() { - map.insert(session_id, config); - } - - MaybeError::None -} - -/// Clears the OTLP traces export configuration for a session (e.g. when OTLP -/// trace export is disabled or the session is torn down). -/// -/// # Safety -/// `session_id` must point to valid, correctly-sized data. -#[no_mangle] -pub unsafe extern "C" fn ddog_sidecar_session_clear_otlp_traces_endpoint(session_id: CharSlice) { - let session_id: String = session_id.to_utf8_lossy().into_owned(); - if let Ok(mut map) = OTLP_TRACES_CONFIG.lock() { - map.remove(&session_id); - } -} - -/// Returns the OTLP traces config registered for a session, if any. Used by the -/// trace export path (and tests) to build the OTLP `TraceExporter`. -pub fn get_otlp_traces_config(session_id: &str) -> Option { - OTLP_TRACES_CONFIG - .lock() - .ok() - .and_then(|map| map.get(session_id).cloned()) -} - -#[cfg(test)] -mod otlp_traces_tests { - use super::*; - - #[test] - fn parse_headers_basic() { - let parsed = parse_otlp_headers("api-key=abc123,team=apm"); - assert_eq!( - parsed, - vec![ - ("api-key".to_string(), "abc123".to_string()), - ("team".to_string(), "apm".to_string()), - ] - ); - } - - #[test] - fn parse_headers_trims_and_skips_malformed() { - let parsed = parse_otlp_headers(" k1 = v1 , , bad , k2=v2 "); - assert_eq!( - parsed, - vec![ - ("k1".to_string(), "v1".to_string()), - ("k2".to_string(), "v2".to_string()), - ] - ); - } - - #[test] - fn parse_headers_empty() { - assert!(parse_otlp_headers("").is_empty()); - } - - #[test] - fn register_and_fetch_roundtrip() { - let session = "session-roundtrip-test"; - unsafe { - let _ = ddog_sidecar_session_set_otlp_traces_endpoint( - CharSlice::from(session), - CharSlice::from("http://localhost:4318/v1/traces"), - CharSlice::from("api-key=secret"), - 5000, - ); - } - let cfg = get_otlp_traces_config(session).expect("config should be registered"); - assert_eq!(cfg.endpoint, "http://localhost:4318/v1/traces"); - assert_eq!(cfg.timeout_ms, 5000); - assert_eq!(cfg.headers, vec![("api-key".to_string(), "secret".to_string())]); - - unsafe { - ddog_sidecar_session_clear_otlp_traces_endpoint(CharSlice::from(session)); - } - assert!(get_otlp_traces_config(session).is_none()); - } -} diff --git a/ext/endpoints.c b/ext/endpoints.c index 661693bec68..6c08f7a37ef 100644 --- a/ext/endpoints.c +++ b/ext/endpoints.c @@ -141,53 +141,3 @@ ddog_Endpoint *datadog_otel_traces_endpoint(void) { free(agent_url); return traces_endpoint; } - -// Resolves the OTLP traces endpoint URL as a heap-allocated, null-terminated -// string (caller must free()). Returns the explicit -// OTEL_EXPORTER_OTLP_TRACES_ENDPOINT verbatim, or the computed default -// http://:4318/v1/traces derived from the agent URL. This is the -// string form passed to the sidecar (see ddog_sidecar_session_set_otlp_traces_endpoint). -char *datadog_otel_traces_url(void) { - zend_string *endpoint_url = get_global_OTEL_EXPORTER_OTLP_TRACES_ENDPOINT(); - if (ZSTR_LEN(endpoint_url) > 0) { - return zend_strndup(ZSTR_VAL(endpoint_url), ZSTR_LEN(endpoint_url)); - } - - char *agent_url = datadog_agent_url(); - bool isIPv6 = false; - const char *host = "localhost"; - char *host_buf = NULL; - - // Extract host from the agent URL (scheme://host:port[/...]). For UDS agent - // URLs (unix://...), fall back to localhost for the OTLP http endpoint. - const char *scheme_sep = strstr(agent_url, "://"); - if (scheme_sep && strncmp(agent_url, "unix", 4) != 0) { - const char *host_start = scheme_sep + 3; - const char *host_end; - if (*host_start == '[') { - // IPv6 literal: http://[::1]:8126 - isIPv6 = true; - host_start++; - host_end = strchr(host_start, ']'); - } else { - host_end = host_start; - while (*host_end && *host_end != ':' && *host_end != '/') { - host_end++; - } - } - if (host_end && host_end > host_start) { - host_buf = zend_strndup(host_start, host_end - host_start); - host = host_buf; - } - } - - char *url; - asprintf(&url, isIPv6 ? "http://[%s]:4318/v1/traces" : "http://%s:4318/v1/traces", host); - if (host_buf) { - free(host_buf); - } - free(agent_url); - - // Normalize to a malloc-allocated buffer freeable with free() (asprintf already is). - return url; -} diff --git a/ext/endpoints.h b/ext/endpoints.h index f28835941b7..8db5b9d1875 100644 --- a/ext/endpoints.h +++ b/ext/endpoints.h @@ -7,6 +7,5 @@ char *datadog_agent_url(void); char *datadog_dogstatsd_url(void); ddog_Endpoint *datadog_otel_metrics_endpoint(void); ddog_Endpoint *datadog_otel_traces_endpoint(void); -char *datadog_otel_traces_url(void); #endif // DATADOG_ENDPOINTS_H diff --git a/ext/sidecar.c b/ext/sidecar.c index 19223ca9ea2..00259f66fcf 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -147,21 +147,24 @@ static void dd_sidecar_post_connect(ddog_SidecarTransport **transport, bool is_f } // Plumb the OTLP traces endpoint to the sidecar, mirroring the OTLP metrics - // endpoint above. The endpoint URL is built from OTEL_EXPORTER_OTLP_TRACES_ENDPOINT + // endpoint above. The endpoint is built from OTEL_EXPORTER_OTLP_TRACES_ENDPOINT // (or the OTEL_EXPORTER_OTLP_ENDPOINT -> /v1/traces fallback / computed // default), gated on OTEL_TRACES_EXPORTER=otlp and the - // DD_TRACE_AGENT_PROTOCOL_VERSION disable check. - if (datadog_otlp_traces_enabled()) { - char *traces_url = datadog_otel_traces_url(); - datadog_ffi_try("Failed setting OTLP traces endpoint on sidecar session", - ddog_sidecar_session_set_otlp_traces_endpoint( - session_id, - (ddog_CharSlice){.ptr = traces_url, .len = strlen(traces_url)}, - dd_zend_string_to_CharSlice(get_global_OTEL_EXPORTER_OTLP_TRACES_HEADERS()), - (uint64_t)get_global_OTEL_EXPORTER_OTLP_TRACES_TIMEOUT())); - free(traces_url); - } else { - ddog_sidecar_session_clear_otlp_traces_endpoint(session_id); + // DD_TRACE_AGENT_PROTOCOL_VERSION disable check. When enabled, the sidecar + // exports this session's traces via libdatadog's OTLP TraceExporter instead + // of the agent msgpack path; a NULL endpoint clears the config and restores + // the default agent path. + ddog_Endpoint *otlp_traces_endpoint = + datadog_otlp_traces_enabled() ? datadog_otel_traces_endpoint() : NULL; + datadog_ffi_try("Failed setting OTLP traces endpoint on sidecar session", + ddog_sidecar_session_set_otlp_traces_endpoint( + transport, + session_id, + otlp_traces_endpoint, + dd_zend_string_to_CharSlice(get_global_OTEL_EXPORTER_OTLP_TRACES_HEADERS()), + (uint64_t)get_global_OTEL_EXPORTER_OTLP_TRACES_TIMEOUT())); + if (otlp_traces_endpoint) { + ddog_endpoint_drop(otlp_traces_endpoint); } if (get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED()) { diff --git a/libdatadog b/libdatadog index 93e97238020..d89cc48267e 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit 93e97238020b5cf1165b2583ec555d58307af3c1 +Subproject commit d89cc48267e5e825802893039a7bc118d7be533b From d97e8b4aeb97a5d82fdbad552e8df8751f3d701a Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Tue, 9 Jun 2026 20:16:27 -0400 Subject: [PATCH 3/4] fix(otlp): bump libdatadog for sidecar OTLP env/version resource attrs Bumps the libdatadog gitlink to pick up the OTLP encoder fallback that derives `deployment.environment.name` / `service.version` resource attributes from span meta for the sidecar OTLP traces path. Co-Authored-By: Claude Opus 4.8 (1M context) --- libdatadog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libdatadog b/libdatadog index d89cc48267e..53ed2e47146 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit d89cc48267e5e825802893039a7bc118d7be533b +Subproject commit 53ed2e47146df1352dd24f9fbda8c0897baa93ae From 985fd21503016be7e5efe6ab0972b4141e3a2961 Mon Sep 17 00:00:00 2001 From: Brian Marks Date: Tue, 9 Jun 2026 21:10:44 -0400 Subject: [PATCH 4/4] Address pre-push review: surface OTLP no-op, clamp timeout, protocol notice - Warn when OTEL_TRACES_EXPORTER=otlp is set but the OTLP traces endpoint can't be resolved (was a silent fallback to the agent path). - Warn on a configured non-http/json OTLP protocol (only http/json is supported this phase), matching dd-trace-rs/rb, so it isn't a silent no-op. - Clamp a negative OTEL_EXPORTER_OTLP_TRACES_TIMEOUT to 0 (FFI default) instead of wrapping to a huge uint64. - Strengthen the agent-url endpoint test to assert Some rather than passing vacuously. Co-Authored-By: Claude Opus 4.8 (1M context) --- components-rs/lib.rs | 5 ++--- ext/sidecar.c | 29 ++++++++++++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/components-rs/lib.rs b/components-rs/lib.rs index dfe756fedf0..cf1fd49d792 100644 --- a/components-rs/lib.rs +++ b/components-rs/lib.rs @@ -400,8 +400,7 @@ mod otel_traces_endpoint_tests { datadog_otel_traces_endpoint_from_agent_url(CharSlice::from("http://")) }; // Falls back to localhost when the agent URL has no host. - if let Some(ep) = ep { - assert_eq!(ep.url.to_string(), "http://localhost:4318/v1/traces"); - } + let ep = ep.expect("endpoint should be derived even when the agent URL has no host"); + assert_eq!(ep.url.to_string(), "http://localhost:4318/v1/traces"); } } diff --git a/ext/sidecar.c b/ext/sidecar.c index 00259f66fcf..9cc9e97a41c 100644 --- a/ext/sidecar.c +++ b/ext/sidecar.c @@ -154,15 +154,38 @@ static void dd_sidecar_post_connect(ddog_SidecarTransport **transport, bool is_f // exports this session's traces via libdatadog's OTLP TraceExporter instead // of the agent msgpack path; a NULL endpoint clears the config and restores // the default agent path. - ddog_Endpoint *otlp_traces_endpoint = - datadog_otlp_traces_enabled() ? datadog_otel_traces_endpoint() : NULL; + bool otlp_traces_enabled = datadog_otlp_traces_enabled(); + ddog_Endpoint *otlp_traces_endpoint = otlp_traces_enabled ? datadog_otel_traces_endpoint() : NULL; + if (otlp_traces_enabled) { + // Only http/json is supported this phase; warn (don't fail) on a configured non-http/json + // protocol so a grpc/http-protobuf misconfiguration isn't a silent no-op. Mirrors dd-trace-rs/rb. + zend_string *otlp_protocol = get_global_OTEL_EXPORTER_OTLP_TRACES_PROTOCOL(); + if (otlp_protocol && ZSTR_LEN(otlp_protocol) > 0 && strcmp(ZSTR_VAL(otlp_protocol), "http/json") != 0) { + LOG(WARN, "OTLP trace export only supports the http/json protocol; the configured protocol '%s' is ignored and traces are sent as http/json.", + ZSTR_VAL(otlp_protocol)); + } + // If OTLP is enabled but the endpoint could not be resolved (e.g. a malformed + // OTEL_EXPORTER_OTLP_TRACES_ENDPOINT), a NULL endpoint silently restores the agent path. + // Surface that rather than letting OTLP be a silent no-op. + if (!otlp_traces_endpoint) { + LOG(WARN, "OTEL_TRACES_EXPORTER=otlp is set but the OTLP traces endpoint could not be resolved; falling back to Datadog agent trace export."); + } + } + + // Clamp a negative timeout (INT config) to 0 so it maps to the FFI's "use default" instead of + // wrapping to a huge uint64. + int64_t otlp_traces_timeout = get_global_OTEL_EXPORTER_OTLP_TRACES_TIMEOUT(); + if (otlp_traces_timeout < 0) { + otlp_traces_timeout = 0; + } + datadog_ffi_try("Failed setting OTLP traces endpoint on sidecar session", ddog_sidecar_session_set_otlp_traces_endpoint( transport, session_id, otlp_traces_endpoint, dd_zend_string_to_CharSlice(get_global_OTEL_EXPORTER_OTLP_TRACES_HEADERS()), - (uint64_t)get_global_OTEL_EXPORTER_OTLP_TRACES_TIMEOUT())); + (uint64_t)otlp_traces_timeout)); if (otlp_traces_endpoint) { ddog_endpoint_drop(otlp_traces_endpoint); }