diff --git a/components-rs/datadog.h b/components-rs/datadog.h index 2a75fb13b9..c771311102 100644 --- a/components-rs/datadog.h +++ b/components-rs/datadog.h @@ -55,6 +55,19 @@ 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); +/** + * Builds an OTLP traces endpoint from an explicit, full endpoint URL, used + * as-is (mirrors `datadog_otel_metrics_endpoint_from_url`). + */ +struct ddog_Endpoint *datadog_otel_traces_endpoint_from_url(ddog_CharSlice 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`). + */ +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*), void *userdata); diff --git a/components-rs/lib.rs b/components-rs/lib.rs index f73beec72d..cf1fd49d79 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,38 @@ 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. + 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/components-rs/sidecar.h b/components-rs/sidecar.h index eb0b5be058..e761fbe0a7 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/ext/configuration.h b/ext/configuration.h index d6402649dc..b3e1dd07f7 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 f46e8e9e6e..6c08f7a37e 100644 --- a/ext/endpoints.c +++ b/ext/endpoints.c @@ -122,3 +122,22 @@ 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; +} diff --git a/ext/endpoints.h b/ext/endpoints.h index 09dea97c3b..8db5b9d187 100644 --- a/ext/endpoints.h +++ b/ext/endpoints.h @@ -6,5 +6,6 @@ char *datadog_agent_url(void); char *datadog_dogstatsd_url(void); ddog_Endpoint *datadog_otel_metrics_endpoint(void); +ddog_Endpoint *datadog_otel_traces_endpoint(void); #endif // DATADOG_ENDPOINTS_H diff --git a/ext/otel_config.c b/ext/otel_config.c index a747652d88..d26c3cc06f 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 04aa1ce157..94597805f8 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 74a1e58884..9cc9e97a41 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,50 @@ 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 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. 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. + 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)otlp_traces_timeout)); + if (otlp_traces_endpoint) { + ddog_endpoint_drop(otlp_traces_endpoint); + } + if (get_global_DD_INSTRUMENTATION_TELEMETRY_ENABLED()) { datadog_telemetry_register_services(transport); } diff --git a/ext/sidecar.h b/ext/sidecar.h index ae0a79c5c5..ae981a69e5 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/libdatadog b/libdatadog index 93e9723802..53ed2e4714 160000 --- a/libdatadog +++ b/libdatadog @@ -1 +1 @@ -Subproject commit 93e97238020b5cf1165b2583ec555d58307af3c1 +Subproject commit 53ed2e47146df1352dd24f9fbda8c0897baa93ae diff --git a/tests/ext/otlp_traces_endpoint_fallback.phpt b/tests/ext/otlp_traces_endpoint_fallback.phpt new file mode 100644 index 0000000000..37b56ad737 --- /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 0000000000..cabd140bd9 --- /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 0000000000..b39f2e6955 --- /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 0000000000..83475a1b78 --- /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 0000000000..bb1a9c81c0 --- /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 9781b68708..52bae0cb40 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); }