Skip to content
Draft
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
13 changes: 13 additions & 0 deletions components-rs/datadog.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
86 changes: 86 additions & 0 deletions components-rs/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Box<Endpoint>> {
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<Box<Endpoint>> {
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<Box<Endpoint>> {
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(
Expand Down Expand Up @@ -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");
}
}
24 changes: 24 additions & 0 deletions components-rs/sidecar.h
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
15 changes: 15 additions & 0 deletions ext/configuration.h
Original file line number Diff line number Diff line change
Expand Up @@ -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) \
Expand Down
19 changes: 19 additions & 0 deletions ext/endpoints.c
Original file line number Diff line number Diff line change
Expand Up @@ -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://<agent_host>: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;
}
1 change: 1 addition & 0 deletions ext/endpoints.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
64 changes: 61 additions & 3 deletions ext/otel_config.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
}
Expand All @@ -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;
}
5 changes: 5 additions & 0 deletions ext/otel_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
54 changes: 54 additions & 0 deletions ext/sidecar.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)};
Expand Down Expand Up @@ -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);
}
Expand Down
3 changes: 3 additions & 0 deletions ext/sidecar.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
Loading
Loading