From a47986f1594833421564b93fe46c4f7034f0fb01 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 28 Apr 2026 18:39:55 +0200 Subject: [PATCH 01/32] feat(http-client): add knob to disable connection pooling --- .../src/backend/hyper_backend.rs | 13 +++++- libdd-http-client/src/backend/mod.rs | 6 ++- .../src/backend/reqwest_backend.rs | 5 +++ libdd-http-client/src/client.rs | 6 ++- libdd-http-client/src/config.rs | 44 ++++++++++++++++--- 5 files changed, 65 insertions(+), 9 deletions(-) diff --git a/libdd-http-client/src/backend/hyper_backend.rs b/libdd-http-client/src/backend/hyper_backend.rs index d9aa203aa1..1dc7da2b50 100644 --- a/libdd-http-client/src/backend/hyper_backend.rs +++ b/libdd-http-client/src/backend/hyper_backend.rs @@ -147,9 +147,18 @@ impl super::Backend for HyperBackend { fn new( _timeout: std::time::Duration, transport: TransportConfig, + allow_connection_pooling: bool, ) -> Result { - let client = http_common::client_builder().build(Connector::default()); - Ok(Self { client, transport }) + let builder = http_common::client_builder(); + + if !allow_connection_pooling { + builder = builder.pool_max_idle_per_host(0); + } + + Ok(Self { + client: builder.build(Connector::default()), + transport, + }) } async fn send( diff --git a/libdd-http-client/src/backend/mod.rs b/libdd-http-client/src/backend/mod.rs index eae180acd4..7db33f8ab4 100644 --- a/libdd-http-client/src/backend/mod.rs +++ b/libdd-http-client/src/backend/mod.rs @@ -16,7 +16,11 @@ pub(crate) mod reqwest_backend; /// Backend`. pub(crate) trait Backend: Sized { /// Construct a new backend with the given timeout and transport. - fn new(timeout: Duration, transport: config::TransportConfig) -> Result; + fn new( + timeout: Duration, + transport: config::TransportConfig, + allow_connection_pooling: bool, + ) -> Result; /// Send an HTTP request and return the response. async fn send( diff --git a/libdd-http-client/src/backend/reqwest_backend.rs b/libdd-http-client/src/backend/reqwest_backend.rs index cb99869fb3..e259252d69 100644 --- a/libdd-http-client/src/backend/reqwest_backend.rs +++ b/libdd-http-client/src/backend/reqwest_backend.rs @@ -20,6 +20,7 @@ impl super::Backend for ReqwestBackend { fn new( timeout: std::time::Duration, transport: TransportConfig, + allow_connection_pooling: bool, ) -> Result { let mut builder = reqwest::Client::builder().timeout(timeout); @@ -35,6 +36,10 @@ impl super::Backend for ReqwestBackend { } } + if !allow_connection_pooling { + builder = builder.pool_max_idle_per_host(0); + } + let client = builder .build() .map_err(|e| HttpClientError::InvalidConfig(e.to_string()))?; diff --git a/libdd-http-client/src/client.rs b/libdd-http-client/src/client.rs index 3ca22dae3c..45a2b06300 100644 --- a/libdd-http-client/src/client.rs +++ b/libdd-http-client/src/client.rs @@ -46,7 +46,11 @@ impl HttpClient { config: HttpClientConfig, transport: TransportConfig, ) -> Result { - let backend = BackendImpl::new(config.timeout(), transport)?; + let backend = BackendImpl::new( + config.timeout(), + transport, + config.allow_connection_pooling(), + )?; Ok(Self { backend, config }) } diff --git a/libdd-http-client/src/config.rs b/libdd-http-client/src/config.rs index c78bf20717..9b89b2d417 100644 --- a/libdd-http-client/src/config.rs +++ b/libdd-http-client/src/config.rs @@ -33,6 +33,7 @@ pub struct HttpClientConfig { timeout: Duration, treat_http_errors_as_errors: bool, retry: Option, + allow_connection_pooling: bool, } impl HttpClientConfig { @@ -44,6 +45,7 @@ impl HttpClientConfig { timeout, treat_http_errors_as_errors: true, retry: None, + allow_connection_pooling: true, } } @@ -66,28 +68,45 @@ impl HttpClientConfig { pub fn retry(&self) -> Option<&RetryConfig> { self.retry.as_ref() } + + /// Whether connection pooling can be used, when available. See + /// [HttpClientBuilder::allow_connection_pooling]. + pub fn allow_connection_pooling(&self) -> bool { + self.allow_connection_pooling + } } /// Builder for [`crate::HttpClient`]. /// /// Obtain via [`crate::HttpClient::builder`]. -#[derive(Debug, Default)] +#[derive(Debug)] pub struct HttpClientBuilder { base_url: Option, timeout: Option, treat_http_errors_as_errors: bool, retry: Option, transport: TransportConfig, + allow_connection_pooling: bool, } -impl HttpClientBuilder { - /// Create a new builder with default settings. - pub fn new() -> Self { +impl Default for HttpClientBuilder { + fn default() -> Self { Self { + base_url: Default::default(), + timeout: Default::default(), treat_http_errors_as_errors: true, - ..Default::default() + retry: Default::default(), + transport: Default::default(), + allow_connection_pooling: true, } } +} + +impl HttpClientBuilder { + /// Create a new builder with default settings. + pub fn new() -> Self { + Self::default() + } /// Set the base URL. pub fn base_url(mut self, url: String) -> Self { @@ -136,6 +155,20 @@ impl HttpClientBuilder { self } + /// Allow connection pooling. Defaults to `true`. + /// + /// Note that whether pooling is actually used depends on the HTTP backend of + /// [libdd_http_client], though both currently available backends (reqwest and hyper) support + /// pooling. This setting should be understood as: if set to `true`, the default behavior of the + /// underlying backend will be selected, which might or might not do connection pooling by + /// default. If set to `false`, we guarantee no connection pooling will happen. + /// + /// This setting is used by the Agent-level HTTP client. + pub fn allow_connection_pooling(mut self, allow: bool) -> Self { + self.allow_connection_pooling = allow; + self + } + /// Build the [`crate::HttpClient`]. /// /// Returns [`crate::HttpClientError::InvalidConfig`] if required fields @@ -152,6 +185,7 @@ impl HttpClientBuilder { timeout, treat_http_errors_as_errors: self.treat_http_errors_as_errors, retry: self.retry, + allow_connection_pooling: self.allow_connection_pooling, }; crate::HttpClient::from_config_and_transport(config, self.transport) } From 03770439ce38cd1b5fd7f201775caf6a8c73e0af Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 28 Apr 2026 18:55:56 +0200 Subject: [PATCH 02/32] fix: compile-time mutability error --- libdd-http-client/src/backend/hyper_backend.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libdd-http-client/src/backend/hyper_backend.rs b/libdd-http-client/src/backend/hyper_backend.rs index 1dc7da2b50..7944737332 100644 --- a/libdd-http-client/src/backend/hyper_backend.rs +++ b/libdd-http-client/src/backend/hyper_backend.rs @@ -149,10 +149,10 @@ impl super::Backend for HyperBackend { transport: TransportConfig, allow_connection_pooling: bool, ) -> Result { - let builder = http_common::client_builder(); + let mut builder = http_common::client_builder(); if !allow_connection_pooling { - builder = builder.pool_max_idle_per_host(0); + builder.pool_max_idle_per_host(0); } Ok(Self { From f3b4bf0668b6b03bae2cf965c860d0f00dccd839 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 24 Mar 2026 00:17:17 +0100 Subject: [PATCH 03/32] wip: draft API for agent HTTP client component --- Cargo.lock | 12 + Cargo.toml | 1 + libdd-agent-client/Cargo.toml | 27 ++ libdd-agent-client/src/agent_info.rs | 32 +++ libdd-agent-client/src/builder.rs | 280 ++++++++++++++++++++ libdd-agent-client/src/client.rs | 132 +++++++++ libdd-agent-client/src/error.rs | 43 +++ libdd-agent-client/src/language_metadata.rs | 54 ++++ libdd-agent-client/src/lib.rs | 99 +++++++ libdd-agent-client/src/telemetry.rs | 28 ++ libdd-agent-client/src/traces.rs | 51 ++++ 11 files changed, 759 insertions(+) create mode 100644 libdd-agent-client/Cargo.toml create mode 100644 libdd-agent-client/src/agent_info.rs create mode 100644 libdd-agent-client/src/builder.rs create mode 100644 libdd-agent-client/src/client.rs create mode 100644 libdd-agent-client/src/error.rs create mode 100644 libdd-agent-client/src/language_metadata.rs create mode 100644 libdd-agent-client/src/lib.rs create mode 100644 libdd-agent-client/src/telemetry.rs create mode 100644 libdd-agent-client/src/traces.rs diff --git a/Cargo.lock b/Cargo.lock index 43efba235d..7a0c3489ec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2810,6 +2810,18 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libdd-agent-client" +version = "29.0.0" +dependencies = [ + "bytes", + "libdd-http-client", + "serde", + "serde_json", + "thiserror 2.0.17", + "tokio", +] + [[package]] name = "libdd-alloc" version = "1.0.0" diff --git a/Cargo.toml b/Cargo.toml index 290d70288a..a4c21d8b64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -51,6 +51,7 @@ members = [ "libdd-tinybytes", "libdd-dogstatsd-client", "libdd-http-client", + "libdd-agent-client", "libdd-log", "libdd-log-ffi", ] diff --git a/libdd-agent-client/Cargo.toml b/libdd-agent-client/Cargo.toml new file mode 100644 index 0000000000..f077e1786c --- /dev/null +++ b/libdd-agent-client/Cargo.toml @@ -0,0 +1,27 @@ +# Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +# SPDX-License-Identifier: Apache-2.0 + +[package] +name = "libdd-agent-client" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +description = "Datadog-agent-specialized HTTP client: language metadata injection, per-endpoint send methods, retry, and compression" +homepage = "https://github.com/DataDog/libdatadog/tree/main/libdd-agent-client" +repository = "https://github.com/DataDog/libdatadog/tree/main/libdd-agent-client" + +[lib] +bench = false + +[dependencies] +bytes = "1.4" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +thiserror = "2" +tokio = { version = "1.23", features = ["rt"] } +libdd-http-client = { path = "../libdd-http-client" } + +[dev-dependencies] +tokio = { version = "1.23", features = ["rt", "macros"] } diff --git a/libdd-agent-client/src/agent_info.rs b/libdd-agent-client/src/agent_info.rs new file mode 100644 index 0000000000..8275a41cb9 --- /dev/null +++ b/libdd-agent-client/src/agent_info.rs @@ -0,0 +1,32 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Types for [`crate::AgentClient::agent_info`]. + +/// Parsed response from a `GET /info` probe. +/// +/// Returned by [`crate::AgentClient::agent_info`]. Contains agent capabilities and the +/// headers that dd-trace-py currently processes via the side-effectful `process_info_headers` +/// function (`agent.py:17-23`) — here they are explicit typed fields instead. +#[derive(Debug, Clone)] +pub struct AgentInfo { + /// Available agent endpoints, e.g. `["/v0.4/traces", "/v0.5/traces"]`. + pub endpoints: Vec, + /// Whether the agent supports client-side P0 dropping. + pub client_drop_p0s: bool, + /// Raw agent configuration block. + pub config: serde_json::Value, + /// Agent version string, if reported. + pub version: Option, + /// Parsed from the `Datadog-Container-Tags-Hash` response header. + /// + /// Used by dd-trace-py to compute the base tag hash (`agent.py:17-23`). + pub container_tags_hash: Option, + /// Value of the `Datadog-Agent-State` response header from the last `/info` fetch. + /// + /// The agent updates this opaque token whenever its internal state changes (e.g. a + /// configuration reload). Clients that poll `/info` periodically can skip re-parsing + /// the response body by comparing this value to the one returned by the previous call + /// and only acting when it differs. + pub state_hash: Option, +} diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs new file mode 100644 index 0000000000..ea22e67688 --- /dev/null +++ b/libdd-agent-client/src/builder.rs @@ -0,0 +1,280 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Builder for [`crate::AgentClient`]. + +use std::collections::HashMap; +use std::time::Duration; + +use libdd_http_client::RetryConfig; + +use crate::{error::BuildError, language_metadata::LanguageMetadata, AgentClient}; + +/// Default timeout for agent requests: 2 000 ms. +/// +/// Matches dd-trace-py's `DEFAULT_TIMEOUT = 2.0 s` (`constants.py:97`). +pub const DEFAULT_TIMEOUT_MS: u64 = 2_000; + +/// Default retry configuration: 2 retries (3 total attempts), 100 ms initial delay, +/// exponential backoff with full jitter. +/// +/// This approximates dd-trace-py's `fibonacci_backoff_with_jitter` pattern used in +/// `writer.py:245-249`, `stats.py:123-126`, and `datastreams/processor.py:140-143`. +pub fn default_retry_config() -> RetryConfig { + todo!() +} + +/// Transport configuration for the agent client. +/// +/// Determines how the client connects to the Datadog agent (or an intake endpoint). +/// Set via [`AgentClientBuilder::transport`] or the convenience helpers +/// [`AgentClientBuilder::http`], [`AgentClientBuilder::https`], +/// [`AgentClientBuilder::unix_socket`], etc. +#[derive(Debug, Clone)] +pub enum AgentTransport { + /// HTTP over TCP to `http://{host}:{port}`. + Http { + /// Hostname or IP address. + host: String, + /// Port number. + port: u16, + }, + /// HTTPS over TCP to `https://{host}:{port}` (e.g. for intake endpoints). + Https { + /// Hostname or IP address. + host: String, + /// Port number. + port: u16, + }, + /// Unix Domain Socket. + /// + /// HTTP requests are still formed with `Host: localhost`; the socket path + /// governs only the transport layer. + #[cfg(unix)] + UnixSocket { + /// Filesystem path to the socket file. + path: std::path::PathBuf, + }, + /// Windows Named Pipe. + #[cfg(windows)] + NamedPipe { + /// Named pipe path, e.g. `\\.\pipe\DD_APM_DRIVER`. + path: std::ffi::OsString, + }, + /// Probe at build time: use UDS if the socket file exists, otherwise fall back to HTTP. + /// + /// Mirrors the auto-detect logic in dd-trace-py's `_agent.py:32-49`. + #[cfg(unix)] + AutoDetect { + /// UDS path to probe. + uds_path: std::path::PathBuf, + /// Fallback host when the socket is absent. + fallback_host: String, + /// Fallback port when the socket is absent (typically 8126). + fallback_port: u16, + }, +} + +impl Default for AgentTransport { + fn default() -> Self { + todo!() + } +} + +/// Connection mode for the underlying HTTP client. +/// +/// # Correctness note +/// +/// The Datadog agent has a low keep-alive timeout that causes "pipe closed" errors on every +/// second connection when connection reuse is enabled. [`ClientMode::Periodic`] (the default) +/// disables connection pooling and is **correct** for all periodic-flush writers (traces, stats, +/// data streams). Only high-frequency continuous senders (e.g. a streaming profiling exporter) +/// should opt into [`ClientMode::Persistent`]. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub enum ClientMode { + /// No connection pooling. Correct for periodic flushes to the agent. + #[default] + Periodic, + /// Keep connections alive across requests. + /// + /// Use only for high-frequency continuous senders. + Persistent, +} + +/// Builder for [`AgentClient`]. +/// +/// Obtain via [`AgentClient::builder`]. +/// +/// # Required fields +/// +/// - Transport: set via [`AgentClientBuilder::transport`] or a convenience method +/// ([`AgentClientBuilder::http`], [`AgentClientBuilder::https`], +/// [`AgentClientBuilder::unix_socket`], [`AgentClientBuilder::windows_named_pipe`], +/// [`AgentClientBuilder::auto_detect`]). +/// - [`AgentClientBuilder::language_metadata`]. +/// +/// # Agentless mode +/// +/// Call [`AgentClientBuilder::api_key`] with your Datadog API key and point the transport to +/// the intake endpoint via [`AgentClientBuilder::https`]. The client injects `dd-api-key` on +/// every request. +/// +/// # Testing +/// +/// Call [`AgentClientBuilder::test_token`] to inject `x-datadog-test-session-token` on every +/// request. This replaces dd-trace-py's `AgentWriter.set_test_session_token` (`writer.py:754-755`). +/// +/// # Fork safety +/// +/// The underlying `libdd-http-client` uses `hickory-dns` by default — an in-process, fork-safe +/// DNS resolver that avoids the class of bugs where a forked child inherits open sockets from a +/// parent's DNS thread pool. This is important for host processes that fork (Django, Flask, +/// Celery workers, PHP-FPM, etc.). +#[derive(Debug, Default)] +pub struct AgentClientBuilder { + transport: Option, + api_key: Option, + test_token: Option, + timeout: Option, + language: Option, + retry: Option, + client_mode: ClientMode, + extra_headers: HashMap, +} + +impl AgentClientBuilder { + /// Create a new builder with default settings. + pub fn new() -> Self { + todo!() + } + + // ── Transport ───────────────────────────────────────────────────────────── + + /// Set the transport configuration. + pub fn transport(self, transport: AgentTransport) -> Self { + todo!() + } + + /// Convenience: HTTP over TCP. + pub fn http(self, host: impl Into, port: u16) -> Self { + todo!() + } + + /// Convenience: HTTPS over TCP. + pub fn https(self, host: impl Into, port: u16) -> Self { + todo!() + } + + /// Convenience: Unix Domain Socket. + #[cfg(unix)] + pub fn unix_socket(self, path: impl Into) -> Self { + todo!() + } + + /// Convenience: Windows Named Pipe. + #[cfg(windows)] + pub fn windows_named_pipe(self, path: impl Into) -> Self { + todo!() + } + + /// Convenience: auto-detect transport (UDS if socket file exists, else HTTP). + /// + /// Mirrors the logic in dd-trace-py's `_agent.py:32-49`. + #[cfg(unix)] + pub fn auto_detect( + self, + uds_path: impl Into, + fallback_host: impl Into, + fallback_port: u16, + ) -> Self { + todo!() + } + + // ── Authentication / routing ────────────────────────────────────────────── + + /// Set the Datadog API key (agentless mode). + /// + /// When set, `dd-api-key: ` is injected on every request. + /// Point the transport to the intake endpoint via [`AgentClientBuilder::https`]. + pub fn api_key(self, key: impl Into) -> Self { + todo!() + } + + /// Set the test session token. + /// + /// When set, `x-datadog-test-session-token: ` is injected on every request. + /// Replaces dd-trace-py's `AgentWriter.set_test_session_token` (`writer.py:754-755`). + pub fn test_token(self, token: impl Into) -> Self { + todo!() + } + + // ── Timeout / retries ───────────────────────────────────────────────────── + + /// Set the request timeout. + /// + /// Defaults to [`DEFAULT_TIMEOUT_MS`] (2 000 ms) when not set. + pub fn timeout(self, timeout: Duration) -> Self { + todo!() + } + + /// Read the timeout from `DD_TRACE_AGENT_TIMEOUT_SECONDS`, falling back to + /// [`DEFAULT_TIMEOUT_MS`] if the variable is unset or unparseable. + pub fn timeout_from_env(self) -> Self { + todo!() + } + + /// Override the default retry configuration. + /// + /// Defaults to [`default_retry_config`]: 2 retries, 100 ms initial delay, exponential + /// backoff with full jitter. + pub fn retry(self, config: RetryConfig) -> Self { + todo!() + } + + // ── Language metadata ───────────────────────────────────────────────────── + + /// Set the language/runtime metadata injected into every request. + /// + /// Required. Drives `Datadog-Meta-Lang`, `Datadog-Meta-Lang-Version`, + /// `Datadog-Meta-Lang-Interpreter`, `Datadog-Meta-Tracer-Version`, and `User-Agent`. + pub fn language_metadata(self, meta: LanguageMetadata) -> Self { + todo!() + } + + // ── Connection pooling ──────────────────────────────────────────────────── + + /// Set the connection mode. Defaults to [`ClientMode::Periodic`]. + /// + /// See [`ClientMode`] for the correctness rationale behind the default. + pub fn client_mode(self, mode: ClientMode) -> Self { + todo!() + } + + // ── Compression ─────────────────────────────────────────────────────────── + // + // Not exposed in v1. Gzip compression (level 6, matching dd-trace-py's trace writer at + // `writer.py:490`) will be added in a follow-up once the core send paths are stable. + // Per-method defaults (e.g. unconditional gzip for `send_pipeline_stats`) are already + // baked in; only the opt-in client-level `gzip(level)` builder knob is deferred. + + // ── Extra headers ───────────────────────────────────────────────────────── + + /// Merge additional headers into every request. + /// + /// Intended for `_DD_TRACE_WRITER_ADDITIONAL_HEADERS` in dd-trace-py. + pub fn extra_headers(self, headers: HashMap) -> Self { + todo!() + } + + // ── Build ───────────────────────────────────────────────────────────────── + + /// Build the [`AgentClient`]. + /// + /// # Errors + /// + /// - [`BuildError::MissingTransport`] — no transport was configured. + /// - [`BuildError::MissingLanguageMetadata`] — no language metadata was configured. + pub fn build(self) -> Result { + todo!() + } +} diff --git a/libdd-agent-client/src/client.rs b/libdd-agent-client/src/client.rs new file mode 100644 index 0000000000..11bf4d6a4e --- /dev/null +++ b/libdd-agent-client/src/client.rs @@ -0,0 +1,132 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! [`AgentClient`] and its send methods. + +use bytes::Bytes; + +use crate::{ + agent_info::AgentInfo, + builder::AgentClientBuilder, + error::SendError, + telemetry::TelemetryRequest, + traces::{AgentResponse, TraceFormat, TraceSendOptions}, +}; + +/// A Datadog-agent-specialized HTTP client. +/// +/// Wraps a configured [`libdd_http_client::HttpClient`] and injects Datadog-specific headers +/// automatically on every request: +/// +/// - Language metadata headers (`Datadog-Meta-Lang`, `Datadog-Meta-Lang-Version`, +/// `Datadog-Meta-Lang-Interpreter`, `Datadog-Meta-Tracer-Version`) from the [`LanguageMetadata`] +/// supplied at build time. +/// - `User-Agent` derived from [`LanguageMetadata::user_agent`]. +/// - Container/entity-ID headers (`Datadog-Container-Id`, `Datadog-Entity-ID`, +/// `Datadog-External-Env`) read from `/proc/self/cgroup` at startup, equivalent to dd-trace-py's +/// `container.update_headers()` (`container.py:157-183`). +/// - `dd-api-key` when an API key was set (agentless mode). +/// - `x-datadog-test-session-token` when a test token was set. +/// - Any extra headers registered via [`AgentClientBuilder::extra_headers`]. +/// +/// Obtain via [`AgentClient::builder`]. +/// +/// [`LanguageMetadata`]: crate::LanguageMetadata +pub struct AgentClient { + // Opaque — fields are an implementation detail. +} + +impl AgentClient { + /// Create a new [`AgentClientBuilder`]. + pub fn builder() -> AgentClientBuilder { + todo!() + } + + /// Send a serialised trace payload to the agent. + /// + /// # Automatically injected headers + /// + /// - `X-Datadog-Trace-Count: ` (per-payload — `writer.py:749-752`) + /// - `Datadog-Send-Real-Http-Status: true` — instructs the agent to return 429 when it drops a + /// payload, rather than silently returning 200. dd-trace-py never sets this header, causing + /// silent drops that are invisible to the caller. + /// - `Datadog-Client-Computed-Top-Level: yes` when [`TraceSendOptions::computed_top_level`] is + /// `true`. + /// - Language metadata headers + container headers (see type-level docs). + /// - `Content-Type` and endpoint path derived from `format`. + /// - `Content-Encoding: gzip` when compression is enabled. + /// + /// # Returns + /// + /// An [`AgentResponse`] with the HTTP status and the parsed `rate_by_service` sampling + /// rates from the agent response body (`writer.py:728-734`). + pub async fn send_traces( + &self, + payload: Bytes, + trace_count: usize, + format: TraceFormat, + opts: TraceSendOptions, + ) -> Result { + todo!() + } + + /// Send span stats (APM concentrator buckets) to `/v0.6/stats`. + /// + /// `Content-Type` is always `application/msgpack`. Replaces the manual + /// `get_connection` + raw `PUT` in `SpanStatsProcessor._flush_stats` (`stats.py:204-228`). + pub async fn send_stats(&self, payload: Bytes) -> Result<(), SendError> { + todo!() + } + + /// Send data-streams pipeline stats to `/v0.1/pipeline_stats`. + /// + /// The payload is **always** gzip-compressed regardless of the client-level compression + /// setting. This is a protocol requirement of the data-streams endpoint + /// (`datastreams/processor.py:132`) and must not be a caller responsibility. + pub async fn send_pipeline_stats(&self, payload: Bytes) -> Result<(), SendError> { + todo!() + } + + /// Send a telemetry event. + /// + /// Endpoint routing: + /// - Agent mode → `telemetry/proxy/api/v2/apmtelemetry` + /// - Agentless mode (API key set) → `api/v2/apmtelemetry` on the configured intake host + /// + /// Per-request headers `DD-Telemetry-Request-Type`, `DD-Telemetry-API-Version`, and + /// `DD-Telemetry-Debug-Enabled` are injected automatically from `req`, replacing the + /// manual construction in `_TelemetryClient.get_headers` (`telemetry/writer.py:111-117`). + pub async fn send_telemetry(&self, req: TelemetryRequest) -> Result<(), SendError> { + todo!() + } + + /// Send an event via the agent's EVP (Event Platform) proxy. + /// + /// The agent forwards the request to `.datadoghq.com`. `subdomain` + /// controls the target intake (injected as `X-Datadog-EVP-Subdomain`); `path` is the + /// endpoint on that intake (e.g. `/api/v2/exposures`). + /// + /// In dd-trace-py's openfeature writer both values are hardcoded constants + /// (`openfeature/writer.py:24-27`), but they are independent routing dimensions and + /// must both be supplied by the caller. + pub async fn send_evp_event( + &self, + subdomain: &str, + path: &str, + payload: Bytes, + content_type: &str, + ) -> Result<(), SendError> { + todo!() + } + + /// Probe `GET /info` and return parsed agent capabilities. + /// + /// Processes the `Datadog-Container-Tags-Hash` response header and exposes it as + /// [`AgentInfo::container_tags_hash`] rather than as a side-effect (as in dd-trace-py's + /// `process_info_headers` at `agent.py:17-23`). + /// + /// Returns `Ok(None)` when the agent returns 404 (remote-config / info not supported). + pub async fn agent_info(&self) -> Result, SendError> { + todo!() + } +} diff --git a/libdd-agent-client/src/error.rs b/libdd-agent-client/src/error.rs new file mode 100644 index 0000000000..e9e6dd9355 --- /dev/null +++ b/libdd-agent-client/src/error.rs @@ -0,0 +1,43 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Error types for [`crate::AgentClient`]. + +use bytes::Bytes; +use thiserror::Error; + +/// Errors that can occur when building an [`crate::AgentClient`]. +#[derive(Debug, Error)] +pub enum BuildError { + /// No transport was configured. + #[error("transport is required")] + MissingTransport, + /// No language metadata was configured. + #[error("language metadata is required")] + MissingLanguageMetadata, +} + +/// Errors that can occur when sending a request via [`crate::AgentClient`]. +#[derive(Debug, Error)] +pub enum SendError { + /// Connection refused, timeout, or I/O error. + #[error("transport error: {0}")] + Transport(#[source] std::io::Error), + /// The server returned an HTTP error status. Includes the raw status and body. + #[error("HTTP error {status}: {body:?}")] + HttpError { + /// HTTP status code returned by the server. + status: u16, + /// Raw response body. + body: Bytes, + }, + /// All retry attempts exhausted without a successful response. + #[error("retries exhausted: {last_error}")] + RetriesExhausted { + /// The last error encountered before giving up. + last_error: Box, + }, + /// Payload serialisation or compression failure. + #[error("encoding error: {0}")] + Encoding(String), +} diff --git a/libdd-agent-client/src/language_metadata.rs b/libdd-agent-client/src/language_metadata.rs new file mode 100644 index 0000000000..c4c277e214 --- /dev/null +++ b/libdd-agent-client/src/language_metadata.rs @@ -0,0 +1,54 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Language/runtime metadata injected into every outgoing request. + +/// Language and runtime metadata that is automatically injected into every request as +/// `Datadog-Meta-*` headers and drives the `User-Agent` string. +/// +/// | Header | Field | +/// |---------------------------------|--------------------| +/// | `Datadog-Meta-Lang` | `language` | +/// | `Datadog-Meta-Lang-Version` | `language_version` | +/// | `Datadog-Meta-Lang-Interpreter` | `interpreter` | +/// | `Datadog-Meta-Tracer-Version` | `tracer_version` | +/// +/// These four headers are today manually assembled in four separate places in dd-trace-py: +/// `writer.py:638-644`, `writer.py:785-792`, `stats.py:113-117`, and +/// `datastreams/processor.py:128-133`. A single `LanguageMetadata` instance replaces all of them. +/// +/// # `User-Agent` +/// +/// [`LanguageMetadata::user_agent`] produces the string passed to +/// `Endpoint::to_request_builder(user_agent)`, so the `User-Agent` and the `Datadog-Meta-*` +/// headers share a single source of truth. +#[derive(Debug, Clone)] +pub struct LanguageMetadata { + /// Language name, e.g. `"python"`. + pub language: String, + /// Language runtime version, e.g. `"3.12.1"`. + pub language_version: String, + /// Interpreter name, e.g. `"CPython"`. + pub interpreter: String, + /// Tracer library version, e.g. `"2.18.0"`. + pub tracer_version: String, +} + +impl LanguageMetadata { + /// Construct a new `LanguageMetadata`. + pub fn new( + language: impl Into, + language_version: impl Into, + interpreter: impl Into, + tracer_version: impl Into, + ) -> Self { + todo!() + } + + /// Produces the `User-Agent` string passed to `Endpoint::to_request_builder()`. + /// + /// Format: `dd-trace-/`, e.g. `dd-trace-python/2.18.0`. + pub fn user_agent(&self) -> String { + todo!() + } +} diff --git a/libdd-agent-client/src/lib.rs b/libdd-agent-client/src/lib.rs new file mode 100644 index 0000000000..f7414343dd --- /dev/null +++ b/libdd-agent-client/src/lib.rs @@ -0,0 +1,99 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! `libdd-agent-client` — Datadog-agent-specialised HTTP client. +//! +//! This crate is **Milestone 2** of APMSP-2721 (libdd-http-client: Common HTTP Client for +//! Language Clients). It sits on top of the basic `libdd-http-client` primitives (Milestone 1) +//! and encapsulates all Datadog-agent-specific concerns, making them the **default** rather than +//! opt-in boilerplate that every subsystem must repeat. +//! +//! # What it replaces in dd-trace-py +//! +//! | Concern | dd-trace-py location | +//! |---------|----------------------| +//! | Language metadata headers | `writer.py:638-644`, `stats.py:113-117`, `datastreams/processor.py:128-133` | +//! | Container/entity-ID headers | `http.py:32-37`, `container.py:157-183` | +//! | Retry logic (fibonacci backoff) | `writer.py:245-249`, `stats.py:123-126`, `datastreams/processor.py:140-143` | +//! | Trace send with `X-Datadog-Trace-Count` | `writer.py:749-752` | +//! | `rate_by_service` parsing | `writer.py:728-734` | +//! | Stats send | `stats.py:204-228` | +//! | Pipeline stats send (always gzip) | `datastreams/processor.py:204-210` | +//! | Telemetry send with per-request headers | `telemetry/writer.py:111-129` | +//! | EVP event send | `openfeature/writer.py:114-117` | +//! | `GET /info` with typed result | `agent.py:17-46` | +//! +//! # Quick start +//! +//! ```rust,no_run +//! # async fn example() -> Result<(), libdd_agent_client::BuildError> { +//! use libdd_agent_client::{AgentClient, LanguageMetadata}; +//! +//! let client = AgentClient::builder() +//! .http("localhost", 8126) +//! .language_metadata(LanguageMetadata::new( +//! "python", "3.12.1", "CPython", "2.18.0", +//! )) +//! .build()?; +//! # Ok(()) +//! # } +//! ``` +//! +//! # Agentless mode +//! +//! Set an API key via [`builder::AgentClientBuilder::api_key`] and point the transport to the +//! intake endpoint: +//! +//! ```rust,no_run +//! # async fn example() -> Result<(), libdd_agent_client::BuildError> { +//! use libdd_agent_client::{AgentClient, LanguageMetadata}; +//! +//! let client = AgentClient::builder() +//! .https("public-trace-http-intake.logs.datadoghq.com", 443) +//! .api_key("my-api-key") +//! .language_metadata(LanguageMetadata::new( +//! "python", "3.12.1", "CPython", "2.18.0", +//! )) +//! .build()?; +//! # Ok(()) +//! # } +//! ``` +//! +//! # Unix Domain Socket +//! +//! ```rust,no_run +//! # #[cfg(unix)] +//! # async fn example() -> Result<(), libdd_agent_client::BuildError> { +//! use libdd_agent_client::{AgentClient, LanguageMetadata}; +//! +//! let client = AgentClient::builder() +//! .unix_socket("/var/run/datadog/apm.socket") +//! .language_metadata(LanguageMetadata::new( +//! "python", "3.12.1", "CPython", "2.18.0", +//! )) +//! .build()?; +//! # Ok(()) +//! # } +//! ``` +//! +//! # Fork safety +//! +//! The underlying `libdd-http-client` uses `hickory-dns` by default — an in-process, fork-safe +//! DNS resolver. This protects against the class of DNS bugs that can occur in forking processes +//! (Django workers, Celery, PHP-FPM, etc.). + +pub mod agent_info; +pub mod builder; +pub mod client; +pub mod error; +pub mod language_metadata; +pub mod telemetry; +pub mod traces; + +pub use agent_info::AgentInfo; +pub use builder::{AgentClientBuilder, AgentTransport, ClientMode}; +pub use client::AgentClient; +pub use error::{BuildError, SendError}; +pub use language_metadata::LanguageMetadata; +pub use telemetry::TelemetryRequest; +pub use traces::{AgentResponse, TraceFormat, TraceSendOptions}; diff --git a/libdd-agent-client/src/telemetry.rs b/libdd-agent-client/src/telemetry.rs new file mode 100644 index 0000000000..4d2c6403b4 --- /dev/null +++ b/libdd-agent-client/src/telemetry.rs @@ -0,0 +1,28 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Types specific to [`crate::AgentClient::send_telemetry`]. + +/// A single telemetry event to send via [`crate::AgentClient::send_telemetry`]. +/// +/// The three per-request headers — `DD-Telemetry-Request-Type`, `DD-Telemetry-API-Version`, and +/// `DD-Telemetry-Debug-Enabled` — are derived automatically from this struct, removing the +/// need for callers to build headers manually (as done in `telemetry/writer.py:111-117`). +/// +/// Endpoint routing (agent proxy vs. agentless intake) is resolved by the client based on +/// whether an API key was set at build time, replacing the ad-hoc logic at +/// `telemetry/writer.py:119-129`. +#[derive(Debug, Clone)] +pub struct TelemetryRequest { + /// Value for the `DD-Telemetry-Request-Type` header, e.g. `"app-started"`. + pub request_type: String, + /// Value for the `DD-Telemetry-API-Version` header, e.g. `"v2"`. + pub api_version: String, + /// When `true`, sets `DD-Telemetry-Debug-Enabled: true`. + pub debug: bool, + /// Pre-serialized JSON payload body. + /// + /// The caller is responsible for serializing the event body to JSON before constructing + /// this struct. The client sends these bytes as-is with `Content-Type: application/json`. + pub body: bytes::Bytes, +} diff --git a/libdd-agent-client/src/traces.rs b/libdd-agent-client/src/traces.rs new file mode 100644 index 0000000000..2db90c6d2b --- /dev/null +++ b/libdd-agent-client/src/traces.rs @@ -0,0 +1,51 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Types specific to [`crate::AgentClient::send_traces`]. + +use std::collections::HashMap; + +/// Wire format of the trace payload. +/// +/// Determines both the `Content-Type` header and the target endpoint. +/// +/// # Format selection +/// +/// The caller is currently responsible for choosing the format. In practice this means +/// starting with [`TraceFormat::MsgpackV5`] and downgrading to [`TraceFormat::MsgpackV4`] +/// when the agent returns 404 or 415 (e.g. on Windows, or when AppSec/IAST is active) — +/// the same sticky downgrade that dd-trace-py performs in `AgentWriter` (`writer.py`). +/// +/// In a future version this negotiation may be moved into the client itself so that format +/// selection becomes automatic and callers no longer need to track the downgrade state. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TraceFormat { + /// `application/msgpack` to `/v0.5/traces`. Preferred format. + MsgpackV5, + /// `application/msgpack` to `/v0.4/traces`. Fallback for Windows / AppSec. + MsgpackV4, + /// `application/json` to `/v1/input`. Used in agentless mode. + JsonV1, +} + +/// Per-request options for [`crate::AgentClient::send_traces`]. +#[derive(Debug, Clone, Default)] +pub struct TraceSendOptions { + /// When `true`, appends `Datadog-Client-Computed-Top-Level: yes`. + /// + /// Signals to the agent that the client has already marked top-level spans, allowing the agent + /// to skip its own top-level computation. In dd-trace-py this header is always set + /// (`writer.py:643`); here it is opt-in so that callers that do not compute top-level spans + /// can omit it. + pub computed_top_level: bool, +} + +/// Parsed response from the agent after a successful trace submission. +#[derive(Debug, Clone)] +pub struct AgentResponse { + /// HTTP status code returned by the agent. + pub status: u16, + /// Per-service sampling rates parsed from the `rate_by_service` field of the agent response + /// body, if present. Mirrors the JSON parsing done in dd-trace-py at `writer.py:728-734`. + pub rate_by_service: Option>, +} From bf9d7f20e9540bf050a36d02225d58fda5692b53 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 15 Apr 2026 15:18:43 +0200 Subject: [PATCH 04/32] Apply suggestion from @brettlangdon Co-authored-by: Brett Langdon --- libdd-agent-client/src/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index ea22e67688..936cd59d69 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -204,7 +204,7 @@ impl AgentClientBuilder { /// /// When set, `x-datadog-test-session-token: ` is injected on every request. /// Replaces dd-trace-py's `AgentWriter.set_test_session_token` (`writer.py:754-755`). - pub fn test_token(self, token: impl Into) -> Self { + pub fn test_agent_session_token(self, token: impl Into) -> Self { todo!() } From 8dd38029da912fcb3e52ddde385999429f5e3b93 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 15 Apr 2026 15:55:05 +0200 Subject: [PATCH 05/32] refactor: get rid of client mode, use boolean instead --- libdd-agent-client/src/builder.rs | 34 ++++++++----------------------- libdd-agent-client/src/lib.rs | 2 +- 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index 936cd59d69..8ba518b38e 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -81,26 +81,6 @@ impl Default for AgentTransport { } } -/// Connection mode for the underlying HTTP client. -/// -/// # Correctness note -/// -/// The Datadog agent has a low keep-alive timeout that causes "pipe closed" errors on every -/// second connection when connection reuse is enabled. [`ClientMode::Periodic`] (the default) -/// disables connection pooling and is **correct** for all periodic-flush writers (traces, stats, -/// data streams). Only high-frequency continuous senders (e.g. a streaming profiling exporter) -/// should opt into [`ClientMode::Persistent`]. -#[derive(Debug, Clone, Default, PartialEq, Eq)] -pub enum ClientMode { - /// No connection pooling. Correct for periodic flushes to the agent. - #[default] - Periodic, - /// Keep connections alive across requests. - /// - /// Use only for high-frequency continuous senders. - Persistent, -} - /// Builder for [`AgentClient`]. /// /// Obtain via [`AgentClient::builder`]. @@ -138,7 +118,7 @@ pub struct AgentClientBuilder { timeout: Option, language: Option, retry: Option, - client_mode: ClientMode, + keep_alive: bool, extra_headers: HashMap, } @@ -243,11 +223,15 @@ impl AgentClientBuilder { // ── Connection pooling ──────────────────────────────────────────────────── - /// Set the connection mode. Defaults to [`ClientMode::Periodic`]. + /// Enable or disable HTTP keep-alive. Defaults to `false`. /// - /// See [`ClientMode`] for the correctness rationale behind the default. - pub fn client_mode(self, mode: ClientMode) -> Self { - todo!() + /// The Datadog agent has a low keep-alive timeout that causes "pipe closed" errors on every + /// second connection when keep-alive is enabled. The default of `false` is correct for all + /// periodic-flush writers (traces, stats, data streams). Set to `true` only for + /// high-frequency continuous senders (e.g. a streaming profiling exporter). + pub fn use_keep_alive(mut self, enabled: bool) -> Self { + self.keep_alive = enabled; + self } // ── Compression ─────────────────────────────────────────────────────────── diff --git a/libdd-agent-client/src/lib.rs b/libdd-agent-client/src/lib.rs index f7414343dd..4858301a7e 100644 --- a/libdd-agent-client/src/lib.rs +++ b/libdd-agent-client/src/lib.rs @@ -91,7 +91,7 @@ pub mod telemetry; pub mod traces; pub use agent_info::AgentInfo; -pub use builder::{AgentClientBuilder, AgentTransport, ClientMode}; +pub use builder::{AgentClientBuilder, AgentTransport}; pub use client::AgentClient; pub use error::{BuildError, SendError}; pub use language_metadata::LanguageMetadata; From 567bc07d01028c0ca4cc1576c7c4edc1d33945fb Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 15 Apr 2026 16:40:32 +0200 Subject: [PATCH 06/32] refactor: get rid of agentless-specific parts of the API Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 2 +- libdd-agent-client/src/builder.rs | 44 ++++++----------------------- libdd-agent-client/src/client.rs | 8 ++---- libdd-agent-client/src/lib.rs | 20 ------------- libdd-agent-client/src/telemetry.rs | 5 ++-- libdd-agent-client/src/traces.rs | 2 -- 6 files changed, 13 insertions(+), 68 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7a0c3489ec..998651276c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2812,7 +2812,7 @@ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libdd-agent-client" -version = "29.0.0" +version = "31.0.0" dependencies = [ "bytes", "libdd-http-client", diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index 8ba518b38e..72a7bdb5f8 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -26,10 +26,9 @@ pub fn default_retry_config() -> RetryConfig { /// Transport configuration for the agent client. /// -/// Determines how the client connects to the Datadog agent (or an intake endpoint). +/// Determines how the client connects to the Datadog agent. /// Set via [`AgentClientBuilder::transport`] or the convenience helpers -/// [`AgentClientBuilder::http`], [`AgentClientBuilder::https`], -/// [`AgentClientBuilder::unix_socket`], etc. +/// [`AgentClientBuilder::http`], [`AgentClientBuilder::unix_socket`], etc. #[derive(Debug, Clone)] pub enum AgentTransport { /// HTTP over TCP to `http://{host}:{port}`. @@ -39,13 +38,6 @@ pub enum AgentTransport { /// Port number. port: u16, }, - /// HTTPS over TCP to `https://{host}:{port}` (e.g. for intake endpoints). - Https { - /// Hostname or IP address. - host: String, - /// Port number. - port: u16, - }, /// Unix Domain Socket. /// /// HTTP requests are still formed with `Host: localhost`; the socket path @@ -88,21 +80,15 @@ impl Default for AgentTransport { /// # Required fields /// /// - Transport: set via [`AgentClientBuilder::transport`] or a convenience method -/// ([`AgentClientBuilder::http`], [`AgentClientBuilder::https`], -/// [`AgentClientBuilder::unix_socket`], [`AgentClientBuilder::windows_named_pipe`], -/// [`AgentClientBuilder::auto_detect`]). +/// ([`AgentClientBuilder::http`], [`AgentClientBuilder::unix_socket`], +/// [`AgentClientBuilder::windows_named_pipe`], [`AgentClientBuilder::auto_detect`]). /// - [`AgentClientBuilder::language_metadata`]. /// -/// # Agentless mode -/// -/// Call [`AgentClientBuilder::api_key`] with your Datadog API key and point the transport to -/// the intake endpoint via [`AgentClientBuilder::https`]. The client injects `dd-api-key` on -/// every request. -/// /// # Testing /// -/// Call [`AgentClientBuilder::test_token`] to inject `x-datadog-test-session-token` on every -/// request. This replaces dd-trace-py's `AgentWriter.set_test_session_token` (`writer.py:754-755`). +/// Call [`AgentClientBuilder::test_agent_session_token`] to inject +/// `x-datadog-test-session-token` on every request. This replaces dd-trace-py's +/// `AgentWriter.set_test_session_token` (`writer.py:754-755`). /// /// # Fork safety /// @@ -113,7 +99,6 @@ impl Default for AgentTransport { #[derive(Debug, Default)] pub struct AgentClientBuilder { transport: Option, - api_key: Option, test_token: Option, timeout: Option, language: Option, @@ -140,11 +125,6 @@ impl AgentClientBuilder { todo!() } - /// Convenience: HTTPS over TCP. - pub fn https(self, host: impl Into, port: u16) -> Self { - todo!() - } - /// Convenience: Unix Domain Socket. #[cfg(unix)] pub fn unix_socket(self, path: impl Into) -> Self { @@ -170,15 +150,7 @@ impl AgentClientBuilder { todo!() } - // ── Authentication / routing ────────────────────────────────────────────── - - /// Set the Datadog API key (agentless mode). - /// - /// When set, `dd-api-key: ` is injected on every request. - /// Point the transport to the intake endpoint via [`AgentClientBuilder::https`]. - pub fn api_key(self, key: impl Into) -> Self { - todo!() - } + // ── Test session token ──────────────────────────────────────────────────── /// Set the test session token. /// diff --git a/libdd-agent-client/src/client.rs b/libdd-agent-client/src/client.rs index 11bf4d6a4e..0b90845388 100644 --- a/libdd-agent-client/src/client.rs +++ b/libdd-agent-client/src/client.rs @@ -53,7 +53,7 @@ impl AgentClient { /// - `Datadog-Client-Computed-Top-Level: yes` when [`TraceSendOptions::computed_top_level`] is /// `true`. /// - Language metadata headers + container headers (see type-level docs). - /// - `Content-Type` and endpoint path derived from `format`. + /// - `Content-Type` (`application/msgpack`) and endpoint path derived from `format`. /// - `Content-Encoding: gzip` when compression is enabled. /// /// # Returns @@ -87,11 +87,7 @@ impl AgentClient { todo!() } - /// Send a telemetry event. - /// - /// Endpoint routing: - /// - Agent mode → `telemetry/proxy/api/v2/apmtelemetry` - /// - Agentless mode (API key set) → `api/v2/apmtelemetry` on the configured intake host + /// Send a telemetry event to the agent's telemetry proxy (`telemetry/proxy/api/v2/apmtelemetry`). /// /// Per-request headers `DD-Telemetry-Request-Type`, `DD-Telemetry-API-Version`, and /// `DD-Telemetry-Debug-Enabled` are injected automatically from `req`, replacing the diff --git a/libdd-agent-client/src/lib.rs b/libdd-agent-client/src/lib.rs index 4858301a7e..21256a0d71 100644 --- a/libdd-agent-client/src/lib.rs +++ b/libdd-agent-client/src/lib.rs @@ -39,26 +39,6 @@ //! # } //! ``` //! -//! # Agentless mode -//! -//! Set an API key via [`builder::AgentClientBuilder::api_key`] and point the transport to the -//! intake endpoint: -//! -//! ```rust,no_run -//! # async fn example() -> Result<(), libdd_agent_client::BuildError> { -//! use libdd_agent_client::{AgentClient, LanguageMetadata}; -//! -//! let client = AgentClient::builder() -//! .https("public-trace-http-intake.logs.datadoghq.com", 443) -//! .api_key("my-api-key") -//! .language_metadata(LanguageMetadata::new( -//! "python", "3.12.1", "CPython", "2.18.0", -//! )) -//! .build()?; -//! # Ok(()) -//! # } -//! ``` -//! //! # Unix Domain Socket //! //! ```rust,no_run diff --git a/libdd-agent-client/src/telemetry.rs b/libdd-agent-client/src/telemetry.rs index 4d2c6403b4..58de85e1de 100644 --- a/libdd-agent-client/src/telemetry.rs +++ b/libdd-agent-client/src/telemetry.rs @@ -9,9 +9,8 @@ /// `DD-Telemetry-Debug-Enabled` — are derived automatically from this struct, removing the /// need for callers to build headers manually (as done in `telemetry/writer.py:111-117`). /// -/// Endpoint routing (agent proxy vs. agentless intake) is resolved by the client based on -/// whether an API key was set at build time, replacing the ad-hoc logic at -/// `telemetry/writer.py:119-129`. +/// The client always routes to the agent telemetry proxy endpoint +/// (`telemetry/proxy/api/v2/apmtelemetry`). #[derive(Debug, Clone)] pub struct TelemetryRequest { /// Value for the `DD-Telemetry-Request-Type` header, e.g. `"app-started"`. diff --git a/libdd-agent-client/src/traces.rs b/libdd-agent-client/src/traces.rs index 2db90c6d2b..9879a558b3 100644 --- a/libdd-agent-client/src/traces.rs +++ b/libdd-agent-client/src/traces.rs @@ -24,8 +24,6 @@ pub enum TraceFormat { MsgpackV5, /// `application/msgpack` to `/v0.4/traces`. Fallback for Windows / AppSec. MsgpackV4, - /// `application/json` to `/v1/input`. Used in agentless mode. - JsonV1, } /// Per-request options for [`crate::AgentClient::send_traces`]. From 99c19c2e449eb97399591ab4308e8eeb18a8a94e Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Thu, 16 Apr 2026 11:01:27 +0200 Subject: [PATCH 07/32] doc: clean LLM logorrhea --- libdd-agent-client/src/agent_info.rs | 4 +- libdd-agent-client/src/builder.rs | 65 ++++----------------- libdd-agent-client/src/client.rs | 40 ++----------- libdd-agent-client/src/language_metadata.rs | 25 ++------ libdd-agent-client/src/lib.rs | 28 ++------- libdd-agent-client/src/telemetry.rs | 11 ++-- libdd-agent-client/src/traces.rs | 13 ++--- 7 files changed, 35 insertions(+), 151 deletions(-) diff --git a/libdd-agent-client/src/agent_info.rs b/libdd-agent-client/src/agent_info.rs index 8275a41cb9..e9193f9d8b 100644 --- a/libdd-agent-client/src/agent_info.rs +++ b/libdd-agent-client/src/agent_info.rs @@ -5,9 +5,7 @@ /// Parsed response from a `GET /info` probe. /// -/// Returned by [`crate::AgentClient::agent_info`]. Contains agent capabilities and the -/// headers that dd-trace-py currently processes via the side-effectful `process_info_headers` -/// function (`agent.py:17-23`) — here they are explicit typed fields instead. +/// Returned by [`crate::AgentClient::agent_info`]. Contains agent capabilities and headers. #[derive(Debug, Clone)] pub struct AgentInfo { /// Available agent endpoints, e.g. `["/v0.4/traces", "/v0.5/traces"]`. diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index 72a7bdb5f8..ec3a2252d1 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -10,16 +10,11 @@ use libdd_http_client::RetryConfig; use crate::{error::BuildError, language_metadata::LanguageMetadata, AgentClient}; -/// Default timeout for agent requests: 2 000 ms. -/// -/// Matches dd-trace-py's `DEFAULT_TIMEOUT = 2.0 s` (`constants.py:97`). +/// Default timeout for agent requests. pub const DEFAULT_TIMEOUT_MS: u64 = 2_000; /// Default retry configuration: 2 retries (3 total attempts), 100 ms initial delay, /// exponential backoff with full jitter. -/// -/// This approximates dd-trace-py's `fibonacci_backoff_with_jitter` pattern used in -/// `writer.py:245-249`, `stats.py:123-126`, and `datastreams/processor.py:140-143`. pub fn default_retry_config() -> RetryConfig { todo!() } @@ -31,7 +26,7 @@ pub fn default_retry_config() -> RetryConfig { /// [`AgentClientBuilder::http`], [`AgentClientBuilder::unix_socket`], etc. #[derive(Debug, Clone)] pub enum AgentTransport { - /// HTTP over TCP to `http://{host}:{port}`. + /// HTTP over TCP. Http { /// Hostname or IP address. host: String, @@ -40,8 +35,8 @@ pub enum AgentTransport { }, /// Unix Domain Socket. /// - /// HTTP requests are still formed with `Host: localhost`; the socket path - /// governs only the transport layer. + /// HTTP requests are still formed with `Host: localhost`. The socket path governs only the + /// transport layer. #[cfg(unix)] UnixSocket { /// Filesystem path to the socket file. @@ -54,8 +49,6 @@ pub enum AgentTransport { path: std::ffi::OsString, }, /// Probe at build time: use UDS if the socket file exists, otherwise fall back to HTTP. - /// - /// Mirrors the auto-detect logic in dd-trace-py's `_agent.py:32-49`. #[cfg(unix)] AutoDetect { /// UDS path to probe. @@ -84,18 +77,10 @@ impl Default for AgentTransport { /// [`AgentClientBuilder::windows_named_pipe`], [`AgentClientBuilder::auto_detect`]). /// - [`AgentClientBuilder::language_metadata`]. /// -/// # Testing +/// # Test tokens /// /// Call [`AgentClientBuilder::test_agent_session_token`] to inject -/// `x-datadog-test-session-token` on every request. This replaces dd-trace-py's -/// `AgentWriter.set_test_session_token` (`writer.py:754-755`). -/// -/// # Fork safety -/// -/// The underlying `libdd-http-client` uses `hickory-dns` by default — an in-process, fork-safe -/// DNS resolver that avoids the class of bugs where a forked child inherits open sockets from a -/// parent's DNS thread pool. This is important for host processes that fork (Django, Flask, -/// Celery workers, PHP-FPM, etc.). +/// `x-datadog-test-session-token` on every request. #[derive(Debug, Default)] pub struct AgentClientBuilder { transport: Option, @@ -113,8 +98,6 @@ impl AgentClientBuilder { todo!() } - // ── Transport ───────────────────────────────────────────────────────────── - /// Set the transport configuration. pub fn transport(self, transport: AgentTransport) -> Self { todo!() @@ -138,8 +121,6 @@ impl AgentClientBuilder { } /// Convenience: auto-detect transport (UDS if socket file exists, else HTTP). - /// - /// Mirrors the logic in dd-trace-py's `_agent.py:32-49`. #[cfg(unix)] pub fn auto_detect( self, @@ -150,18 +131,13 @@ impl AgentClientBuilder { todo!() } - // ── Test session token ──────────────────────────────────────────────────── - /// Set the test session token. /// /// When set, `x-datadog-test-session-token: ` is injected on every request. - /// Replaces dd-trace-py's `AgentWriter.set_test_session_token` (`writer.py:754-755`). pub fn test_agent_session_token(self, token: impl Into) -> Self { todo!() } - // ── Timeout / retries ───────────────────────────────────────────────────── - /// Set the request timeout. /// /// Defaults to [`DEFAULT_TIMEOUT_MS`] (2 000 ms) when not set. @@ -177,24 +153,16 @@ impl AgentClientBuilder { /// Override the default retry configuration. /// - /// Defaults to [`default_retry_config`]: 2 retries, 100 ms initial delay, exponential - /// backoff with full jitter. + /// Defaults to [`default_retry_config`]. pub fn retry(self, config: RetryConfig) -> Self { todo!() } - // ── Language metadata ───────────────────────────────────────────────────── - - /// Set the language/runtime metadata injected into every request. - /// - /// Required. Drives `Datadog-Meta-Lang`, `Datadog-Meta-Lang-Version`, - /// `Datadog-Meta-Lang-Interpreter`, `Datadog-Meta-Tracer-Version`, and `User-Agent`. + /// Set the language/runtime metadata injected into every request. Required. pub fn language_metadata(self, meta: LanguageMetadata) -> Self { todo!() } - // ── Connection pooling ──────────────────────────────────────────────────── - /// Enable or disable HTTP keep-alive. Defaults to `false`. /// /// The Datadog agent has a low keep-alive timeout that causes "pipe closed" errors on every @@ -206,30 +174,19 @@ impl AgentClientBuilder { self } - // ── Compression ─────────────────────────────────────────────────────────── + // Compression // - // Not exposed in v1. Gzip compression (level 6, matching dd-trace-py's trace writer at + // Not exposed in this libv1. Gzip compression (level 6, matching dd-trace-py's trace writer at // `writer.py:490`) will be added in a follow-up once the core send paths are stable. // Per-method defaults (e.g. unconditional gzip for `send_pipeline_stats`) are already // baked in; only the opt-in client-level `gzip(level)` builder knob is deferred. - // ── Extra headers ───────────────────────────────────────────────────────── - - /// Merge additional headers into every request. - /// - /// Intended for `_DD_TRACE_WRITER_ADDITIONAL_HEADERS` in dd-trace-py. + /// Additional custom headers to inject. pub fn extra_headers(self, headers: HashMap) -> Self { todo!() } - // ── Build ───────────────────────────────────────────────────────────────── - /// Build the [`AgentClient`]. - /// - /// # Errors - /// - /// - [`BuildError::MissingTransport`] — no transport was configured. - /// - [`BuildError::MissingLanguageMetadata`] — no language metadata was configured. pub fn build(self) -> Result { todo!() } diff --git a/libdd-agent-client/src/client.rs b/libdd-agent-client/src/client.rs index 0b90845388..4e882fbedd 100644 --- a/libdd-agent-client/src/client.rs +++ b/libdd-agent-client/src/client.rs @@ -20,12 +20,10 @@ use crate::{ /// /// - Language metadata headers (`Datadog-Meta-Lang`, `Datadog-Meta-Lang-Version`, /// `Datadog-Meta-Lang-Interpreter`, `Datadog-Meta-Tracer-Version`) from the [`LanguageMetadata`] -/// supplied at build time. +/// supplied when creating the client. /// - `User-Agent` derived from [`LanguageMetadata::user_agent`]. /// - Container/entity-ID headers (`Datadog-Container-Id`, `Datadog-Entity-ID`, -/// `Datadog-External-Env`) read from `/proc/self/cgroup` at startup, equivalent to dd-trace-py's -/// `container.update_headers()` (`container.py:157-183`). -/// - `dd-api-key` when an API key was set (agentless mode). +/// `Datadog-External-Env`) read from `/proc/self/cgroup` at startup. /// - `x-datadog-test-session-token` when a test token was set. /// - Any extra headers registered via [`AgentClientBuilder::extra_headers`]. /// @@ -42,24 +40,12 @@ impl AgentClient { todo!() } - /// Send a serialised trace payload to the agent. - /// - /// # Automatically injected headers - /// - /// - `X-Datadog-Trace-Count: ` (per-payload — `writer.py:749-752`) - /// - `Datadog-Send-Real-Http-Status: true` — instructs the agent to return 429 when it drops a - /// payload, rather than silently returning 200. dd-trace-py never sets this header, causing - /// silent drops that are invisible to the caller. - /// - `Datadog-Client-Computed-Top-Level: yes` when [`TraceSendOptions::computed_top_level`] is - /// `true`. - /// - Language metadata headers + container headers (see type-level docs). - /// - `Content-Type` (`application/msgpack`) and endpoint path derived from `format`. - /// - `Content-Encoding: gzip` when compression is enabled. + /// Send a serialised trace payload to the agent with automatically injected headers. /// /// # Returns /// /// An [`AgentResponse`] with the HTTP status and the parsed `rate_by_service` sampling - /// rates from the agent response body (`writer.py:728-734`). + /// rates from the agent response body. pub async fn send_traces( &self, payload: Bytes, @@ -71,9 +57,6 @@ impl AgentClient { } /// Send span stats (APM concentrator buckets) to `/v0.6/stats`. - /// - /// `Content-Type` is always `application/msgpack`. Replaces the manual - /// `get_connection` + raw `PUT` in `SpanStatsProcessor._flush_stats` (`stats.py:204-228`). pub async fn send_stats(&self, payload: Bytes) -> Result<(), SendError> { todo!() } @@ -81,17 +64,12 @@ impl AgentClient { /// Send data-streams pipeline stats to `/v0.1/pipeline_stats`. /// /// The payload is **always** gzip-compressed regardless of the client-level compression - /// setting. This is a protocol requirement of the data-streams endpoint - /// (`datastreams/processor.py:132`) and must not be a caller responsibility. + /// setting. This is a protocol requirement of the data-streams endpoint. pub async fn send_pipeline_stats(&self, payload: Bytes) -> Result<(), SendError> { todo!() } /// Send a telemetry event to the agent's telemetry proxy (`telemetry/proxy/api/v2/apmtelemetry`). - /// - /// Per-request headers `DD-Telemetry-Request-Type`, `DD-Telemetry-API-Version`, and - /// `DD-Telemetry-Debug-Enabled` are injected automatically from `req`, replacing the - /// manual construction in `_TelemetryClient.get_headers` (`telemetry/writer.py:111-117`). pub async fn send_telemetry(&self, req: TelemetryRequest) -> Result<(), SendError> { todo!() } @@ -101,10 +79,6 @@ impl AgentClient { /// The agent forwards the request to `.datadoghq.com`. `subdomain` /// controls the target intake (injected as `X-Datadog-EVP-Subdomain`); `path` is the /// endpoint on that intake (e.g. `/api/v2/exposures`). - /// - /// In dd-trace-py's openfeature writer both values are hardcoded constants - /// (`openfeature/writer.py:24-27`), but they are independent routing dimensions and - /// must both be supplied by the caller. pub async fn send_evp_event( &self, subdomain: &str, @@ -117,10 +91,6 @@ impl AgentClient { /// Probe `GET /info` and return parsed agent capabilities. /// - /// Processes the `Datadog-Container-Tags-Hash` response header and exposes it as - /// [`AgentInfo::container_tags_hash`] rather than as a side-effect (as in dd-trace-py's - /// `process_info_headers` at `agent.py:17-23`). - /// /// Returns `Ok(None)` when the agent returns 404 (remote-config / info not supported). pub async fn agent_info(&self) -> Result, SendError> { todo!() diff --git a/libdd-agent-client/src/language_metadata.rs b/libdd-agent-client/src/language_metadata.rs index c4c277e214..f6347ff96e 100644 --- a/libdd-agent-client/src/language_metadata.rs +++ b/libdd-agent-client/src/language_metadata.rs @@ -5,32 +5,15 @@ /// Language and runtime metadata that is automatically injected into every request as /// `Datadog-Meta-*` headers and drives the `User-Agent` string. -/// -/// | Header | Field | -/// |---------------------------------|--------------------| -/// | `Datadog-Meta-Lang` | `language` | -/// | `Datadog-Meta-Lang-Version` | `language_version` | -/// | `Datadog-Meta-Lang-Interpreter` | `interpreter` | -/// | `Datadog-Meta-Tracer-Version` | `tracer_version` | -/// -/// These four headers are today manually assembled in four separate places in dd-trace-py: -/// `writer.py:638-644`, `writer.py:785-792`, `stats.py:113-117`, and -/// `datastreams/processor.py:128-133`. A single `LanguageMetadata` instance replaces all of them. -/// -/// # `User-Agent` -/// -/// [`LanguageMetadata::user_agent`] produces the string passed to -/// `Endpoint::to_request_builder(user_agent)`, so the `User-Agent` and the `Datadog-Meta-*` -/// headers share a single source of truth. #[derive(Debug, Clone)] pub struct LanguageMetadata { - /// Language name, e.g. `"python"`. + /// Value of `Datadog-Meta-Lang`, e.g. `"python"`. pub language: String, - /// Language runtime version, e.g. `"3.12.1"`. + /// Value of `Datadog-Meta-Lang-Version`, e.g. `"3.12.1"`. pub language_version: String, - /// Interpreter name, e.g. `"CPython"`. + /// Value of `Datadog-Meta-Lang-Interpreter`, e.g. `"CPython"`. pub interpreter: String, - /// Tracer library version, e.g. `"2.18.0"`. + /// Value of `Datadog-Meta-Tracer-Version`, e.g. `"2.18.0"`. pub tracer_version: String, } diff --git a/libdd-agent-client/src/lib.rs b/libdd-agent-client/src/lib.rs index 21256a0d71..c28a78edc9 100644 --- a/libdd-agent-client/src/lib.rs +++ b/libdd-agent-client/src/lib.rs @@ -1,27 +1,9 @@ // Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -//! `libdd-agent-client` — Datadog-agent-specialised HTTP client. -//! -//! This crate is **Milestone 2** of APMSP-2721 (libdd-http-client: Common HTTP Client for -//! Language Clients). It sits on top of the basic `libdd-http-client` primitives (Milestone 1) -//! and encapsulates all Datadog-agent-specific concerns, making them the **default** rather than -//! opt-in boilerplate that every subsystem must repeat. -//! -//! # What it replaces in dd-trace-py -//! -//! | Concern | dd-trace-py location | -//! |---------|----------------------| -//! | Language metadata headers | `writer.py:638-644`, `stats.py:113-117`, `datastreams/processor.py:128-133` | -//! | Container/entity-ID headers | `http.py:32-37`, `container.py:157-183` | -//! | Retry logic (fibonacci backoff) | `writer.py:245-249`, `stats.py:123-126`, `datastreams/processor.py:140-143` | -//! | Trace send with `X-Datadog-Trace-Count` | `writer.py:749-752` | -//! | `rate_by_service` parsing | `writer.py:728-734` | -//! | Stats send | `stats.py:204-228` | -//! | Pipeline stats send (always gzip) | `datastreams/processor.py:204-210` | -//! | Telemetry send with per-request headers | `telemetry/writer.py:111-129` | -//! | EVP event send | `openfeature/writer.py:114-117` | -//! | `GET /info` with typed result | `agent.py:17-46` | +//! This crate provides a Datadog-agent-specific HTTP clientm sitting on top of the basic +//! `libdd-http-client` primitives. The API is higher-level and makes agent-specific settings +//! (headers, etc.) the default rather than opt-in boilerplate. //! //! # Quick start //! @@ -58,9 +40,7 @@ //! //! # Fork safety //! -//! The underlying `libdd-http-client` uses `hickory-dns` by default — an in-process, fork-safe -//! DNS resolver. This protects against the class of DNS bugs that can occur in forking processes -//! (Django workers, Celery, PHP-FPM, etc.). +//! The underlying `libdd-http-client` uses the `hickory-dns` DNS resolver by default, which is in-process and fork-safe. pub mod agent_info; pub mod builder; diff --git a/libdd-agent-client/src/telemetry.rs b/libdd-agent-client/src/telemetry.rs index 58de85e1de..de1630b10b 100644 --- a/libdd-agent-client/src/telemetry.rs +++ b/libdd-agent-client/src/telemetry.rs @@ -5,9 +5,8 @@ /// A single telemetry event to send via [`crate::AgentClient::send_telemetry`]. /// -/// The three per-request headers — `DD-Telemetry-Request-Type`, `DD-Telemetry-API-Version`, and -/// `DD-Telemetry-Debug-Enabled` — are derived automatically from this struct, removing the -/// need for callers to build headers manually (as done in `telemetry/writer.py:111-117`). +/// The three per-request headers `DD-Telemetry-Request-Type`, `DD-Telemetry-API-Version`, and +/// `DD-Telemetry-Debug-Enabled` are derived automatically from the struct by the client. /// /// The client always routes to the agent telemetry proxy endpoint /// (`telemetry/proxy/api/v2/apmtelemetry`). @@ -17,11 +16,11 @@ pub struct TelemetryRequest { pub request_type: String, /// Value for the `DD-Telemetry-API-Version` header, e.g. `"v2"`. pub api_version: String, - /// When `true`, sets `DD-Telemetry-Debug-Enabled: true`. + /// Value for the `DD-Telemetry-Debug-Enabled` header. pub debug: bool, /// Pre-serialized JSON payload body. /// - /// The caller is responsible for serializing the event body to JSON before constructing - /// this struct. The client sends these bytes as-is with `Content-Type: application/json`. + /// The caller is responsible for serializing the event body to JSON before constructing this + /// struct. The client sends these bytes as `Content-Type: application/json`. pub body: bytes::Bytes, } diff --git a/libdd-agent-client/src/traces.rs b/libdd-agent-client/src/traces.rs index 9879a558b3..28827b87f4 100644 --- a/libdd-agent-client/src/traces.rs +++ b/libdd-agent-client/src/traces.rs @@ -11,10 +11,9 @@ use std::collections::HashMap; /// /// # Format selection /// -/// The caller is currently responsible for choosing the format. In practice this means -/// starting with [`TraceFormat::MsgpackV5`] and downgrading to [`TraceFormat::MsgpackV4`] -/// when the agent returns 404 or 415 (e.g. on Windows, or when AppSec/IAST is active) — -/// the same sticky downgrade that dd-trace-py performs in `AgentWriter` (`writer.py`). +/// The caller is currently responsible for choosing the format. In practice this means starting +/// with [`TraceFormat::MsgpackV5`] and downgrading to [`TraceFormat::MsgpackV4`] when the agent +/// returns 404 or 415 (e.g. on Windows, or when AppSec/IAST is active). /// /// In a future version this negotiation may be moved into the client itself so that format /// selection becomes automatic and callers no longer need to track the downgrade state. @@ -32,9 +31,7 @@ pub struct TraceSendOptions { /// When `true`, appends `Datadog-Client-Computed-Top-Level: yes`. /// /// Signals to the agent that the client has already marked top-level spans, allowing the agent - /// to skip its own top-level computation. In dd-trace-py this header is always set - /// (`writer.py:643`); here it is opt-in so that callers that do not compute top-level spans - /// can omit it. + /// to skip its own top-level computation. pub computed_top_level: bool, } @@ -44,6 +41,6 @@ pub struct AgentResponse { /// HTTP status code returned by the agent. pub status: u16, /// Per-service sampling rates parsed from the `rate_by_service` field of the agent response - /// body, if present. Mirrors the JSON parsing done in dd-trace-py at `writer.py:728-734`. + /// body, if present. pub rate_by_service: Option>, } From e2430c7bf17ee1e487e1fdf1bd886c001cffe08c Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 21 Apr 2026 11:11:41 +0200 Subject: [PATCH 08/32] implement: fill in all todo!() placeholders in libdd-agent-client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - LanguageMetadata::new() and user_agent() (dd-trace-/) - AgentClientBuilder: all builder methods, AutoDetect resolution, timeout_from_env(), container header injection from /proc/self/cgroup, pre-computed static headers at build time - AgentClient: send_traces (v0.4/v0.5, trace-count, real-http-status), send_stats, send_pipeline_stats (unconditional gzip level 6), send_telemetry, send_evp_event, agent_info (404 → None) - BuildError::HttpClient variant for underlying client build failures - Integration test suite with httpmock covering all send methods Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 3 + libdd-agent-client/Cargo.toml | 3 + libdd-agent-client/src/builder.rs | 319 +++++++++++++- libdd-agent-client/src/client.rs | 293 +++++++++++- libdd-agent-client/src/error.rs | 3 + libdd-agent-client/src/language_metadata.rs | 35 +- libdd-agent-client/tests/integration.rs | 464 ++++++++++++++++++++ 7 files changed, 1088 insertions(+), 32 deletions(-) create mode 100644 libdd-agent-client/tests/integration.rs diff --git a/Cargo.lock b/Cargo.lock index 998651276c..53f08d7f11 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2815,7 +2815,10 @@ name = "libdd-agent-client" version = "31.0.0" dependencies = [ "bytes", + "flate2", + "httpmock", "libdd-http-client", + "rustls", "serde", "serde_json", "thiserror 2.0.17", diff --git a/libdd-agent-client/Cargo.toml b/libdd-agent-client/Cargo.toml index f077e1786c..dd65c36fd6 100644 --- a/libdd-agent-client/Cargo.toml +++ b/libdd-agent-client/Cargo.toml @@ -17,6 +17,7 @@ bench = false [dependencies] bytes = "1.4" +flate2 = "1" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "2" @@ -24,4 +25,6 @@ tokio = { version = "1.23", features = ["rt"] } libdd-http-client = { path = "../libdd-http-client" } [dev-dependencies] +httpmock = "0.8.0-alpha.1" +rustls = { version = "0.23", default-features = false, features = ["ring"] } tokio = { version = "1.23", features = ["rt", "macros"] } diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index ec3a2252d1..fbabe04914 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -16,7 +16,10 @@ pub const DEFAULT_TIMEOUT_MS: u64 = 2_000; /// Default retry configuration: 2 retries (3 total attempts), 100 ms initial delay, /// exponential backoff with full jitter. pub fn default_retry_config() -> RetryConfig { - todo!() + RetryConfig::new() + .max_retries(2) + .initial_delay(Duration::from_millis(100)) + .with_jitter(true) } /// Transport configuration for the agent client. @@ -62,7 +65,10 @@ pub enum AgentTransport { impl Default for AgentTransport { fn default() -> Self { - todo!() + AgentTransport::Http { + host: "localhost".to_string(), + port: 8126, + } } } @@ -95,29 +101,33 @@ pub struct AgentClientBuilder { impl AgentClientBuilder { /// Create a new builder with default settings. pub fn new() -> Self { - todo!() + Self::default() } /// Set the transport configuration. - pub fn transport(self, transport: AgentTransport) -> Self { - todo!() + pub fn transport(mut self, transport: AgentTransport) -> Self { + self.transport = Some(transport); + self } /// Convenience: HTTP over TCP. pub fn http(self, host: impl Into, port: u16) -> Self { - todo!() + self.transport(AgentTransport::Http { + host: host.into(), + port, + }) } /// Convenience: Unix Domain Socket. #[cfg(unix)] pub fn unix_socket(self, path: impl Into) -> Self { - todo!() + self.transport(AgentTransport::UnixSocket { path: path.into() }) } /// Convenience: Windows Named Pipe. #[cfg(windows)] pub fn windows_named_pipe(self, path: impl Into) -> Self { - todo!() + self.transport(AgentTransport::NamedPipe { path: path.into() }) } /// Convenience: auto-detect transport (UDS if socket file exists, else HTTP). @@ -128,39 +138,53 @@ impl AgentClientBuilder { fallback_host: impl Into, fallback_port: u16, ) -> Self { - todo!() + self.transport(AgentTransport::AutoDetect { + uds_path: uds_path.into(), + fallback_host: fallback_host.into(), + fallback_port, + }) } /// Set the test session token. /// /// When set, `x-datadog-test-session-token: ` is injected on every request. - pub fn test_agent_session_token(self, token: impl Into) -> Self { - todo!() + pub fn test_agent_session_token(mut self, token: impl Into) -> Self { + self.test_token = Some(token.into()); + self } /// Set the request timeout. /// /// Defaults to [`DEFAULT_TIMEOUT_MS`] (2 000 ms) when not set. - pub fn timeout(self, timeout: Duration) -> Self { - todo!() + pub fn timeout(mut self, timeout: Duration) -> Self { + self.timeout = Some(timeout); + self } /// Read the timeout from `DD_TRACE_AGENT_TIMEOUT_SECONDS`, falling back to /// [`DEFAULT_TIMEOUT_MS`] if the variable is unset or unparseable. - pub fn timeout_from_env(self) -> Self { - todo!() + pub fn timeout_from_env(mut self) -> Self { + let timeout = std::env::var("DD_TRACE_AGENT_TIMEOUT_SECONDS") + .ok() + .and_then(|v| v.parse::().ok()) + .map(|secs| Duration::from_millis((secs * 1000.0) as u64)) + .unwrap_or(Duration::from_millis(DEFAULT_TIMEOUT_MS)); + self.timeout = Some(timeout); + self } /// Override the default retry configuration. /// /// Defaults to [`default_retry_config`]. - pub fn retry(self, config: RetryConfig) -> Self { - todo!() + pub fn retry(mut self, config: RetryConfig) -> Self { + self.retry = Some(config); + self } /// Set the language/runtime metadata injected into every request. Required. - pub fn language_metadata(self, meta: LanguageMetadata) -> Self { - todo!() + pub fn language_metadata(mut self, meta: LanguageMetadata) -> Self { + self.language = Some(meta); + self } /// Enable or disable HTTP keep-alive. Defaults to `false`. @@ -182,12 +206,263 @@ impl AgentClientBuilder { // baked in; only the opt-in client-level `gzip(level)` builder knob is deferred. /// Additional custom headers to inject. - pub fn extra_headers(self, headers: HashMap) -> Self { - todo!() + pub fn extra_headers(mut self, headers: HashMap) -> Self { + self.extra_headers = headers; + self } /// Build the [`AgentClient`]. pub fn build(self) -> Result { - todo!() + let transport = self.transport.ok_or(BuildError::MissingTransport)?; + let language = self.language.ok_or(BuildError::MissingLanguageMetadata)?; + let timeout = self + .timeout + .unwrap_or(Duration::from_millis(DEFAULT_TIMEOUT_MS)); + let retry = self.retry.unwrap_or_else(default_retry_config); + + // Resolve AutoDetect to a concrete transport. + let resolved = resolve_transport(transport); + + // Build the underlying HTTP client. + let http = build_http_client(resolved, timeout, retry) + .map_err(|e| BuildError::HttpClient(e.to_string()))?; + + // Pre-compute all static headers that are injected on every request. + let static_headers = build_static_headers(&language, self.test_token, self.extra_headers); + + Ok(AgentClient::new(http, static_headers)) + } +} + +/// A resolved (concrete) transport — no AutoDetect. +pub(crate) enum ResolvedTransport { + Http { host: String, port: u16 }, + #[cfg(unix)] + UnixSocket { path: std::path::PathBuf }, + #[cfg(windows)] + NamedPipe { path: std::ffi::OsString }, +} + +/// Resolve `AutoDetect` at build time; other variants pass through unchanged. +fn resolve_transport(transport: AgentTransport) -> ResolvedTransport { + match transport { + AgentTransport::Http { host, port } => ResolvedTransport::Http { host, port }, + #[cfg(unix)] + AgentTransport::UnixSocket { path } => ResolvedTransport::UnixSocket { path }, + #[cfg(windows)] + AgentTransport::NamedPipe { path } => ResolvedTransport::NamedPipe { path }, + #[cfg(unix)] + AgentTransport::AutoDetect { + uds_path, + fallback_host, + fallback_port, + } => { + if uds_path.exists() { + ResolvedTransport::UnixSocket { path: uds_path } + } else { + ResolvedTransport::Http { + host: fallback_host, + port: fallback_port, + } + } + } + } +} + +/// Derive the base URL for a resolved transport. +pub(crate) fn base_url_for(transport: &ResolvedTransport) -> String { + match transport { + ResolvedTransport::Http { host, port } => format!("http://{}:{}", host, port), + #[cfg(unix)] + ResolvedTransport::UnixSocket { .. } => "http://localhost".to_string(), + #[cfg(windows)] + ResolvedTransport::NamedPipe { .. } => "http://localhost".to_string(), + } +} + +fn build_http_client( + transport: ResolvedTransport, + timeout: Duration, + retry: RetryConfig, +) -> Result { + let base_url = base_url_for(&transport); + let mut builder = libdd_http_client::HttpClient::builder() + .base_url(base_url) + .timeout(timeout) + // HTTP errors are handled by each send method, not by the underlying client. + // This allows methods like `agent_info` to interpret 404 as Ok(None) rather than + // an error, and avoids retrying on HTTP 4xx/5xx. + .treat_http_errors_as_errors(false) + .retry(retry); + + match transport { + ResolvedTransport::Http { .. } => {} + #[cfg(unix)] + ResolvedTransport::UnixSocket { path } => { + builder = builder.unix_socket(path); + } + #[cfg(windows)] + ResolvedTransport::NamedPipe { path } => { + builder = builder.windows_named_pipe(path); + } + } + + builder.build() +} + +fn build_static_headers( + language: &LanguageMetadata, + test_token: Option, + extra_headers: HashMap, +) -> Vec<(String, String)> { + let mut headers = Vec::new(); + + headers.push(("Datadog-Meta-Lang".to_string(), language.language.clone())); + headers.push(( + "Datadog-Meta-Lang-Version".to_string(), + language.language_version.clone(), + )); + headers.push(( + "Datadog-Meta-Lang-Interpreter".to_string(), + language.interpreter.clone(), + )); + headers.push(( + "Datadog-Meta-Tracer-Version".to_string(), + language.tracer_version.clone(), + )); + headers.push(("User-Agent".to_string(), language.user_agent())); + + if let Some(token) = test_token { + headers.push(("x-datadog-test-session-token".to_string(), token)); + } + + headers.extend(container_headers()); + headers.extend(extra_headers); + + headers +} + +/// Read container / entity-ID headers from the host environment. +/// +/// On Linux, parses `/proc/self/cgroup` to extract the container ID and injects +/// `Datadog-Container-Id` and `Datadog-Entity-ID`. Always injects `Datadog-External-Env` +/// when `DD_EXTERNAL_ENV` is set. +fn container_headers() -> Vec<(String, String)> { + let mut headers = Vec::new(); + + if let Ok(env) = std::env::var("DD_EXTERNAL_ENV") { + if !env.is_empty() { + headers.push(("Datadog-External-Env".to_string(), env)); + } + } + + #[cfg(target_os = "linux")] + if let Some(container_id) = read_container_id_from_cgroup() { + headers.push(("Datadog-Container-Id".to_string(), container_id.clone())); + headers.push(("Datadog-Entity-ID".to_string(), format!("ci-{}", container_id))); + } + + headers +} + +/// Parse a 64-character hex container ID from `/proc/self/cgroup`. +/// +/// cgroup v1 paths end with the container ID, e.g.: +/// `12:blkio:/docker/abc123...64hex...` +#[cfg(target_os = "linux")] +fn read_container_id_from_cgroup() -> Option { + let content = std::fs::read_to_string("/proc/self/cgroup").ok()?; + for line in content.lines() { + // Each cgroup line is: :: + let path = line.splitn(3, ':').nth(2)?; + // Container ID is a 64-char hex segment at the end of the cgroup path. + for segment in path.split('/').rev() { + if segment.len() == 64 && segment.bytes().all(|b| b.is_ascii_hexdigit()) { + return Some(segment.to_string()); + } + } + } + None +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn default_transport_is_localhost_8126() { + let t = AgentTransport::default(); + match t { + AgentTransport::Http { host, port } => { + assert_eq!(host, "localhost"); + assert_eq!(port, 8126); + } + #[allow(unreachable_patterns)] + _ => panic!("unexpected default transport"), + } + } + + #[test] + fn default_retry_config_is_constructable() { + // Just verify default_retry_config() doesn't panic. + let _cfg = default_retry_config(); + } + + #[test] + fn builder_new_is_default() { + let b = AgentClientBuilder::new(); + assert!(b.transport.is_none()); + assert!(b.language.is_none()); + assert!(!b.keep_alive); + } + + #[test] + fn build_fails_without_transport() { + let result = AgentClientBuilder::new() + .language_metadata(LanguageMetadata::new("python", "3.12", "CPython", "2.0")) + .build(); + assert!(matches!(result, Err(BuildError::MissingTransport))); + } + + #[test] + fn build_fails_without_language_metadata() { + let result = AgentClientBuilder::new().http("localhost", 8126).build(); + assert!(matches!(result, Err(BuildError::MissingLanguageMetadata))); + } + + #[test] + fn build_succeeds_with_required_fields() { + let _ = rustls::crypto::ring::default_provider().install_default(); + let result = AgentClientBuilder::new() + .http("localhost", 8126) + .language_metadata(LanguageMetadata::new("python", "3.12", "CPython", "2.0")) + .build(); + assert!(result.is_ok()); + } + + #[test] + fn timeout_from_env_uses_default_when_unset() { + std::env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); + let b = AgentClientBuilder::new().timeout_from_env(); + assert_eq!( + b.timeout, + Some(Duration::from_millis(DEFAULT_TIMEOUT_MS)) + ); + } + + #[test] + fn timeout_from_env_parses_env_var() { + std::env::set_var("DD_TRACE_AGENT_TIMEOUT_SECONDS", "5"); + let b = AgentClientBuilder::new().timeout_from_env(); + assert_eq!(b.timeout, Some(Duration::from_secs(5))); + std::env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); + } + + #[test] + fn extra_headers_stored() { + let mut headers = HashMap::new(); + headers.insert("X-Custom".to_string(), "value".to_string()); + let b = AgentClientBuilder::new().extra_headers(headers); + assert_eq!(b.extra_headers.get("X-Custom").map(|s| s.as_str()), Some("value")); } } diff --git a/libdd-agent-client/src/client.rs b/libdd-agent-client/src/client.rs index 4e882fbedd..7e6e1f5f0a 100644 --- a/libdd-agent-client/src/client.rs +++ b/libdd-agent-client/src/client.rs @@ -3,7 +3,11 @@ //! [`AgentClient`] and its send methods. +use std::collections::HashMap; + use bytes::Bytes; +use flate2::{write::GzEncoder, Compression}; +use std::io::Write as _; use crate::{ agent_info::AgentInfo, @@ -31,13 +35,27 @@ use crate::{ /// /// [`LanguageMetadata`]: crate::LanguageMetadata pub struct AgentClient { - // Opaque — fields are an implementation detail. + http: libdd_http_client::HttpClient, + base_url: String, + static_headers: Vec<(String, String)>, } impl AgentClient { + pub(crate) fn new( + http: libdd_http_client::HttpClient, + static_headers: Vec<(String, String)>, + ) -> Self { + let base_url = http.config().base_url().to_string(); + Self { + http, + base_url, + static_headers, + } + } + /// Create a new [`AgentClientBuilder`]. pub fn builder() -> AgentClientBuilder { - todo!() + AgentClientBuilder::new() } /// Send a serialised trace payload to the agent with automatically injected headers. @@ -53,12 +71,59 @@ impl AgentClient { format: TraceFormat, opts: TraceSendOptions, ) -> Result { - todo!() + let (path, content_type) = match format { + TraceFormat::MsgpackV5 => ("/v0.5/traces", "application/msgpack"), + TraceFormat::MsgpackV4 => ("/v0.4/traces", "application/msgpack"), + }; + + let url = format!("{}{}", self.base_url, path); + let mut request = + libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Put, url) + .with_body(payload); + + for (k, v) in &self.static_headers { + request = request.with_header(k, v); + } + + request = request + .with_header("Content-Type", content_type) + .with_header("X-Datadog-Trace-Count", trace_count.to_string()) + .with_header("Datadog-Send-Real-Http-Status", "true"); + + if opts.computed_top_level { + request = request.with_header("Datadog-Client-Computed-Top-Level", "yes"); + } + + let response = self.http.send(request).await.map_err(map_http_error)?; + + if response.status_code() >= 400 { + return Err(SendError::HttpError { + status: response.status_code(), + body: response.body().clone(), + }); + } + + let rate_by_service = parse_rate_by_service(response.body()); + Ok(AgentResponse { + status: response.status_code(), + rate_by_service, + }) } /// Send span stats (APM concentrator buckets) to `/v0.6/stats`. pub async fn send_stats(&self, payload: Bytes) -> Result<(), SendError> { - todo!() + let url = format!("{}/v0.6/stats", self.base_url); + let mut request = + libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Put, url) + .with_body(payload); + + for (k, v) in &self.static_headers { + request = request.with_header(k, v); + } + request = request.with_header("Content-Type", "application/msgpack"); + + let response = self.http.send(request).await.map_err(map_http_error)?; + check_status(response) } /// Send data-streams pipeline stats to `/v0.1/pipeline_stats`. @@ -66,12 +131,45 @@ impl AgentClient { /// The payload is **always** gzip-compressed regardless of the client-level compression /// setting. This is a protocol requirement of the data-streams endpoint. pub async fn send_pipeline_stats(&self, payload: Bytes) -> Result<(), SendError> { - todo!() + let compressed = gzip_compress(payload)?; + + let url = format!("{}/v0.1/pipeline_stats", self.base_url); + let mut request = + libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Put, url) + .with_body(compressed); + + for (k, v) in &self.static_headers { + request = request.with_header(k, v); + } + request = request + .with_header("Content-Type", "application/msgpack") + .with_header("Content-Encoding", "gzip"); + + let response = self.http.send(request).await.map_err(map_http_error)?; + check_status(response) } /// Send a telemetry event to the agent's telemetry proxy (`telemetry/proxy/api/v2/apmtelemetry`). pub async fn send_telemetry(&self, req: TelemetryRequest) -> Result<(), SendError> { - todo!() + let url = format!("{}/telemetry/proxy/api/v2/apmtelemetry", self.base_url); + let mut request = + libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Post, url) + .with_body(req.body); + + for (k, v) in &self.static_headers { + request = request.with_header(k, v); + } + request = request + .with_header("Content-Type", "application/json") + .with_header("DD-Telemetry-Request-Type", &req.request_type) + .with_header("DD-Telemetry-API-Version", &req.api_version) + .with_header( + "DD-Telemetry-Debug-Enabled", + if req.debug { "true" } else { "false" }, + ); + + let response = self.http.send(request).await.map_err(map_http_error)?; + check_status(response) } /// Send an event via the agent's EVP (Event Platform) proxy. @@ -86,13 +184,192 @@ impl AgentClient { payload: Bytes, content_type: &str, ) -> Result<(), SendError> { - todo!() + let url = format!("{}{}", self.base_url, path); + let mut request = + libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Post, url) + .with_body(payload); + + for (k, v) in &self.static_headers { + request = request.with_header(k, v); + } + request = request + .with_header("Content-Type", content_type) + .with_header("X-Datadog-EVP-Subdomain", subdomain); + + let response = self.http.send(request).await.map_err(map_http_error)?; + check_status(response) } /// Probe `GET /info` and return parsed agent capabilities. /// /// Returns `Ok(None)` when the agent returns 404 (remote-config / info not supported). pub async fn agent_info(&self) -> Result, SendError> { - todo!() + let url = format!("{}/info", self.base_url); + let mut request = + libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Get, url); + + for (k, v) in &self.static_headers { + request = request.with_header(k, v); + } + + let response = self.http.send(request).await.map_err(map_http_error)?; + + if response.status_code() == 404 { + return Ok(None); + } + + if response.status_code() >= 400 { + return Err(SendError::HttpError { + status: response.status_code(), + body: response.body().clone(), + }); + } + + let container_tags_hash = header_value(response.headers(), "datadog-container-tags-hash"); + let state_hash = header_value(response.headers(), "datadog-agent-state"); + + #[derive(serde::Deserialize)] + struct InfoResponse { + version: Option, + endpoints: Option>, + client_drop_p0s: Option, + config: Option, + } + + let info: InfoResponse = serde_json::from_slice(response.body()) + .map_err(|e| SendError::Encoding(e.to_string()))?; + + Ok(Some(AgentInfo { + endpoints: info.endpoints.unwrap_or_default(), + client_drop_p0s: info.client_drop_p0s.unwrap_or(false), + config: info.config.unwrap_or(serde_json::Value::Null), + version: info.version, + container_tags_hash, + state_hash, + })) + } +} + +/// Parse `rate_by_service` from an agent trace response body. +fn parse_rate_by_service(body: &Bytes) -> Option> { + #[derive(serde::Deserialize)] + struct TraceResponse { + rate_by_service: Option>, + } + + serde_json::from_slice::(body) + .ok() + .and_then(|r| r.rate_by_service) +} + +/// Return `Ok(())` for 2xx, or `Err(SendError::HttpError)` for anything else. +fn check_status(response: libdd_http_client::HttpResponse) -> Result<(), SendError> { + if response.status_code() >= 400 { + Err(SendError::HttpError { + status: response.status_code(), + body: response.body().clone(), + }) + } else { + Ok(()) } } + +/// Case-insensitive lookup of a response header value. +fn header_value(headers: &[(String, String)], name: &str) -> Option { + headers + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case(name)) + .map(|(_, v)| v.clone()) +} + +/// Gzip-compress `payload` at level 6 (matching dd-trace-py's trace writer). +fn gzip_compress(payload: Bytes) -> Result { + let mut encoder = GzEncoder::new(Vec::new(), Compression::new(6)); + encoder + .write_all(&payload) + .map_err(|e| SendError::Encoding(e.to_string()))?; + let compressed = encoder + .finish() + .map_err(|e| SendError::Encoding(e.to_string()))?; + Ok(Bytes::from(compressed)) +} + +/// Map a [`libdd_http_client::HttpClientError`] to a [`SendError`]. +fn map_http_error(e: libdd_http_client::HttpClientError) -> SendError { + match e { + libdd_http_client::HttpClientError::ConnectionFailed(s) => SendError::Transport( + std::io::Error::new(std::io::ErrorKind::ConnectionRefused, s), + ), + libdd_http_client::HttpClientError::TimedOut => SendError::Transport( + std::io::Error::new(std::io::ErrorKind::TimedOut, "request timed out"), + ), + libdd_http_client::HttpClientError::IoError(s) => { + SendError::Transport(std::io::Error::new(std::io::ErrorKind::Other, s)) + } + libdd_http_client::HttpClientError::InvalidConfig(s) => { + SendError::Transport(std::io::Error::new(std::io::ErrorKind::InvalidInput, s)) + } + libdd_http_client::HttpClientError::RequestFailed { status, body } => { + SendError::HttpError { + status, + body: Bytes::from(body), + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::{AgentClient, LanguageMetadata}; + + fn ensure_crypto_provider() { + let _ = rustls::crypto::ring::default_provider().install_default(); + } + + fn test_client(port: u16) -> AgentClient { + ensure_crypto_provider(); + AgentClient::builder() + .http("localhost", port) + .language_metadata(LanguageMetadata::new("python", "3.12", "CPython", "2.0")) + .build() + .unwrap() + } + + #[test] + fn builder_roundtrip() { + let client = test_client(8126); + assert!(client.base_url.contains("localhost")); + } + + #[test] + fn static_headers_contain_language_metadata() { + let client = test_client(8126); + let keys: Vec<&str> = client.static_headers.iter().map(|(k, _)| k.as_str()).collect(); + assert!(keys.contains(&"Datadog-Meta-Lang")); + assert!(keys.contains(&"Datadog-Meta-Lang-Version")); + assert!(keys.contains(&"User-Agent")); + } + + #[test] + fn gzip_compress_produces_valid_gzip() { + let input = Bytes::from_static(b"hello world"); + let compressed = gzip_compress(input).unwrap(); + // gzip magic bytes: 0x1f 0x8b + assert_eq!(&compressed[..2], &[0x1f, 0x8b]); + } + + #[test] + fn parse_rate_by_service_valid_json() { + let body = Bytes::from(r#"{"rate_by_service":{"service:env":0.5}}"#); + let rates = parse_rate_by_service(&body).unwrap(); + assert_eq!(rates.get("service:env"), Some(&0.5)); + } + + #[test] + fn parse_rate_by_service_absent_field() { + let body = Bytes::from(r#"{"other":"value"}"#); + assert!(parse_rate_by_service(&body).is_none()); + } + +} diff --git a/libdd-agent-client/src/error.rs b/libdd-agent-client/src/error.rs index e9e6dd9355..413d3a06d1 100644 --- a/libdd-agent-client/src/error.rs +++ b/libdd-agent-client/src/error.rs @@ -15,6 +15,9 @@ pub enum BuildError { /// No language metadata was configured. #[error("language metadata is required")] MissingLanguageMetadata, + /// The underlying HTTP client could not be constructed. + #[error("HTTP client error: {0}")] + HttpClient(String), } /// Errors that can occur when sending a request via [`crate::AgentClient`]. diff --git a/libdd-agent-client/src/language_metadata.rs b/libdd-agent-client/src/language_metadata.rs index f6347ff96e..0e8d912c70 100644 --- a/libdd-agent-client/src/language_metadata.rs +++ b/libdd-agent-client/src/language_metadata.rs @@ -25,13 +25,44 @@ impl LanguageMetadata { interpreter: impl Into, tracer_version: impl Into, ) -> Self { - todo!() + Self { + language: language.into(), + language_version: language_version.into(), + interpreter: interpreter.into(), + tracer_version: tracer_version.into(), + } } /// Produces the `User-Agent` string passed to `Endpoint::to_request_builder()`. /// /// Format: `dd-trace-/`, e.g. `dd-trace-python/2.18.0`. pub fn user_agent(&self) -> String { - todo!() + format!("dd-trace-{}/{}", self.language, self.tracer_version) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn new_stores_fields() { + let m = LanguageMetadata::new("python", "3.12.1", "CPython", "2.18.0"); + assert_eq!(m.language, "python"); + assert_eq!(m.language_version, "3.12.1"); + assert_eq!(m.interpreter, "CPython"); + assert_eq!(m.tracer_version, "2.18.0"); + } + + #[test] + fn user_agent_format() { + let m = LanguageMetadata::new("python", "3.12.1", "CPython", "2.18.0"); + assert_eq!(m.user_agent(), "dd-trace-python/2.18.0"); + } + + #[test] + fn user_agent_ruby() { + let m = LanguageMetadata::new("ruby", "3.2.0", "MRI", "1.13.0"); + assert_eq!(m.user_agent(), "dd-trace-ruby/1.13.0"); } } diff --git a/libdd-agent-client/tests/integration.rs b/libdd-agent-client/tests/integration.rs new file mode 100644 index 0000000000..26102a7784 --- /dev/null +++ b/libdd-agent-client/tests/integration.rs @@ -0,0 +1,464 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +//! Integration tests using a mock HTTP server. + +use bytes::Bytes; +use httpmock::prelude::*; +use libdd_agent_client::{ + AgentClient, LanguageMetadata, TelemetryRequest, TraceFormat, TraceSendOptions, +}; + +fn ensure_crypto_provider() { + let _ = rustls::crypto::ring::default_provider().install_default(); +} + +fn client_for(server: &MockServer) -> AgentClient { + ensure_crypto_provider(); + AgentClient::builder() + .http("localhost", server.port()) + .language_metadata(LanguageMetadata::new("python", "3.12.1", "CPython", "2.18.0")) + .build() + .expect("client build failed") +} + +// ── send_traces ──────────────────────────────────────────────────────────────── + +#[tokio::test] +async fn send_traces_v5_puts_to_correct_endpoint() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT).path("/v0.5/traces"); + then.status(200).body(r#"{"rate_by_service":{}}"#); + }); + + let client = client_for(&server); + let resp = client + .send_traces( + Bytes::from_static(b"\x91\x90"), + 1, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + mock.assert(); + assert_eq!(resp.status, 200); +} + +#[tokio::test] +async fn send_traces_v4_puts_to_v4_endpoint() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT).path("/v0.4/traces"); + then.status(200).body(r#"{}"#); + }); + + let client = client_for(&server); + client + .send_traces( + Bytes::from_static(b"\x91\x90"), + 1, + TraceFormat::MsgpackV4, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn send_traces_injects_trace_count_header() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.5/traces") + .header("X-Datadog-Trace-Count", "42"); + then.status(200).body(r#"{}"#); + }); + + let client = client_for(&server); + client + .send_traces( + Bytes::from_static(b"\x91\x90"), + 42, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn send_traces_injects_send_real_http_status_header() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.5/traces") + .header("Datadog-Send-Real-Http-Status", "true"); + then.status(200).body(r#"{}"#); + }); + + let client = client_for(&server); + client + .send_traces( + Bytes::from_static(b""), + 0, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn send_traces_computed_top_level_injects_header() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.5/traces") + .header("Datadog-Client-Computed-Top-Level", "yes"); + then.status(200).body(r#"{}"#); + }); + + let client = client_for(&server); + client + .send_traces( + Bytes::from_static(b""), + 0, + TraceFormat::MsgpackV5, + TraceSendOptions { + computed_top_level: true, + }, + ) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn send_traces_parses_rate_by_service() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(PUT).path("/v0.5/traces"); + then.status(200) + .body(r#"{"rate_by_service":{"service:env":0.75}}"#); + }); + + let client = client_for(&server); + let resp = client + .send_traces( + Bytes::from_static(b""), + 0, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + assert_eq!( + resp.rate_by_service + .as_ref() + .and_then(|m| m.get("service:env")), + Some(&0.75) + ); +} + +#[tokio::test] +async fn send_traces_returns_http_error_on_5xx() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(PUT).path("/v0.5/traces"); + then.status(503).body("overloaded"); + }); + + let client = client_for(&server); + let err = client + .send_traces( + Bytes::from_static(b""), + 0, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap_err(); + + assert!(matches!( + err, + libdd_agent_client::SendError::HttpError { status: 503, .. } + )); +} + +// ── send_stats ───────────────────────────────────────────────────────────────── + +#[tokio::test] +async fn send_stats_puts_to_v06_stats() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT).path("/v0.6/stats"); + then.status(200).body(""); + }); + + let client = client_for(&server); + client + .send_stats(Bytes::from_static(b"\x80")) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn send_stats_sets_msgpack_content_type() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.6/stats") + .header("Content-Type", "application/msgpack"); + then.status(200).body(""); + }); + + let client = client_for(&server); + client + .send_stats(Bytes::from_static(b"\x80")) + .await + .unwrap(); + + mock.assert(); +} + +// ── send_pipeline_stats ──────────────────────────────────────────────────────── + +#[tokio::test] +async fn send_pipeline_stats_puts_to_correct_endpoint() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT).path("/v0.1/pipeline_stats"); + then.status(200).body(""); + }); + + let client = client_for(&server); + client + .send_pipeline_stats(Bytes::from_static(b"\x80")) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn send_pipeline_stats_sets_gzip_encoding() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.1/pipeline_stats") + .header("Content-Encoding", "gzip"); + then.status(200).body(""); + }); + + let client = client_for(&server); + client + .send_pipeline_stats(Bytes::from_static(b"\x80")) + .await + .unwrap(); + + mock.assert(); +} + +// ── send_telemetry ───────────────────────────────────────────────────────────── + +#[tokio::test] +async fn send_telemetry_posts_to_telemetry_proxy() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(POST) + .path("/telemetry/proxy/api/v2/apmtelemetry"); + then.status(202).body(""); + }); + + let client = client_for(&server); + client + .send_telemetry(TelemetryRequest { + request_type: "app-started".to_string(), + api_version: "v2".to_string(), + debug: false, + body: Bytes::from_static(b"{}"), + }) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn send_telemetry_injects_per_request_headers() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(POST) + .path("/telemetry/proxy/api/v2/apmtelemetry") + .header("DD-Telemetry-Request-Type", "app-started") + .header("DD-Telemetry-API-Version", "v2") + .header("DD-Telemetry-Debug-Enabled", "false"); + then.status(202).body(""); + }); + + let client = client_for(&server); + client + .send_telemetry(TelemetryRequest { + request_type: "app-started".to_string(), + api_version: "v2".to_string(), + debug: false, + body: Bytes::from_static(b"{}"), + }) + .await + .unwrap(); + + mock.assert(); +} + +// ── send_evp_event ───────────────────────────────────────────────────────────── + +#[tokio::test] +async fn send_evp_event_posts_to_path_with_subdomain_header() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(POST) + .path("/api/v2/exposures") + .header("X-Datadog-EVP-Subdomain", "event-platform-intake"); + then.status(200).body(""); + }); + + let client = client_for(&server); + client + .send_evp_event( + "event-platform-intake", + "/api/v2/exposures", + Bytes::from_static(b"{}"), + "application/json", + ) + .await + .unwrap(); + + mock.assert(); +} + +// ── agent_info ───────────────────────────────────────────────────────────────── + +#[tokio::test] +async fn agent_info_parses_info_response() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/info"); + then.status(200).body( + r#"{ + "version": "7.50.0", + "endpoints": ["/v0.4/traces", "/v0.5/traces"], + "client_drop_p0s": true, + "config": {} + }"#, + ); + }); + + let client = client_for(&server); + let info = client.agent_info().await.unwrap().expect("expected Some"); + + assert_eq!(info.version.as_deref(), Some("7.50.0")); + assert!(info.endpoints.contains(&"/v0.5/traces".to_string())); + assert!(info.client_drop_p0s); +} + +#[tokio::test] +async fn agent_info_returns_none_on_404() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/info"); + then.status(404).body("not found"); + }); + + let client = client_for(&server); + let result = client.agent_info().await.unwrap(); + assert!(result.is_none()); +} + +#[tokio::test] +async fn agent_info_extracts_container_tags_hash_header() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/info"); + then.status(200) + .header("Datadog-Container-Tags-Hash", "abc123") + .body(r#"{"endpoints":[],"client_drop_p0s":false}"#); + }); + + let client = client_for(&server); + let info = client.agent_info().await.unwrap().unwrap(); + assert_eq!(info.container_tags_hash.as_deref(), Some("abc123")); +} + +// ── static headers ───────────────────────────────────────────────────────────── + +#[tokio::test] +async fn language_metadata_headers_injected_on_all_requests() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.5/traces") + .header("Datadog-Meta-Lang", "python") + .header("Datadog-Meta-Lang-Version", "3.12.1") + .header("Datadog-Meta-Lang-Interpreter", "CPython") + .header("Datadog-Meta-Tracer-Version", "2.18.0") + .header("User-Agent", "dd-trace-python/2.18.0"); + then.status(200).body(r#"{}"#); + }); + + let client = client_for(&server); + client + .send_traces( + Bytes::from_static(b""), + 0, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn test_token_injected_when_set() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.5/traces") + .header("x-datadog-test-session-token", "my-token"); + then.status(200).body(r#"{}"#); + }); + + ensure_crypto_provider(); + let client = AgentClient::builder() + .http("localhost", server.port()) + .language_metadata(LanguageMetadata::new("python", "3.12.1", "CPython", "2.18.0")) + .test_agent_session_token("my-token") + .build() + .unwrap(); + + client + .send_traces( + Bytes::from_static(b""), + 0, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + mock.assert(); +} From 4cd9e9ee8469b7081bc561cad5f3bb925feef9b6 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 21 Apr 2026 11:15:00 +0200 Subject: [PATCH 09/32] fix: address review findings - Fix typo "clientm" -> "client" in lib.rs doc comment - Serialize env-var tests with a static mutex to prevent parallel-test flakiness in timeout_from_env_{uses_default,parses_env_var} - Use std::io::Error::other() (stable since 1.81) for IoError mapping - Avoid unnecessary .clone() when building container entity-ID header Co-Authored-By: Claude Sonnet 4.6 --- Cargo.lock | 1 + libdd-agent-client/Cargo.toml | 1 + libdd-agent-client/src/builder.rs | 9 ++++++--- libdd-agent-client/src/client.rs | 2 +- libdd-agent-client/src/lib.rs | 2 +- 5 files changed, 10 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 53f08d7f11..74b1adfc3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2821,6 +2821,7 @@ dependencies = [ "rustls", "serde", "serde_json", + "serial_test", "thiserror 2.0.17", "tokio", ] diff --git a/libdd-agent-client/Cargo.toml b/libdd-agent-client/Cargo.toml index dd65c36fd6..7e60b173fd 100644 --- a/libdd-agent-client/Cargo.toml +++ b/libdd-agent-client/Cargo.toml @@ -27,4 +27,5 @@ libdd-http-client = { path = "../libdd-http-client" } [dev-dependencies] httpmock = "0.8.0-alpha.1" rustls = { version = "0.23", default-features = false, features = ["ring"] } +serial_test = "3.2" tokio = { version = "1.23", features = ["rt", "macros"] } diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index fbabe04914..c4849c7bd9 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -358,8 +358,9 @@ fn container_headers() -> Vec<(String, String)> { #[cfg(target_os = "linux")] if let Some(container_id) = read_container_id_from_cgroup() { - headers.push(("Datadog-Container-Id".to_string(), container_id.clone())); - headers.push(("Datadog-Entity-ID".to_string(), format!("ci-{}", container_id))); + let entity_id = format!("ci-{}", container_id); + headers.push(("Datadog-Container-Id".to_string(), container_id)); + headers.push(("Datadog-Entity-ID".to_string(), entity_id)); } headers @@ -441,6 +442,7 @@ mod tests { } #[test] + #[serial_test::serial] fn timeout_from_env_uses_default_when_unset() { std::env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); let b = AgentClientBuilder::new().timeout_from_env(); @@ -451,11 +453,12 @@ mod tests { } #[test] + #[serial_test::serial] fn timeout_from_env_parses_env_var() { std::env::set_var("DD_TRACE_AGENT_TIMEOUT_SECONDS", "5"); let b = AgentClientBuilder::new().timeout_from_env(); - assert_eq!(b.timeout, Some(Duration::from_secs(5))); std::env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); + assert_eq!(b.timeout, Some(Duration::from_secs(5))); } #[test] diff --git a/libdd-agent-client/src/client.rs b/libdd-agent-client/src/client.rs index 7e6e1f5f0a..1ab04cda35 100644 --- a/libdd-agent-client/src/client.rs +++ b/libdd-agent-client/src/client.rs @@ -304,7 +304,7 @@ fn map_http_error(e: libdd_http_client::HttpClientError) -> SendError { std::io::Error::new(std::io::ErrorKind::TimedOut, "request timed out"), ), libdd_http_client::HttpClientError::IoError(s) => { - SendError::Transport(std::io::Error::new(std::io::ErrorKind::Other, s)) + SendError::Transport(std::io::Error::other(s)) } libdd_http_client::HttpClientError::InvalidConfig(s) => { SendError::Transport(std::io::Error::new(std::io::ErrorKind::InvalidInput, s)) diff --git a/libdd-agent-client/src/lib.rs b/libdd-agent-client/src/lib.rs index c28a78edc9..054cbf01ec 100644 --- a/libdd-agent-client/src/lib.rs +++ b/libdd-agent-client/src/lib.rs @@ -1,7 +1,7 @@ // Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ // SPDX-License-Identifier: Apache-2.0 -//! This crate provides a Datadog-agent-specific HTTP clientm sitting on top of the basic +//! This crate provides a Datadog-agent-specific HTTP client sitting on top of the basic //! `libdd-http-client` primitives. The API is higher-level and makes agent-specific settings //! (headers, etc.) the default rather than opt-in boilerplate. //! From 96e93695d78980b61dfab12006afbaca8101a395 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 21 Apr 2026 13:19:07 +0200 Subject: [PATCH 10/32] refactor: get rid of ResolvedTransport --- libdd-agent-client/src/builder.rs | 203 ++++++++++++------------------ 1 file changed, 80 insertions(+), 123 deletions(-) diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index c4849c7bd9..83807a5ef9 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -5,6 +5,10 @@ use std::collections::HashMap; use std::time::Duration; +#[cfg(unix)] +use std::path::PathBuf; +#[cfg(windows)] +use OsString; use libdd_http_client::RetryConfig; @@ -43,23 +47,13 @@ pub enum AgentTransport { #[cfg(unix)] UnixSocket { /// Filesystem path to the socket file. - path: std::path::PathBuf, + path: PathBuf, }, /// Windows Named Pipe. #[cfg(windows)] NamedPipe { /// Named pipe path, e.g. `\\.\pipe\DD_APM_DRIVER`. - path: std::ffi::OsString, - }, - /// Probe at build time: use UDS if the socket file exists, otherwise fall back to HTTP. - #[cfg(unix)] - AutoDetect { - /// UDS path to probe. - uds_path: std::path::PathBuf, - /// Fallback host when the socket is absent. - fallback_host: String, - /// Fallback port when the socket is absent (typically 8126). - fallback_port: u16, + path: OsString, }, } @@ -120,13 +114,13 @@ impl AgentClientBuilder { /// Convenience: Unix Domain Socket. #[cfg(unix)] - pub fn unix_socket(self, path: impl Into) -> Self { + pub fn unix_socket(self, path: impl Into) -> Self { self.transport(AgentTransport::UnixSocket { path: path.into() }) } /// Convenience: Windows Named Pipe. #[cfg(windows)] - pub fn windows_named_pipe(self, path: impl Into) -> Self { + pub fn windows_named_pipe(self, path: impl Into) -> Self { self.transport(AgentTransport::NamedPipe { path: path.into() }) } @@ -134,15 +128,20 @@ impl AgentClientBuilder { #[cfg(unix)] pub fn auto_detect( self, - uds_path: impl Into, + uds_path: impl Into, fallback_host: impl Into, fallback_port: u16, ) -> Self { - self.transport(AgentTransport::AutoDetect { - uds_path: uds_path.into(), - fallback_host: fallback_host.into(), - fallback_port, - }) + let uds_path = uds_path.into(); + let transport = if uds_path.try_exists().unwrap_or(false) { + AgentTransport::UnixSocket { path: uds_path } + } else { + AgentTransport::Http { + host: fallback_host.into(), + port: fallback_port, + } + }; + self.transport(transport) } /// Set the test session token. @@ -220,126 +219,84 @@ impl AgentClientBuilder { .unwrap_or(Duration::from_millis(DEFAULT_TIMEOUT_MS)); let retry = self.retry.unwrap_or_else(default_retry_config); - // Resolve AutoDetect to a concrete transport. - let resolved = resolve_transport(transport); - // Build the underlying HTTP client. - let http = build_http_client(resolved, timeout, retry) + let http = Self::build_http_client(transport, timeout, retry) .map_err(|e| BuildError::HttpClient(e.to_string()))?; // Pre-compute all static headers that are injected on every request. - let static_headers = build_static_headers(&language, self.test_token, self.extra_headers); + let static_headers = Self::build_static_headers(&language, self.test_token, self.extra_headers); Ok(AgentClient::new(http, static_headers)) } -} -/// A resolved (concrete) transport — no AutoDetect. -pub(crate) enum ResolvedTransport { - Http { host: String, port: u16 }, - #[cfg(unix)] - UnixSocket { path: std::path::PathBuf }, - #[cfg(windows)] - NamedPipe { path: std::ffi::OsString }, -} - -/// Resolve `AutoDetect` at build time; other variants pass through unchanged. -fn resolve_transport(transport: AgentTransport) -> ResolvedTransport { - match transport { - AgentTransport::Http { host, port } => ResolvedTransport::Http { host, port }, - #[cfg(unix)] - AgentTransport::UnixSocket { path } => ResolvedTransport::UnixSocket { path }, - #[cfg(windows)] - AgentTransport::NamedPipe { path } => ResolvedTransport::NamedPipe { path }, - #[cfg(unix)] - AgentTransport::AutoDetect { - uds_path, - fallback_host, - fallback_port, - } => { - if uds_path.exists() { - ResolvedTransport::UnixSocket { path: uds_path } - } else { - ResolvedTransport::Http { - host: fallback_host, - port: fallback_port, - } + fn build_http_client( + transport: AgentTransport, + timeout: Duration, + retry: RetryConfig, + ) -> Result { + let base_url = match &transport { + AgentTransport::Http { host, port } => format!("http://{}:{}", host, port), + #[cfg(unix)] + AgentTransport::UnixSocket { .. } => "http://localhost".to_string(), + #[cfg(windows)] + AgentTransport::NamedPipe { .. } => "http://localhost".to_string(), + }; + + let mut builder = libdd_http_client::HttpClient::builder() + .base_url(base_url) + .timeout(timeout) + // HTTP errors are handled by each send method, not by the underlying client. + // This allows methods like `agent_info` to interpret 404 as Ok(None) rather than + // an error, and avoids retrying on HTTP 4xx/5xx. + .treat_http_errors_as_errors(false) + .retry(retry); + + match transport { + AgentTransport::Http { .. } => {} + #[cfg(unix)] + AgentTransport::UnixSocket { path } => { + builder = builder.unix_socket(path); + } + #[cfg(windows)] + AgentTransport::NamedPipe { path } => { + builder = builder.windows_named_pipe(path); } } - } -} -/// Derive the base URL for a resolved transport. -pub(crate) fn base_url_for(transport: &ResolvedTransport) -> String { - match transport { - ResolvedTransport::Http { host, port } => format!("http://{}:{}", host, port), - #[cfg(unix)] - ResolvedTransport::UnixSocket { .. } => "http://localhost".to_string(), - #[cfg(windows)] - ResolvedTransport::NamedPipe { .. } => "http://localhost".to_string(), + builder.build() } -} -fn build_http_client( - transport: ResolvedTransport, - timeout: Duration, - retry: RetryConfig, -) -> Result { - let base_url = base_url_for(&transport); - let mut builder = libdd_http_client::HttpClient::builder() - .base_url(base_url) - .timeout(timeout) - // HTTP errors are handled by each send method, not by the underlying client. - // This allows methods like `agent_info` to interpret 404 as Ok(None) rather than - // an error, and avoids retrying on HTTP 4xx/5xx. - .treat_http_errors_as_errors(false) - .retry(retry); - - match transport { - ResolvedTransport::Http { .. } => {} - #[cfg(unix)] - ResolvedTransport::UnixSocket { path } => { - builder = builder.unix_socket(path); - } - #[cfg(windows)] - ResolvedTransport::NamedPipe { path } => { - builder = builder.windows_named_pipe(path); + fn build_static_headers( + language: &LanguageMetadata, + test_token: Option, + extra_headers: HashMap, + ) -> Vec<(String, String)> { + let mut headers = Vec::new(); + + headers.push(("Datadog-Meta-Lang".to_string(), language.language.clone())); + headers.push(( + "Datadog-Meta-Lang-Version".to_string(), + language.language_version.clone(), + )); + headers.push(( + "Datadog-Meta-Lang-Interpreter".to_string(), + language.interpreter.clone(), + )); + headers.push(( + "Datadog-Meta-Tracer-Version".to_string(), + language.tracer_version.clone(), + )); + headers.push(("User-Agent".to_string(), language.user_agent())); + + if let Some(token) = test_token { + headers.push(("x-datadog-test-session-token".to_string(), token)); } - } - - builder.build() -} -fn build_static_headers( - language: &LanguageMetadata, - test_token: Option, - extra_headers: HashMap, -) -> Vec<(String, String)> { - let mut headers = Vec::new(); + headers.extend(container_headers()); + headers.extend(extra_headers); - headers.push(("Datadog-Meta-Lang".to_string(), language.language.clone())); - headers.push(( - "Datadog-Meta-Lang-Version".to_string(), - language.language_version.clone(), - )); - headers.push(( - "Datadog-Meta-Lang-Interpreter".to_string(), - language.interpreter.clone(), - )); - headers.push(( - "Datadog-Meta-Tracer-Version".to_string(), - language.tracer_version.clone(), - )); - headers.push(("User-Agent".to_string(), language.user_agent())); - - if let Some(token) = test_token { - headers.push(("x-datadog-test-session-token".to_string(), token)); + headers } - - headers.extend(container_headers()); - headers.extend(extra_headers); - - headers } /// Read container / entity-ID headers from the host environment. From 5fdd98c4d5de9fb6f47bb0b47bdef6a3a42fb3f5 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 21 Apr 2026 14:10:16 +0200 Subject: [PATCH 11/32] refactor: clean the agent-client code a bit --- Cargo.lock | 1 + libdd-agent-client/Cargo.toml | 1 + libdd-agent-client/src/builder.rs | 107 ++++++++++++--------------- libdd-agent-client/src/client.rs | 117 ++++++++++++------------------ 4 files changed, 94 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 74b1adfc3e..378adf88d6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2817,6 +2817,7 @@ dependencies = [ "bytes", "flate2", "httpmock", + "libdd-common", "libdd-http-client", "rustls", "serde", diff --git a/libdd-agent-client/Cargo.toml b/libdd-agent-client/Cargo.toml index 7e60b173fd..f8f80c17cb 100644 --- a/libdd-agent-client/Cargo.toml +++ b/libdd-agent-client/Cargo.toml @@ -23,6 +23,7 @@ serde_json = "1.0" thiserror = "2" tokio = { version = "1.23", features = ["rt"] } libdd-http-client = { path = "../libdd-http-client" } +libdd-common = { version = "3.0.2", path = "../libdd-common", default-features = false } [dev-dependencies] httpmock = "0.8.0-alpha.1" diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index 83807a5ef9..032a5251da 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -4,9 +4,9 @@ //! Builder for [`crate::AgentClient`]. use std::collections::HashMap; -use std::time::Duration; #[cfg(unix)] use std::path::PathBuf; +use std::time::Duration; #[cfg(windows)] use OsString; @@ -19,6 +19,8 @@ pub const DEFAULT_TIMEOUT_MS: u64 = 2_000; /// Default retry configuration: 2 retries (3 total attempts), 100 ms initial delay, /// exponential backoff with full jitter. +//TODO: Do we really want something different from `RetryConfig::default()` for the agent? The only +//difference is the number of retries : 3 vs 2 pub fn default_retry_config() -> RetryConfig { RetryConfig::new() .max_retries(2) @@ -94,11 +96,13 @@ pub struct AgentClientBuilder { impl AgentClientBuilder { /// Create a new builder with default settings. + #[inline] pub fn new() -> Self { Self::default() } /// Set the transport configuration. + #[inline] pub fn transport(mut self, transport: AgentTransport) -> Self { self.transport = Some(transport); self @@ -114,6 +118,7 @@ impl AgentClientBuilder { /// Convenience: Unix Domain Socket. #[cfg(unix)] + #[inline] pub fn unix_socket(self, path: impl Into) -> Self { self.transport(AgentTransport::UnixSocket { path: path.into() }) } @@ -133,7 +138,7 @@ impl AgentClientBuilder { fallback_port: u16, ) -> Self { let uds_path = uds_path.into(); - let transport = if uds_path.try_exists().unwrap_or(false) { + let transport = if let Ok(true) = uds_path.try_exists() { AgentTransport::UnixSocket { path: uds_path } } else { AgentTransport::Http { @@ -147,6 +152,7 @@ impl AgentClientBuilder { /// Set the test session token. /// /// When set, `x-datadog-test-session-token: ` is injected on every request. + #[inline] pub fn test_agent_session_token(mut self, token: impl Into) -> Self { self.test_token = Some(token.into()); self @@ -155,6 +161,7 @@ impl AgentClientBuilder { /// Set the request timeout. /// /// Defaults to [`DEFAULT_TIMEOUT_MS`] (2 000 ms) when not set. + #[inline] pub fn timeout(mut self, timeout: Duration) -> Self { self.timeout = Some(timeout); self @@ -175,12 +182,14 @@ impl AgentClientBuilder { /// Override the default retry configuration. /// /// Defaults to [`default_retry_config`]. + #[inline] pub fn retry(mut self, config: RetryConfig) -> Self { self.retry = Some(config); self } /// Set the language/runtime metadata injected into every request. Required. + #[inline] pub fn language_metadata(mut self, meta: LanguageMetadata) -> Self { self.language = Some(meta); self @@ -192,6 +201,7 @@ impl AgentClientBuilder { /// second connection when keep-alive is enabled. The default of `false` is correct for all /// periodic-flush writers (traces, stats, data streams). Set to `true` only for /// high-frequency continuous senders (e.g. a streaming profiling exporter). + #[inline] pub fn use_keep_alive(mut self, enabled: bool) -> Self { self.keep_alive = enabled; self @@ -205,6 +215,7 @@ impl AgentClientBuilder { // baked in; only the opt-in client-level `gzip(level)` builder knob is deferred. /// Additional custom headers to inject. + #[inline] pub fn extra_headers(mut self, headers: HashMap) -> Self { self.extra_headers = headers; self @@ -224,7 +235,8 @@ impl AgentClientBuilder { .map_err(|e| BuildError::HttpClient(e.to_string()))?; // Pre-compute all static headers that are injected on every request. - let static_headers = Self::build_static_headers(&language, self.test_token, self.extra_headers); + let static_headers = + Self::build_static_headers(&language, self.test_token, self.extra_headers); Ok(AgentClient::new(http, static_headers)) } @@ -271,76 +283,47 @@ impl AgentClientBuilder { test_token: Option, extra_headers: HashMap, ) -> Vec<(String, String)> { - let mut headers = Vec::new(); - - headers.push(("Datadog-Meta-Lang".to_string(), language.language.clone())); - headers.push(( - "Datadog-Meta-Lang-Version".to_string(), - language.language_version.clone(), - )); - headers.push(( - "Datadog-Meta-Lang-Interpreter".to_string(), - language.interpreter.clone(), - )); - headers.push(( - "Datadog-Meta-Tracer-Version".to_string(), - language.tracer_version.clone(), - )); - headers.push(("User-Agent".to_string(), language.user_agent())); + let mut headers = vec![ + ("Datadog-Meta-Lang".to_string(), language.language.clone()), + ("Datadog-Meta-Lang-Version".to_string(), language.language_version.clone()), + ("Datadog-Meta-Lang-Interpreter".to_string(), language.interpreter.clone()), + ("Datadog-Meta-Tracer-Version".to_string(), language.tracer_version.clone()), + ("User-Agent".to_string(), language.user_agent()), + ]; if let Some(token) = test_token { headers.push(("x-datadog-test-session-token".to_string(), token)); } - headers.extend(container_headers()); + headers.extend(Self::container_headers()); headers.extend(extra_headers); headers } -} -/// Read container / entity-ID headers from the host environment. -/// -/// On Linux, parses `/proc/self/cgroup` to extract the container ID and injects -/// `Datadog-Container-Id` and `Datadog-Entity-ID`. Always injects `Datadog-External-Env` -/// when `DD_EXTERNAL_ENV` is set. -fn container_headers() -> Vec<(String, String)> { - let mut headers = Vec::new(); - - if let Ok(env) = std::env::var("DD_EXTERNAL_ENV") { - if !env.is_empty() { - headers.push(("Datadog-External-Env".to_string(), env)); + /// Read container / entity-ID headers from the host environment. Always injects + /// `Datadog-External-Env` when `DD_EXTERNAL_ENV` is set. + fn container_headers() -> Vec<(String, String)> { + let mut headers = Vec::new(); + + if let Ok(env) = std::env::var("DD_EXTERNAL_ENV") { + if !env.is_empty() { + headers.push(("Datadog-External-Env".to_string(), env)); + } } - } - #[cfg(target_os = "linux")] - if let Some(container_id) = read_container_id_from_cgroup() { - let entity_id = format!("ci-{}", container_id); - headers.push(("Datadog-Container-Id".to_string(), container_id)); - headers.push(("Datadog-Entity-ID".to_string(), entity_id)); - } + use libdd_common::entity_id; - headers -} + if let Some(container_id) = entity_id::get_container_id() { + headers.push(("Datadog-Container-Id".to_string(), container_id.to_owned())); + } -/// Parse a 64-character hex container ID from `/proc/self/cgroup`. -/// -/// cgroup v1 paths end with the container ID, e.g.: -/// `12:blkio:/docker/abc123...64hex...` -#[cfg(target_os = "linux")] -fn read_container_id_from_cgroup() -> Option { - let content = std::fs::read_to_string("/proc/self/cgroup").ok()?; - for line in content.lines() { - // Each cgroup line is: :: - let path = line.splitn(3, ':').nth(2)?; - // Container ID is a 64-char hex segment at the end of the cgroup path. - for segment in path.split('/').rev() { - if segment.len() == 64 && segment.bytes().all(|b| b.is_ascii_hexdigit()) { - return Some(segment.to_string()); - } + if let Some(entity_id) = entity_id::get_entity_id() { + headers.push(("Datadog-Entity-ID".to_string(), entity_id.to_owned())); } + + headers } - None } #[cfg(test)] @@ -403,10 +386,7 @@ mod tests { fn timeout_from_env_uses_default_when_unset() { std::env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); let b = AgentClientBuilder::new().timeout_from_env(); - assert_eq!( - b.timeout, - Some(Duration::from_millis(DEFAULT_TIMEOUT_MS)) - ); + assert_eq!(b.timeout, Some(Duration::from_millis(DEFAULT_TIMEOUT_MS))); } #[test] @@ -423,6 +403,9 @@ mod tests { let mut headers = HashMap::new(); headers.insert("X-Custom".to_string(), "value".to_string()); let b = AgentClientBuilder::new().extra_headers(headers); - assert_eq!(b.extra_headers.get("X-Custom").map(|s| s.as_str()), Some("value")); + assert_eq!( + b.extra_headers.get("X-Custom").map(|s| s.as_str()), + Some("value") + ); } } diff --git a/libdd-agent-client/src/client.rs b/libdd-agent-client/src/client.rs index 1ab04cda35..7db283246d 100644 --- a/libdd-agent-client/src/client.rs +++ b/libdd-agent-client/src/client.rs @@ -79,16 +79,11 @@ impl AgentClient { let url = format!("{}{}", self.base_url, path); let mut request = libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Put, url) - .with_body(payload); - - for (k, v) in &self.static_headers { - request = request.with_header(k, v); - } - - request = request - .with_header("Content-Type", content_type) - .with_header("X-Datadog-Trace-Count", trace_count.to_string()) - .with_header("Datadog-Send-Real-Http-Status", "true"); + .with_body(payload) + .with_headers(self.static_headers.iter().cloned()) + .with_header("Content-Type", content_type) + .with_header("X-Datadog-Trace-Count", trace_count.to_string()) + .with_header("Datadog-Send-Real-Http-Status", "true"); if opts.computed_top_level { request = request.with_header("Datadog-Client-Computed-Top-Level", "yes"); @@ -113,14 +108,10 @@ impl AgentClient { /// Send span stats (APM concentrator buckets) to `/v0.6/stats`. pub async fn send_stats(&self, payload: Bytes) -> Result<(), SendError> { let url = format!("{}/v0.6/stats", self.base_url); - let mut request = - libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Put, url) - .with_body(payload); - - for (k, v) in &self.static_headers { - request = request.with_header(k, v); - } - request = request.with_header("Content-Type", "application/msgpack"); + let request = libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Put, url) + .with_body(payload) + .with_headers(self.static_headers.iter().cloned()) + .with_header("Content-Type", "application/msgpack"); let response = self.http.send(request).await.map_err(map_http_error)?; check_status(response) @@ -134,14 +125,9 @@ impl AgentClient { let compressed = gzip_compress(payload)?; let url = format!("{}/v0.1/pipeline_stats", self.base_url); - let mut request = - libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Put, url) - .with_body(compressed); - - for (k, v) in &self.static_headers { - request = request.with_header(k, v); - } - request = request + let request = libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Put, url) + .with_body(compressed) + .with_headers(self.static_headers.iter().cloned()) .with_header("Content-Type", "application/msgpack") .with_header("Content-Encoding", "gzip"); @@ -152,14 +138,9 @@ impl AgentClient { /// Send a telemetry event to the agent's telemetry proxy (`telemetry/proxy/api/v2/apmtelemetry`). pub async fn send_telemetry(&self, req: TelemetryRequest) -> Result<(), SendError> { let url = format!("{}/telemetry/proxy/api/v2/apmtelemetry", self.base_url); - let mut request = - libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Post, url) - .with_body(req.body); - - for (k, v) in &self.static_headers { - request = request.with_header(k, v); - } - request = request + let request = libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Post, url) + .with_body(req.body) + .with_headers(self.static_headers.iter().cloned()) .with_header("Content-Type", "application/json") .with_header("DD-Telemetry-Request-Type", &req.request_type) .with_header("DD-Telemetry-API-Version", &req.api_version) @@ -185,14 +166,9 @@ impl AgentClient { content_type: &str, ) -> Result<(), SendError> { let url = format!("{}{}", self.base_url, path); - let mut request = - libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Post, url) - .with_body(payload); - - for (k, v) in &self.static_headers { - request = request.with_header(k, v); - } - request = request + let request = libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Post, url) + .with_body(payload) + .with_headers(self.static_headers.iter().cloned()) .with_header("Content-Type", content_type) .with_header("X-Datadog-EVP-Subdomain", subdomain); @@ -204,14 +180,18 @@ impl AgentClient { /// /// Returns `Ok(None)` when the agent returns 404 (remote-config / info not supported). pub async fn agent_info(&self) -> Result, SendError> { - let url = format!("{}/info", self.base_url); - let mut request = - libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Get, url); - - for (k, v) in &self.static_headers { - request = request.with_header(k, v); + #[derive(serde::Deserialize)] + struct InfoResponse { + version: Option, + endpoints: Option>, + client_drop_p0s: Option, + config: Option, } + let url = format!("{}/info", self.base_url); + let request = libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Get, url) + .with_headers(self.static_headers.iter().cloned()); + let response = self.http.send(request).await.map_err(map_http_error)?; if response.status_code() == 404 { @@ -225,16 +205,17 @@ impl AgentClient { }); } - let container_tags_hash = header_value(response.headers(), "datadog-container-tags-hash"); - let state_hash = header_value(response.headers(), "datadog-agent-state"); + // Case-insensitive lookup of a response header value. + let header = |name: &str| -> Option { + response + .headers() + .iter() + .find(|(k, _)| k.eq_ignore_ascii_case(name)) + .map(|(_, v)| v.clone()) + }; - #[derive(serde::Deserialize)] - struct InfoResponse { - version: Option, - endpoints: Option>, - client_drop_p0s: Option, - config: Option, - } + let container_tags_hash = header("datadog-container-tags-hash"); + let state_hash = header("datadog-agent-state"); let info: InfoResponse = serde_json::from_slice(response.body()) .map_err(|e| SendError::Encoding(e.to_string()))?; @@ -274,14 +255,6 @@ fn check_status(response: libdd_http_client::HttpResponse) -> Result<(), SendErr } } -/// Case-insensitive lookup of a response header value. -fn header_value(headers: &[(String, String)], name: &str) -> Option { - headers - .iter() - .find(|(k, _)| k.eq_ignore_ascii_case(name)) - .map(|(_, v)| v.clone()) -} - /// Gzip-compress `payload` at level 6 (matching dd-trace-py's trace writer). fn gzip_compress(payload: Bytes) -> Result { let mut encoder = GzEncoder::new(Vec::new(), Compression::new(6)); @@ -300,9 +273,10 @@ fn map_http_error(e: libdd_http_client::HttpClientError) -> SendError { libdd_http_client::HttpClientError::ConnectionFailed(s) => SendError::Transport( std::io::Error::new(std::io::ErrorKind::ConnectionRefused, s), ), - libdd_http_client::HttpClientError::TimedOut => SendError::Transport( - std::io::Error::new(std::io::ErrorKind::TimedOut, "request timed out"), - ), + libdd_http_client::HttpClientError::TimedOut => SendError::Transport(std::io::Error::new( + std::io::ErrorKind::TimedOut, + "request timed out", + )), libdd_http_client::HttpClientError::IoError(s) => { SendError::Transport(std::io::Error::other(s)) } @@ -345,7 +319,11 @@ mod tests { #[test] fn static_headers_contain_language_metadata() { let client = test_client(8126); - let keys: Vec<&str> = client.static_headers.iter().map(|(k, _)| k.as_str()).collect(); + let keys: Vec<&str> = client + .static_headers + .iter() + .map(|(k, _)| k.as_str()) + .collect(); assert!(keys.contains(&"Datadog-Meta-Lang")); assert!(keys.contains(&"Datadog-Meta-Lang-Version")); assert!(keys.contains(&"User-Agent")); @@ -371,5 +349,4 @@ mod tests { let body = Bytes::from(r#"{"other":"value"}"#); assert!(parse_rate_by_service(&body).is_none()); } - } From 7504f3110cb7db4592dce6fdfcff55da7c8f5ce9 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 21 Apr 2026 14:11:07 +0200 Subject: [PATCH 12/32] feat: add with_headers() method to http-client request --- libdd-http-client/src/request.rs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/libdd-http-client/src/request.rs b/libdd-http-client/src/request.rs index 5b27cc2f1d..3d38384492 100644 --- a/libdd-http-client/src/request.rs +++ b/libdd-http-client/src/request.rs @@ -134,6 +134,13 @@ impl HttpRequest { self } + /// Append headers to this request. + #[inline] + pub fn with_headers<'a, K, V>(mut self, it: impl IntoIterator) -> Self where K: Into, V: Into { + self.headers.extend(it.into_iter().map(|(k, v)| (k.into(), v.into()))); + self + } + /// Set the request body. #[inline] pub fn with_body(mut self, body: impl Into) -> Self { From 3038a0558410cb9abc603f4d221577764e68de62 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 21 Apr 2026 14:11:42 +0200 Subject: [PATCH 13/32] style: formatting --- libdd-agent-client/src/builder.rs | 15 ++++++++++++--- libdd-agent-client/src/client.rs | 3 ++- libdd-agent-client/src/lib.rs | 3 ++- libdd-agent-client/tests/integration.rs | 8 ++++++-- 4 files changed, 22 insertions(+), 7 deletions(-) diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index 032a5251da..e8b9738506 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -285,9 +285,18 @@ impl AgentClientBuilder { ) -> Vec<(String, String)> { let mut headers = vec![ ("Datadog-Meta-Lang".to_string(), language.language.clone()), - ("Datadog-Meta-Lang-Version".to_string(), language.language_version.clone()), - ("Datadog-Meta-Lang-Interpreter".to_string(), language.interpreter.clone()), - ("Datadog-Meta-Tracer-Version".to_string(), language.tracer_version.clone()), + ( + "Datadog-Meta-Lang-Version".to_string(), + language.language_version.clone(), + ), + ( + "Datadog-Meta-Lang-Interpreter".to_string(), + language.interpreter.clone(), + ), + ( + "Datadog-Meta-Tracer-Version".to_string(), + language.tracer_version.clone(), + ), ("User-Agent".to_string(), language.user_agent()), ]; diff --git a/libdd-agent-client/src/client.rs b/libdd-agent-client/src/client.rs index 7db283246d..5131beda10 100644 --- a/libdd-agent-client/src/client.rs +++ b/libdd-agent-client/src/client.rs @@ -135,7 +135,8 @@ impl AgentClient { check_status(response) } - /// Send a telemetry event to the agent's telemetry proxy (`telemetry/proxy/api/v2/apmtelemetry`). + /// Send a telemetry event to the agent's telemetry proxy + /// (`telemetry/proxy/api/v2/apmtelemetry`). pub async fn send_telemetry(&self, req: TelemetryRequest) -> Result<(), SendError> { let url = format!("{}/telemetry/proxy/api/v2/apmtelemetry", self.base_url); let request = libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Post, url) diff --git a/libdd-agent-client/src/lib.rs b/libdd-agent-client/src/lib.rs index 054cbf01ec..8b8e69fad3 100644 --- a/libdd-agent-client/src/lib.rs +++ b/libdd-agent-client/src/lib.rs @@ -40,7 +40,8 @@ //! //! # Fork safety //! -//! The underlying `libdd-http-client` uses the `hickory-dns` DNS resolver by default, which is in-process and fork-safe. +//! The underlying `libdd-http-client` uses the `hickory-dns` DNS resolver by default, which is +//! in-process and fork-safe. pub mod agent_info; pub mod builder; diff --git a/libdd-agent-client/tests/integration.rs b/libdd-agent-client/tests/integration.rs index 26102a7784..87ad635394 100644 --- a/libdd-agent-client/tests/integration.rs +++ b/libdd-agent-client/tests/integration.rs @@ -17,7 +17,9 @@ fn client_for(server: &MockServer) -> AgentClient { ensure_crypto_provider(); AgentClient::builder() .http("localhost", server.port()) - .language_metadata(LanguageMetadata::new("python", "3.12.1", "CPython", "2.18.0")) + .language_metadata(LanguageMetadata::new( + "python", "3.12.1", "CPython", "2.18.0", + )) .build() .expect("client build failed") } @@ -445,7 +447,9 @@ async fn test_token_injected_when_set() { ensure_crypto_provider(); let client = AgentClient::builder() .http("localhost", server.port()) - .language_metadata(LanguageMetadata::new("python", "3.12.1", "CPython", "2.18.0")) + .language_metadata(LanguageMetadata::new( + "python", "3.12.1", "CPython", "2.18.0", + )) .test_agent_session_token("my-token") .build() .unwrap(); From a654449e761f61570df4230489bc14e37738ef48 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 21 Apr 2026 14:13:55 +0200 Subject: [PATCH 14/32] refactor: split integration tests into per-topic files Replace the single tests/integration.rs (with section separators) with one file per topic: send_traces, send_stats, send_pipeline_stats, send_telemetry, send_evp_event, agent_info, static_headers. Shared helpers (ensure_crypto_provider, client_for) live in tests/common/mod.rs. Co-Authored-By: Claude Sonnet 4.6 --- libdd-agent-client/tests/agent_info.rs | 57 +++ libdd-agent-client/tests/common/mod.rs | 20 + libdd-agent-client/tests/integration.rs | 468 ------------------ libdd-agent-client/tests/send_evp_event.rs | 31 ++ .../tests/send_pipeline_stats.rs | 43 ++ libdd-agent-client/tests/send_stats.rs | 43 ++ libdd-agent-client/tests/send_telemetry.rs | 57 +++ libdd-agent-client/tests/send_traces.rs | 180 +++++++ libdd-agent-client/tests/static_headers.rs | 69 +++ 9 files changed, 500 insertions(+), 468 deletions(-) create mode 100644 libdd-agent-client/tests/agent_info.rs create mode 100644 libdd-agent-client/tests/common/mod.rs delete mode 100644 libdd-agent-client/tests/integration.rs create mode 100644 libdd-agent-client/tests/send_evp_event.rs create mode 100644 libdd-agent-client/tests/send_pipeline_stats.rs create mode 100644 libdd-agent-client/tests/send_stats.rs create mode 100644 libdd-agent-client/tests/send_telemetry.rs create mode 100644 libdd-agent-client/tests/send_traces.rs create mode 100644 libdd-agent-client/tests/static_headers.rs diff --git a/libdd-agent-client/tests/agent_info.rs b/libdd-agent-client/tests/agent_info.rs new file mode 100644 index 0000000000..b0f26442c0 --- /dev/null +++ b/libdd-agent-client/tests/agent_info.rs @@ -0,0 +1,57 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +mod common; + +use httpmock::prelude::*; + +#[tokio::test] +async fn parses_info_response() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/info"); + then.status(200).body( + r#"{ + "version": "7.50.0", + "endpoints": ["/v0.4/traces", "/v0.5/traces"], + "client_drop_p0s": true, + "config": {} + }"#, + ); + }); + + let client = common::client_for(&server); + let info = client.agent_info().await.unwrap().expect("expected Some"); + + assert_eq!(info.version.as_deref(), Some("7.50.0")); + assert!(info.endpoints.contains(&"/v0.5/traces".to_string())); + assert!(info.client_drop_p0s); +} + +#[tokio::test] +async fn returns_none_on_404() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/info"); + then.status(404).body("not found"); + }); + + let client = common::client_for(&server); + let result = client.agent_info().await.unwrap(); + assert!(result.is_none()); +} + +#[tokio::test] +async fn extracts_container_tags_hash_header() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(GET).path("/info"); + then.status(200) + .header("Datadog-Container-Tags-Hash", "abc123") + .body(r#"{"endpoints":[],"client_drop_p0s":false}"#); + }); + + let client = common::client_for(&server); + let info = client.agent_info().await.unwrap().unwrap(); + assert_eq!(info.container_tags_hash.as_deref(), Some("abc123")); +} diff --git a/libdd-agent-client/tests/common/mod.rs b/libdd-agent-client/tests/common/mod.rs new file mode 100644 index 0000000000..79f0869d9f --- /dev/null +++ b/libdd-agent-client/tests/common/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +use httpmock::MockServer; +use libdd_agent_client::{AgentClient, LanguageMetadata}; + +pub fn ensure_crypto_provider() { + let _ = rustls::crypto::ring::default_provider().install_default(); +} + +pub fn client_for(server: &MockServer) -> AgentClient { + ensure_crypto_provider(); + AgentClient::builder() + .http("localhost", server.port()) + .language_metadata(LanguageMetadata::new( + "python", "3.12.1", "CPython", "2.18.0", + )) + .build() + .expect("client build failed") +} diff --git a/libdd-agent-client/tests/integration.rs b/libdd-agent-client/tests/integration.rs deleted file mode 100644 index 87ad635394..0000000000 --- a/libdd-agent-client/tests/integration.rs +++ /dev/null @@ -1,468 +0,0 @@ -// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ -// SPDX-License-Identifier: Apache-2.0 - -//! Integration tests using a mock HTTP server. - -use bytes::Bytes; -use httpmock::prelude::*; -use libdd_agent_client::{ - AgentClient, LanguageMetadata, TelemetryRequest, TraceFormat, TraceSendOptions, -}; - -fn ensure_crypto_provider() { - let _ = rustls::crypto::ring::default_provider().install_default(); -} - -fn client_for(server: &MockServer) -> AgentClient { - ensure_crypto_provider(); - AgentClient::builder() - .http("localhost", server.port()) - .language_metadata(LanguageMetadata::new( - "python", "3.12.1", "CPython", "2.18.0", - )) - .build() - .expect("client build failed") -} - -// ── send_traces ──────────────────────────────────────────────────────────────── - -#[tokio::test] -async fn send_traces_v5_puts_to_correct_endpoint() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(PUT).path("/v0.5/traces"); - then.status(200).body(r#"{"rate_by_service":{}}"#); - }); - - let client = client_for(&server); - let resp = client - .send_traces( - Bytes::from_static(b"\x91\x90"), - 1, - TraceFormat::MsgpackV5, - TraceSendOptions::default(), - ) - .await - .unwrap(); - - mock.assert(); - assert_eq!(resp.status, 200); -} - -#[tokio::test] -async fn send_traces_v4_puts_to_v4_endpoint() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(PUT).path("/v0.4/traces"); - then.status(200).body(r#"{}"#); - }); - - let client = client_for(&server); - client - .send_traces( - Bytes::from_static(b"\x91\x90"), - 1, - TraceFormat::MsgpackV4, - TraceSendOptions::default(), - ) - .await - .unwrap(); - - mock.assert(); -} - -#[tokio::test] -async fn send_traces_injects_trace_count_header() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(PUT) - .path("/v0.5/traces") - .header("X-Datadog-Trace-Count", "42"); - then.status(200).body(r#"{}"#); - }); - - let client = client_for(&server); - client - .send_traces( - Bytes::from_static(b"\x91\x90"), - 42, - TraceFormat::MsgpackV5, - TraceSendOptions::default(), - ) - .await - .unwrap(); - - mock.assert(); -} - -#[tokio::test] -async fn send_traces_injects_send_real_http_status_header() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(PUT) - .path("/v0.5/traces") - .header("Datadog-Send-Real-Http-Status", "true"); - then.status(200).body(r#"{}"#); - }); - - let client = client_for(&server); - client - .send_traces( - Bytes::from_static(b""), - 0, - TraceFormat::MsgpackV5, - TraceSendOptions::default(), - ) - .await - .unwrap(); - - mock.assert(); -} - -#[tokio::test] -async fn send_traces_computed_top_level_injects_header() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(PUT) - .path("/v0.5/traces") - .header("Datadog-Client-Computed-Top-Level", "yes"); - then.status(200).body(r#"{}"#); - }); - - let client = client_for(&server); - client - .send_traces( - Bytes::from_static(b""), - 0, - TraceFormat::MsgpackV5, - TraceSendOptions { - computed_top_level: true, - }, - ) - .await - .unwrap(); - - mock.assert(); -} - -#[tokio::test] -async fn send_traces_parses_rate_by_service() { - let server = MockServer::start(); - server.mock(|when, then| { - when.method(PUT).path("/v0.5/traces"); - then.status(200) - .body(r#"{"rate_by_service":{"service:env":0.75}}"#); - }); - - let client = client_for(&server); - let resp = client - .send_traces( - Bytes::from_static(b""), - 0, - TraceFormat::MsgpackV5, - TraceSendOptions::default(), - ) - .await - .unwrap(); - - assert_eq!( - resp.rate_by_service - .as_ref() - .and_then(|m| m.get("service:env")), - Some(&0.75) - ); -} - -#[tokio::test] -async fn send_traces_returns_http_error_on_5xx() { - let server = MockServer::start(); - server.mock(|when, then| { - when.method(PUT).path("/v0.5/traces"); - then.status(503).body("overloaded"); - }); - - let client = client_for(&server); - let err = client - .send_traces( - Bytes::from_static(b""), - 0, - TraceFormat::MsgpackV5, - TraceSendOptions::default(), - ) - .await - .unwrap_err(); - - assert!(matches!( - err, - libdd_agent_client::SendError::HttpError { status: 503, .. } - )); -} - -// ── send_stats ───────────────────────────────────────────────────────────────── - -#[tokio::test] -async fn send_stats_puts_to_v06_stats() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(PUT).path("/v0.6/stats"); - then.status(200).body(""); - }); - - let client = client_for(&server); - client - .send_stats(Bytes::from_static(b"\x80")) - .await - .unwrap(); - - mock.assert(); -} - -#[tokio::test] -async fn send_stats_sets_msgpack_content_type() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(PUT) - .path("/v0.6/stats") - .header("Content-Type", "application/msgpack"); - then.status(200).body(""); - }); - - let client = client_for(&server); - client - .send_stats(Bytes::from_static(b"\x80")) - .await - .unwrap(); - - mock.assert(); -} - -// ── send_pipeline_stats ──────────────────────────────────────────────────────── - -#[tokio::test] -async fn send_pipeline_stats_puts_to_correct_endpoint() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(PUT).path("/v0.1/pipeline_stats"); - then.status(200).body(""); - }); - - let client = client_for(&server); - client - .send_pipeline_stats(Bytes::from_static(b"\x80")) - .await - .unwrap(); - - mock.assert(); -} - -#[tokio::test] -async fn send_pipeline_stats_sets_gzip_encoding() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(PUT) - .path("/v0.1/pipeline_stats") - .header("Content-Encoding", "gzip"); - then.status(200).body(""); - }); - - let client = client_for(&server); - client - .send_pipeline_stats(Bytes::from_static(b"\x80")) - .await - .unwrap(); - - mock.assert(); -} - -// ── send_telemetry ───────────────────────────────────────────────────────────── - -#[tokio::test] -async fn send_telemetry_posts_to_telemetry_proxy() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(POST) - .path("/telemetry/proxy/api/v2/apmtelemetry"); - then.status(202).body(""); - }); - - let client = client_for(&server); - client - .send_telemetry(TelemetryRequest { - request_type: "app-started".to_string(), - api_version: "v2".to_string(), - debug: false, - body: Bytes::from_static(b"{}"), - }) - .await - .unwrap(); - - mock.assert(); -} - -#[tokio::test] -async fn send_telemetry_injects_per_request_headers() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(POST) - .path("/telemetry/proxy/api/v2/apmtelemetry") - .header("DD-Telemetry-Request-Type", "app-started") - .header("DD-Telemetry-API-Version", "v2") - .header("DD-Telemetry-Debug-Enabled", "false"); - then.status(202).body(""); - }); - - let client = client_for(&server); - client - .send_telemetry(TelemetryRequest { - request_type: "app-started".to_string(), - api_version: "v2".to_string(), - debug: false, - body: Bytes::from_static(b"{}"), - }) - .await - .unwrap(); - - mock.assert(); -} - -// ── send_evp_event ───────────────────────────────────────────────────────────── - -#[tokio::test] -async fn send_evp_event_posts_to_path_with_subdomain_header() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(POST) - .path("/api/v2/exposures") - .header("X-Datadog-EVP-Subdomain", "event-platform-intake"); - then.status(200).body(""); - }); - - let client = client_for(&server); - client - .send_evp_event( - "event-platform-intake", - "/api/v2/exposures", - Bytes::from_static(b"{}"), - "application/json", - ) - .await - .unwrap(); - - mock.assert(); -} - -// ── agent_info ───────────────────────────────────────────────────────────────── - -#[tokio::test] -async fn agent_info_parses_info_response() { - let server = MockServer::start(); - server.mock(|when, then| { - when.method(GET).path("/info"); - then.status(200).body( - r#"{ - "version": "7.50.0", - "endpoints": ["/v0.4/traces", "/v0.5/traces"], - "client_drop_p0s": true, - "config": {} - }"#, - ); - }); - - let client = client_for(&server); - let info = client.agent_info().await.unwrap().expect("expected Some"); - - assert_eq!(info.version.as_deref(), Some("7.50.0")); - assert!(info.endpoints.contains(&"/v0.5/traces".to_string())); - assert!(info.client_drop_p0s); -} - -#[tokio::test] -async fn agent_info_returns_none_on_404() { - let server = MockServer::start(); - server.mock(|when, then| { - when.method(GET).path("/info"); - then.status(404).body("not found"); - }); - - let client = client_for(&server); - let result = client.agent_info().await.unwrap(); - assert!(result.is_none()); -} - -#[tokio::test] -async fn agent_info_extracts_container_tags_hash_header() { - let server = MockServer::start(); - server.mock(|when, then| { - when.method(GET).path("/info"); - then.status(200) - .header("Datadog-Container-Tags-Hash", "abc123") - .body(r#"{"endpoints":[],"client_drop_p0s":false}"#); - }); - - let client = client_for(&server); - let info = client.agent_info().await.unwrap().unwrap(); - assert_eq!(info.container_tags_hash.as_deref(), Some("abc123")); -} - -// ── static headers ───────────────────────────────────────────────────────────── - -#[tokio::test] -async fn language_metadata_headers_injected_on_all_requests() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(PUT) - .path("/v0.5/traces") - .header("Datadog-Meta-Lang", "python") - .header("Datadog-Meta-Lang-Version", "3.12.1") - .header("Datadog-Meta-Lang-Interpreter", "CPython") - .header("Datadog-Meta-Tracer-Version", "2.18.0") - .header("User-Agent", "dd-trace-python/2.18.0"); - then.status(200).body(r#"{}"#); - }); - - let client = client_for(&server); - client - .send_traces( - Bytes::from_static(b""), - 0, - TraceFormat::MsgpackV5, - TraceSendOptions::default(), - ) - .await - .unwrap(); - - mock.assert(); -} - -#[tokio::test] -async fn test_token_injected_when_set() { - let server = MockServer::start(); - let mock = server.mock(|when, then| { - when.method(PUT) - .path("/v0.5/traces") - .header("x-datadog-test-session-token", "my-token"); - then.status(200).body(r#"{}"#); - }); - - ensure_crypto_provider(); - let client = AgentClient::builder() - .http("localhost", server.port()) - .language_metadata(LanguageMetadata::new( - "python", "3.12.1", "CPython", "2.18.0", - )) - .test_agent_session_token("my-token") - .build() - .unwrap(); - - client - .send_traces( - Bytes::from_static(b""), - 0, - TraceFormat::MsgpackV5, - TraceSendOptions::default(), - ) - .await - .unwrap(); - - mock.assert(); -} diff --git a/libdd-agent-client/tests/send_evp_event.rs b/libdd-agent-client/tests/send_evp_event.rs new file mode 100644 index 0000000000..7176ffe164 --- /dev/null +++ b/libdd-agent-client/tests/send_evp_event.rs @@ -0,0 +1,31 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +mod common; + +use bytes::Bytes; +use httpmock::prelude::*; + +#[tokio::test] +async fn posts_to_path_with_subdomain_header() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(POST) + .path("/api/v2/exposures") + .header("X-Datadog-EVP-Subdomain", "event-platform-intake"); + then.status(200).body(""); + }); + + let client = common::client_for(&server); + client + .send_evp_event( + "event-platform-intake", + "/api/v2/exposures", + Bytes::from_static(b"{}"), + "application/json", + ) + .await + .unwrap(); + + mock.assert(); +} diff --git a/libdd-agent-client/tests/send_pipeline_stats.rs b/libdd-agent-client/tests/send_pipeline_stats.rs new file mode 100644 index 0000000000..cf3b179d55 --- /dev/null +++ b/libdd-agent-client/tests/send_pipeline_stats.rs @@ -0,0 +1,43 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +mod common; + +use bytes::Bytes; +use httpmock::prelude::*; + +#[tokio::test] +async fn puts_to_correct_endpoint() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT).path("/v0.1/pipeline_stats"); + then.status(200).body(""); + }); + + let client = common::client_for(&server); + client + .send_pipeline_stats(Bytes::from_static(b"\x80")) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn sets_gzip_encoding() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.1/pipeline_stats") + .header("Content-Encoding", "gzip"); + then.status(200).body(""); + }); + + let client = common::client_for(&server); + client + .send_pipeline_stats(Bytes::from_static(b"\x80")) + .await + .unwrap(); + + mock.assert(); +} diff --git a/libdd-agent-client/tests/send_stats.rs b/libdd-agent-client/tests/send_stats.rs new file mode 100644 index 0000000000..83aafd9168 --- /dev/null +++ b/libdd-agent-client/tests/send_stats.rs @@ -0,0 +1,43 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +mod common; + +use bytes::Bytes; +use httpmock::prelude::*; + +#[tokio::test] +async fn puts_to_v06_stats() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT).path("/v0.6/stats"); + then.status(200).body(""); + }); + + let client = common::client_for(&server); + client + .send_stats(Bytes::from_static(b"\x80")) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn sets_msgpack_content_type() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.6/stats") + .header("Content-Type", "application/msgpack"); + then.status(200).body(""); + }); + + let client = common::client_for(&server); + client + .send_stats(Bytes::from_static(b"\x80")) + .await + .unwrap(); + + mock.assert(); +} diff --git a/libdd-agent-client/tests/send_telemetry.rs b/libdd-agent-client/tests/send_telemetry.rs new file mode 100644 index 0000000000..f5d99e3337 --- /dev/null +++ b/libdd-agent-client/tests/send_telemetry.rs @@ -0,0 +1,57 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +mod common; + +use bytes::Bytes; +use httpmock::prelude::*; +use libdd_agent_client::TelemetryRequest; + +#[tokio::test] +async fn posts_to_telemetry_proxy() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(POST) + .path("/telemetry/proxy/api/v2/apmtelemetry"); + then.status(202).body(""); + }); + + let client = common::client_for(&server); + client + .send_telemetry(TelemetryRequest { + request_type: "app-started".to_string(), + api_version: "v2".to_string(), + debug: false, + body: Bytes::from_static(b"{}"), + }) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn injects_per_request_headers() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(POST) + .path("/telemetry/proxy/api/v2/apmtelemetry") + .header("DD-Telemetry-Request-Type", "app-started") + .header("DD-Telemetry-API-Version", "v2") + .header("DD-Telemetry-Debug-Enabled", "false"); + then.status(202).body(""); + }); + + let client = common::client_for(&server); + client + .send_telemetry(TelemetryRequest { + request_type: "app-started".to_string(), + api_version: "v2".to_string(), + debug: false, + body: Bytes::from_static(b"{}"), + }) + .await + .unwrap(); + + mock.assert(); +} diff --git a/libdd-agent-client/tests/send_traces.rs b/libdd-agent-client/tests/send_traces.rs new file mode 100644 index 0000000000..1f833135a0 --- /dev/null +++ b/libdd-agent-client/tests/send_traces.rs @@ -0,0 +1,180 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +mod common; + +use bytes::Bytes; +use httpmock::prelude::*; +use libdd_agent_client::{TraceFormat, TraceSendOptions}; + +#[tokio::test] +async fn v5_puts_to_correct_endpoint() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT).path("/v0.5/traces"); + then.status(200).body(r#"{"rate_by_service":{}}"#); + }); + + let client = common::client_for(&server); + let resp = client + .send_traces( + Bytes::from_static(b"\x91\x90"), + 1, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + mock.assert(); + assert_eq!(resp.status, 200); +} + +#[tokio::test] +async fn v4_puts_to_v4_endpoint() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT).path("/v0.4/traces"); + then.status(200).body(r#"{}"#); + }); + + let client = common::client_for(&server); + client + .send_traces( + Bytes::from_static(b"\x91\x90"), + 1, + TraceFormat::MsgpackV4, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn injects_trace_count_header() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.5/traces") + .header("X-Datadog-Trace-Count", "42"); + then.status(200).body(r#"{}"#); + }); + + let client = common::client_for(&server); + client + .send_traces( + Bytes::from_static(b"\x91\x90"), + 42, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn injects_send_real_http_status_header() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.5/traces") + .header("Datadog-Send-Real-Http-Status", "true"); + then.status(200).body(r#"{}"#); + }); + + let client = common::client_for(&server); + client + .send_traces( + Bytes::from_static(b""), + 0, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn computed_top_level_injects_header() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.5/traces") + .header("Datadog-Client-Computed-Top-Level", "yes"); + then.status(200).body(r#"{}"#); + }); + + let client = common::client_for(&server); + client + .send_traces( + Bytes::from_static(b""), + 0, + TraceFormat::MsgpackV5, + TraceSendOptions { + computed_top_level: true, + }, + ) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn parses_rate_by_service() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(PUT).path("/v0.5/traces"); + then.status(200) + .body(r#"{"rate_by_service":{"service:env":0.75}}"#); + }); + + let client = common::client_for(&server); + let resp = client + .send_traces( + Bytes::from_static(b""), + 0, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + assert_eq!( + resp.rate_by_service + .as_ref() + .and_then(|m| m.get("service:env")), + Some(&0.75) + ); +} + +#[tokio::test] +async fn returns_http_error_on_5xx() { + let server = MockServer::start(); + server.mock(|when, then| { + when.method(PUT).path("/v0.5/traces"); + then.status(503).body("overloaded"); + }); + + let client = common::client_for(&server); + let err = client + .send_traces( + Bytes::from_static(b""), + 0, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap_err(); + + assert!(matches!( + err, + libdd_agent_client::SendError::HttpError { status: 503, .. } + )); +} diff --git a/libdd-agent-client/tests/static_headers.rs b/libdd-agent-client/tests/static_headers.rs new file mode 100644 index 0000000000..c0a22831f1 --- /dev/null +++ b/libdd-agent-client/tests/static_headers.rs @@ -0,0 +1,69 @@ +// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/ +// SPDX-License-Identifier: Apache-2.0 + +mod common; + +use bytes::Bytes; +use httpmock::prelude::*; +use libdd_agent_client::{AgentClient, LanguageMetadata, TraceFormat, TraceSendOptions}; + +#[tokio::test] +async fn language_metadata_headers_injected_on_all_requests() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.5/traces") + .header("Datadog-Meta-Lang", "python") + .header("Datadog-Meta-Lang-Version", "3.12.1") + .header("Datadog-Meta-Lang-Interpreter", "CPython") + .header("Datadog-Meta-Tracer-Version", "2.18.0") + .header("User-Agent", "dd-trace-python/2.18.0"); + then.status(200).body(r#"{}"#); + }); + + let client = common::client_for(&server); + client + .send_traces( + Bytes::from_static(b""), + 0, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + mock.assert(); +} + +#[tokio::test] +async fn test_token_injected_when_set() { + let server = MockServer::start(); + let mock = server.mock(|when, then| { + when.method(PUT) + .path("/v0.5/traces") + .header("x-datadog-test-session-token", "my-token"); + then.status(200).body(r#"{}"#); + }); + + common::ensure_crypto_provider(); + let client = AgentClient::builder() + .http("localhost", server.port()) + .language_metadata(LanguageMetadata::new( + "python", "3.12.1", "CPython", "2.18.0", + )) + .test_agent_session_token("my-token") + .build() + .unwrap(); + + client + .send_traces( + Bytes::from_static(b""), + 0, + TraceFormat::MsgpackV5, + TraceSendOptions::default(), + ) + .await + .unwrap(); + + mock.assert(); +} From 03aa81ce352e611424a597bd84feaee616ce5129 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 21 Apr 2026 14:21:40 +0200 Subject: [PATCH 15/32] refactor: drop send_ prefix from test modules, flatten common to common.rs Co-Authored-By: Claude Sonnet 4.6 --- libdd-agent-client/tests/{common/mod.rs => common.rs} | 0 libdd-agent-client/tests/{send_evp_event.rs => evp_event.rs} | 0 .../tests/{send_pipeline_stats.rs => pipeline_stats.rs} | 0 libdd-agent-client/tests/{send_stats.rs => stats.rs} | 0 libdd-agent-client/tests/{send_telemetry.rs => telemetry.rs} | 0 libdd-agent-client/tests/{send_traces.rs => traces.rs} | 0 6 files changed, 0 insertions(+), 0 deletions(-) rename libdd-agent-client/tests/{common/mod.rs => common.rs} (100%) rename libdd-agent-client/tests/{send_evp_event.rs => evp_event.rs} (100%) rename libdd-agent-client/tests/{send_pipeline_stats.rs => pipeline_stats.rs} (100%) rename libdd-agent-client/tests/{send_stats.rs => stats.rs} (100%) rename libdd-agent-client/tests/{send_telemetry.rs => telemetry.rs} (100%) rename libdd-agent-client/tests/{send_traces.rs => traces.rs} (100%) diff --git a/libdd-agent-client/tests/common/mod.rs b/libdd-agent-client/tests/common.rs similarity index 100% rename from libdd-agent-client/tests/common/mod.rs rename to libdd-agent-client/tests/common.rs diff --git a/libdd-agent-client/tests/send_evp_event.rs b/libdd-agent-client/tests/evp_event.rs similarity index 100% rename from libdd-agent-client/tests/send_evp_event.rs rename to libdd-agent-client/tests/evp_event.rs diff --git a/libdd-agent-client/tests/send_pipeline_stats.rs b/libdd-agent-client/tests/pipeline_stats.rs similarity index 100% rename from libdd-agent-client/tests/send_pipeline_stats.rs rename to libdd-agent-client/tests/pipeline_stats.rs diff --git a/libdd-agent-client/tests/send_stats.rs b/libdd-agent-client/tests/stats.rs similarity index 100% rename from libdd-agent-client/tests/send_stats.rs rename to libdd-agent-client/tests/stats.rs diff --git a/libdd-agent-client/tests/send_telemetry.rs b/libdd-agent-client/tests/telemetry.rs similarity index 100% rename from libdd-agent-client/tests/send_telemetry.rs rename to libdd-agent-client/tests/telemetry.rs diff --git a/libdd-agent-client/tests/send_traces.rs b/libdd-agent-client/tests/traces.rs similarity index 100% rename from libdd-agent-client/tests/send_traces.rs rename to libdd-agent-client/tests/traces.rs From a2ec20a155cf3253a0dffc2193b2b96b6ab74af2 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 21 Apr 2026 14:51:41 +0200 Subject: [PATCH 16/32] refactor: fold timeout_from_env + transport env-vars into auto_detect() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit auto_detect() now covers the full standard priority chain: DD_TRACE_AGENT_URL → DD_AGENT_HOST/DD_TRACE_AGENT_PORT → /var/run/datadog/apm.socket → localhost:8126, plus DD_TRACE_AGENT_TIMEOUT_SECONDS for the timeout. timeout_from_env() is removed; the module-level docs are updated with an auto_detect example alongside the explicit-transport example. Co-Authored-By: Claude Sonnet 4.6 --- libdd-agent-client/src/builder.rs | 193 ++++++++++++++++++++++++------ libdd-agent-client/src/lib.rs | 27 +++-- 2 files changed, 173 insertions(+), 47 deletions(-) diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index e8b9738506..0650d318f1 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -74,9 +74,10 @@ impl Default for AgentTransport { /// /// # Required fields /// -/// - Transport: set via [`AgentClientBuilder::transport`] or a convenience method +/// - Transport: set via [`AgentClientBuilder::auto_detect`] (reads standard env vars and +/// probes the local socket) or an explicit convenience method /// ([`AgentClientBuilder::http`], [`AgentClientBuilder::unix_socket`], -/// [`AgentClientBuilder::windows_named_pipe`], [`AgentClientBuilder::auto_detect`]). +/// [`AgentClientBuilder::windows_named_pipe`], [`AgentClientBuilder::transport`]). /// - [`AgentClientBuilder::language_metadata`]. /// /// # Test tokens @@ -129,24 +130,101 @@ impl AgentClientBuilder { self.transport(AgentTransport::NamedPipe { path: path.into() }) } - /// Convenience: auto-detect transport (UDS if socket file exists, else HTTP). + /// Auto-configure transport and timeout from the environment. + /// + /// Transport priority: + /// 1. `DD_TRACE_AGENT_URL` — parsed as `http://host:port` or `unix:///path`. + /// 2. `DD_AGENT_HOST` / `DD_TRACE_AGENT_PORT` — explicit host and/or port. + /// 3. `/var/run/datadog/apm.socket` — Unix domain socket if the file exists. + /// 4. `localhost:8126` — HTTP fallback. + /// + /// Timeout is read from `DD_TRACE_AGENT_TIMEOUT_SECONDS` (seconds, float), + /// defaulting to [`DEFAULT_TIMEOUT_MS`] when unset or unparseable. #[cfg(unix)] - pub fn auto_detect( - self, - uds_path: impl Into, - fallback_host: impl Into, - fallback_port: u16, - ) -> Self { - let uds_path = uds_path.into(); - let transport = if let Ok(true) = uds_path.try_exists() { - AgentTransport::UnixSocket { path: uds_path } - } else { - AgentTransport::Http { - host: fallback_host.into(), - port: fallback_port, + pub fn auto_detect(mut self) -> Self { + let transport = Self::transport_from_env().unwrap_or_else(|| { + let uds = PathBuf::from("/var/run/datadog/apm.socket"); + if uds.try_exists().unwrap_or(false) { + AgentTransport::UnixSocket { path: uds } + } else { + AgentTransport::Http { + host: "localhost".to_string(), + port: 8126, + } + } + }); + self.transport = Some(transport); + + self.timeout = Some( + std::env::var("DD_TRACE_AGENT_TIMEOUT_SECONDS") + .ok() + .and_then(|v| v.parse::().ok()) + .map(|secs| Duration::from_millis((secs * 1000.0) as u64)) + .unwrap_or(Duration::from_millis(DEFAULT_TIMEOUT_MS)), + ); + + self + } + + /// Read transport from env vars (`DD_TRACE_AGENT_URL`, then `DD_AGENT_HOST`/`DD_TRACE_AGENT_PORT`). + /// Returns `None` when none of the variables are set. + #[cfg(unix)] + fn transport_from_env() -> Option { + if let Ok(url) = std::env::var("DD_TRACE_AGENT_URL") { + if let Some(t) = Self::parse_agent_url(&url) { + return Some(t); } + } + + let host = std::env::var("DD_AGENT_HOST").ok(); + let port = std::env::var("DD_TRACE_AGENT_PORT") + .ok() + .and_then(|p| p.parse::().ok()); + + if host.is_some() || port.is_some() { + return Some(AgentTransport::Http { + host: host.unwrap_or_else(|| "localhost".to_string()), + port: port.unwrap_or(8126), + }); + } + + None + } + + /// Parse a Datadog agent URL into an [`AgentTransport`]. + /// + /// Supported schemes: `http://`, `https://`, `unix://`. + #[cfg(unix)] + fn parse_agent_url(url: &str) -> Option { + if let Some(after_scheme) = url.strip_prefix("unix://") { + // unix:///abs/path or unix://localhost/abs/path + let path = if after_scheme.starts_with('/') { + after_scheme + } else { + &after_scheme[after_scheme.find('/')?..] + }; + return Some(AgentTransport::UnixSocket { + path: PathBuf::from(path), + }); + } + + let rest = url + .strip_prefix("http://") + .or_else(|| url.strip_prefix("https://"))?; + + // Drop any trailing path (e.g. "host:port/") + let authority = rest.split('/').next().unwrap_or(rest); + let (host, port) = if let Some(colon) = authority.rfind(':') { + let port = authority[colon + 1..].parse::().ok().unwrap_or(8126); + (&authority[..colon], port) + } else { + (authority, 8126u16) }; - self.transport(transport) + + Some(AgentTransport::Http { + host: host.to_string(), + port, + }) } /// Set the test session token. @@ -167,18 +245,6 @@ impl AgentClientBuilder { self } - /// Read the timeout from `DD_TRACE_AGENT_TIMEOUT_SECONDS`, falling back to - /// [`DEFAULT_TIMEOUT_MS`] if the variable is unset or unparseable. - pub fn timeout_from_env(mut self) -> Self { - let timeout = std::env::var("DD_TRACE_AGENT_TIMEOUT_SECONDS") - .ok() - .and_then(|v| v.parse::().ok()) - .map(|secs| Duration::from_millis((secs * 1000.0) as u64)) - .unwrap_or(Duration::from_millis(DEFAULT_TIMEOUT_MS)); - self.timeout = Some(timeout); - self - } - /// Override the default retry configuration. /// /// Defaults to [`default_retry_config`]. @@ -390,23 +456,80 @@ mod tests { assert!(result.is_ok()); } + #[cfg(unix)] #[test] #[serial_test::serial] - fn timeout_from_env_uses_default_when_unset() { - std::env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); - let b = AgentClientBuilder::new().timeout_from_env(); - assert_eq!(b.timeout, Some(Duration::from_millis(DEFAULT_TIMEOUT_MS))); + fn auto_detect_uses_dd_trace_agent_url_http() { + std::env::set_var("DD_TRACE_AGENT_URL", "http://myhost:9000"); + std::env::remove_var("DD_AGENT_HOST"); + std::env::remove_var("DD_TRACE_AGENT_PORT"); + let b = AgentClientBuilder::new().auto_detect(); + std::env::remove_var("DD_TRACE_AGENT_URL"); + assert!(matches!( + b.transport, + Some(AgentTransport::Http { ref host, port }) + if host == "myhost" && port == 9000 + )); + } + + #[cfg(unix)] + #[test] + #[serial_test::serial] + fn auto_detect_uses_dd_trace_agent_url_unix() { + std::env::set_var("DD_TRACE_AGENT_URL", "unix:///tmp/test.sock"); + std::env::remove_var("DD_AGENT_HOST"); + std::env::remove_var("DD_TRACE_AGENT_PORT"); + let b = AgentClientBuilder::new().auto_detect(); + std::env::remove_var("DD_TRACE_AGENT_URL"); + assert!(matches!( + b.transport, + Some(AgentTransport::UnixSocket { ref path }) + if path.to_str() == Some("/tmp/test.sock") + )); } + #[cfg(unix)] + #[test] + #[serial_test::serial] + fn auto_detect_uses_dd_agent_host_and_port() { + std::env::remove_var("DD_TRACE_AGENT_URL"); + std::env::set_var("DD_AGENT_HOST", "remotehost"); + std::env::set_var("DD_TRACE_AGENT_PORT", "7777"); + let b = AgentClientBuilder::new().auto_detect(); + std::env::remove_var("DD_AGENT_HOST"); + std::env::remove_var("DD_TRACE_AGENT_PORT"); + assert!(matches!( + b.transport, + Some(AgentTransport::Http { ref host, port }) + if host == "remotehost" && port == 7777 + )); + } + + #[cfg(unix)] #[test] #[serial_test::serial] - fn timeout_from_env_parses_env_var() { + fn auto_detect_reads_timeout_from_env() { + std::env::remove_var("DD_TRACE_AGENT_URL"); + std::env::remove_var("DD_AGENT_HOST"); + std::env::remove_var("DD_TRACE_AGENT_PORT"); std::env::set_var("DD_TRACE_AGENT_TIMEOUT_SECONDS", "5"); - let b = AgentClientBuilder::new().timeout_from_env(); + let b = AgentClientBuilder::new().auto_detect(); std::env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); assert_eq!(b.timeout, Some(Duration::from_secs(5))); } + #[cfg(unix)] + #[test] + #[serial_test::serial] + fn auto_detect_uses_default_timeout_when_unset() { + std::env::remove_var("DD_TRACE_AGENT_URL"); + std::env::remove_var("DD_AGENT_HOST"); + std::env::remove_var("DD_TRACE_AGENT_PORT"); + std::env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); + let b = AgentClientBuilder::new().auto_detect(); + assert_eq!(b.timeout, Some(Duration::from_millis(DEFAULT_TIMEOUT_MS))); + } + #[test] fn extra_headers_stored() { let mut headers = HashMap::new(); diff --git a/libdd-agent-client/src/lib.rs b/libdd-agent-client/src/lib.rs index 8b8e69fad3..37ecdea220 100644 --- a/libdd-agent-client/src/lib.rs +++ b/libdd-agent-client/src/lib.rs @@ -7,32 +7,35 @@ //! //! # Quick start //! +//! Call [`AgentClientBuilder::auto_detect`] to let the client configure transport and timeout +//! from the standard Datadog environment variables (`DD_TRACE_AGENT_URL`, `DD_AGENT_HOST`, +//! `DD_TRACE_AGENT_PORT`, `DD_TRACE_AGENT_TIMEOUT_SECONDS`), falling back to a local Unix +//! socket at `/var/run/datadog/apm.socket` when it exists, and finally to `localhost:8126`. +//! //! ```rust,no_run -//! # async fn example() -> Result<(), libdd_agent_client::BuildError> { +//! # #[cfg(unix)] +//! # fn example() -> Result<(), libdd_agent_client::BuildError> { //! use libdd_agent_client::{AgentClient, LanguageMetadata}; //! //! let client = AgentClient::builder() -//! .http("localhost", 8126) -//! .language_metadata(LanguageMetadata::new( -//! "python", "3.12.1", "CPython", "2.18.0", -//! )) +//! .auto_detect() +//! .language_metadata(LanguageMetadata::new("python", "3.12.1", "CPython", "2.18.0")) //! .build()?; //! # Ok(()) //! # } //! ``` //! -//! # Unix Domain Socket +//! # Explicit transport +//! +//! When the host and port are known at build time, set them directly: //! //! ```rust,no_run -//! # #[cfg(unix)] -//! # async fn example() -> Result<(), libdd_agent_client::BuildError> { +//! # fn example() -> Result<(), libdd_agent_client::BuildError> { //! use libdd_agent_client::{AgentClient, LanguageMetadata}; //! //! let client = AgentClient::builder() -//! .unix_socket("/var/run/datadog/apm.socket") -//! .language_metadata(LanguageMetadata::new( -//! "python", "3.12.1", "CPython", "2.18.0", -//! )) +//! .http("localhost", 8126) +//! .language_metadata(LanguageMetadata::new("python", "3.12.1", "CPython", "2.18.0")) //! .build()?; //! # Ok(()) //! # } From bf8d3a76df0e4d6fbd8d3fcc9209348c16b5acb1 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 22 Apr 2026 18:37:41 +0200 Subject: [PATCH 17/32] style: replace repeated fully-qualified paths with use imports in libdd-agent-client Co-Authored-By: Claude Sonnet 4.6 --- libdd-agent-client/src/builder.rs | 69 ++++++++++++++-------------- libdd-agent-client/src/client.rs | 75 ++++++++++++++----------------- libdd-agent-client/src/lib.rs | 8 +++- 3 files changed, 76 insertions(+), 76 deletions(-) diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index 0650d318f1..69731f02b8 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -4,6 +4,7 @@ //! Builder for [`crate::AgentClient`]. use std::collections::HashMap; +use std::env; #[cfg(unix)] use std::path::PathBuf; use std::time::Duration; @@ -74,10 +75,10 @@ impl Default for AgentTransport { /// /// # Required fields /// -/// - Transport: set via [`AgentClientBuilder::auto_detect`] (reads standard env vars and -/// probes the local socket) or an explicit convenience method -/// ([`AgentClientBuilder::http`], [`AgentClientBuilder::unix_socket`], -/// [`AgentClientBuilder::windows_named_pipe`], [`AgentClientBuilder::transport`]). +/// - Transport: set via [`AgentClientBuilder::auto_detect`] (reads standard env vars and probes the +/// local socket) or an explicit convenience method ([`AgentClientBuilder::http`], +/// [`AgentClientBuilder::unix_socket`], [`AgentClientBuilder::windows_named_pipe`], +/// [`AgentClientBuilder::transport`]). /// - [`AgentClientBuilder::language_metadata`]. /// /// # Test tokens @@ -156,7 +157,7 @@ impl AgentClientBuilder { self.transport = Some(transport); self.timeout = Some( - std::env::var("DD_TRACE_AGENT_TIMEOUT_SECONDS") + env::var("DD_TRACE_AGENT_TIMEOUT_SECONDS") .ok() .and_then(|v| v.parse::().ok()) .map(|secs| Duration::from_millis((secs * 1000.0) as u64)) @@ -166,18 +167,19 @@ impl AgentClientBuilder { self } - /// Read transport from env vars (`DD_TRACE_AGENT_URL`, then `DD_AGENT_HOST`/`DD_TRACE_AGENT_PORT`). - /// Returns `None` when none of the variables are set. + /// Read transport from env vars (`DD_TRACE_AGENT_URL`, then + /// `DD_AGENT_HOST`/`DD_TRACE_AGENT_PORT`). Returns `None` when none of the variables are + /// set. #[cfg(unix)] fn transport_from_env() -> Option { - if let Ok(url) = std::env::var("DD_TRACE_AGENT_URL") { + if let Ok(url) = env::var("DD_TRACE_AGENT_URL") { if let Some(t) = Self::parse_agent_url(&url) { return Some(t); } } - let host = std::env::var("DD_AGENT_HOST").ok(); - let port = std::env::var("DD_TRACE_AGENT_PORT") + let host = env::var("DD_AGENT_HOST").ok(); + let port = env::var("DD_TRACE_AGENT_PORT") .ok() .and_then(|p| p.parse::().ok()); @@ -381,7 +383,7 @@ impl AgentClientBuilder { fn container_headers() -> Vec<(String, String)> { let mut headers = Vec::new(); - if let Ok(env) = std::env::var("DD_EXTERNAL_ENV") { + if let Ok(env) = env::var("DD_EXTERNAL_ENV") { if !env.is_empty() { headers.push(("Datadog-External-Env".to_string(), env)); } @@ -404,6 +406,7 @@ impl AgentClientBuilder { #[cfg(test)] mod tests { use super::*; + use std::env; #[test] fn default_transport_is_localhost_8126() { @@ -460,11 +463,11 @@ mod tests { #[test] #[serial_test::serial] fn auto_detect_uses_dd_trace_agent_url_http() { - std::env::set_var("DD_TRACE_AGENT_URL", "http://myhost:9000"); - std::env::remove_var("DD_AGENT_HOST"); - std::env::remove_var("DD_TRACE_AGENT_PORT"); + env::set_var("DD_TRACE_AGENT_URL", "http://myhost:9000"); + env::remove_var("DD_AGENT_HOST"); + env::remove_var("DD_TRACE_AGENT_PORT"); let b = AgentClientBuilder::new().auto_detect(); - std::env::remove_var("DD_TRACE_AGENT_URL"); + env::remove_var("DD_TRACE_AGENT_URL"); assert!(matches!( b.transport, Some(AgentTransport::Http { ref host, port }) @@ -476,11 +479,11 @@ mod tests { #[test] #[serial_test::serial] fn auto_detect_uses_dd_trace_agent_url_unix() { - std::env::set_var("DD_TRACE_AGENT_URL", "unix:///tmp/test.sock"); - std::env::remove_var("DD_AGENT_HOST"); - std::env::remove_var("DD_TRACE_AGENT_PORT"); + env::set_var("DD_TRACE_AGENT_URL", "unix:///tmp/test.sock"); + env::remove_var("DD_AGENT_HOST"); + env::remove_var("DD_TRACE_AGENT_PORT"); let b = AgentClientBuilder::new().auto_detect(); - std::env::remove_var("DD_TRACE_AGENT_URL"); + env::remove_var("DD_TRACE_AGENT_URL"); assert!(matches!( b.transport, Some(AgentTransport::UnixSocket { ref path }) @@ -492,12 +495,12 @@ mod tests { #[test] #[serial_test::serial] fn auto_detect_uses_dd_agent_host_and_port() { - std::env::remove_var("DD_TRACE_AGENT_URL"); - std::env::set_var("DD_AGENT_HOST", "remotehost"); - std::env::set_var("DD_TRACE_AGENT_PORT", "7777"); + env::remove_var("DD_TRACE_AGENT_URL"); + env::set_var("DD_AGENT_HOST", "remotehost"); + env::set_var("DD_TRACE_AGENT_PORT", "7777"); let b = AgentClientBuilder::new().auto_detect(); - std::env::remove_var("DD_AGENT_HOST"); - std::env::remove_var("DD_TRACE_AGENT_PORT"); + env::remove_var("DD_AGENT_HOST"); + env::remove_var("DD_TRACE_AGENT_PORT"); assert!(matches!( b.transport, Some(AgentTransport::Http { ref host, port }) @@ -509,12 +512,12 @@ mod tests { #[test] #[serial_test::serial] fn auto_detect_reads_timeout_from_env() { - std::env::remove_var("DD_TRACE_AGENT_URL"); - std::env::remove_var("DD_AGENT_HOST"); - std::env::remove_var("DD_TRACE_AGENT_PORT"); - std::env::set_var("DD_TRACE_AGENT_TIMEOUT_SECONDS", "5"); + env::remove_var("DD_TRACE_AGENT_URL"); + env::remove_var("DD_AGENT_HOST"); + env::remove_var("DD_TRACE_AGENT_PORT"); + env::set_var("DD_TRACE_AGENT_TIMEOUT_SECONDS", "5"); let b = AgentClientBuilder::new().auto_detect(); - std::env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); + env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); assert_eq!(b.timeout, Some(Duration::from_secs(5))); } @@ -522,10 +525,10 @@ mod tests { #[test] #[serial_test::serial] fn auto_detect_uses_default_timeout_when_unset() { - std::env::remove_var("DD_TRACE_AGENT_URL"); - std::env::remove_var("DD_AGENT_HOST"); - std::env::remove_var("DD_TRACE_AGENT_PORT"); - std::env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); + env::remove_var("DD_TRACE_AGENT_URL"); + env::remove_var("DD_AGENT_HOST"); + env::remove_var("DD_TRACE_AGENT_PORT"); + env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); let b = AgentClientBuilder::new().auto_detect(); assert_eq!(b.timeout, Some(Duration::from_millis(DEFAULT_TIMEOUT_MS))); } diff --git a/libdd-agent-client/src/client.rs b/libdd-agent-client/src/client.rs index 5131beda10..e3b450464c 100644 --- a/libdd-agent-client/src/client.rs +++ b/libdd-agent-client/src/client.rs @@ -7,7 +7,9 @@ use std::collections::HashMap; use bytes::Bytes; use flate2::{write::GzEncoder, Compression}; -use std::io::Write as _; +use libdd_http_client::{HttpClient, HttpClientError, HttpMethod, HttpRequest}; +use serde_json::{from_slice, Value}; +use std::io::{Error, ErrorKind, Write as _}; use crate::{ agent_info::AgentInfo, @@ -35,16 +37,13 @@ use crate::{ /// /// [`LanguageMetadata`]: crate::LanguageMetadata pub struct AgentClient { - http: libdd_http_client::HttpClient, + http: HttpClient, base_url: String, static_headers: Vec<(String, String)>, } impl AgentClient { - pub(crate) fn new( - http: libdd_http_client::HttpClient, - static_headers: Vec<(String, String)>, - ) -> Self { + pub(crate) fn new(http: HttpClient, static_headers: Vec<(String, String)>) -> Self { let base_url = http.config().base_url().to_string(); Self { http, @@ -77,13 +76,12 @@ impl AgentClient { }; let url = format!("{}{}", self.base_url, path); - let mut request = - libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Put, url) - .with_body(payload) - .with_headers(self.static_headers.iter().cloned()) - .with_header("Content-Type", content_type) - .with_header("X-Datadog-Trace-Count", trace_count.to_string()) - .with_header("Datadog-Send-Real-Http-Status", "true"); + let mut request = HttpRequest::new(HttpMethod::Put, url) + .with_body(payload) + .with_headers(self.static_headers.iter().cloned()) + .with_header("Content-Type", content_type) + .with_header("X-Datadog-Trace-Count", trace_count.to_string()) + .with_header("Datadog-Send-Real-Http-Status", "true"); if opts.computed_top_level { request = request.with_header("Datadog-Client-Computed-Top-Level", "yes"); @@ -108,7 +106,7 @@ impl AgentClient { /// Send span stats (APM concentrator buckets) to `/v0.6/stats`. pub async fn send_stats(&self, payload: Bytes) -> Result<(), SendError> { let url = format!("{}/v0.6/stats", self.base_url); - let request = libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Put, url) + let request = HttpRequest::new(HttpMethod::Put, url) .with_body(payload) .with_headers(self.static_headers.iter().cloned()) .with_header("Content-Type", "application/msgpack"); @@ -125,7 +123,7 @@ impl AgentClient { let compressed = gzip_compress(payload)?; let url = format!("{}/v0.1/pipeline_stats", self.base_url); - let request = libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Put, url) + let request = HttpRequest::new(HttpMethod::Put, url) .with_body(compressed) .with_headers(self.static_headers.iter().cloned()) .with_header("Content-Type", "application/msgpack") @@ -139,7 +137,7 @@ impl AgentClient { /// (`telemetry/proxy/api/v2/apmtelemetry`). pub async fn send_telemetry(&self, req: TelemetryRequest) -> Result<(), SendError> { let url = format!("{}/telemetry/proxy/api/v2/apmtelemetry", self.base_url); - let request = libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Post, url) + let request = HttpRequest::new(HttpMethod::Post, url) .with_body(req.body) .with_headers(self.static_headers.iter().cloned()) .with_header("Content-Type", "application/json") @@ -167,7 +165,7 @@ impl AgentClient { content_type: &str, ) -> Result<(), SendError> { let url = format!("{}{}", self.base_url, path); - let request = libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Post, url) + let request = HttpRequest::new(HttpMethod::Post, url) .with_body(payload) .with_headers(self.static_headers.iter().cloned()) .with_header("Content-Type", content_type) @@ -186,11 +184,11 @@ impl AgentClient { version: Option, endpoints: Option>, client_drop_p0s: Option, - config: Option, + config: Option, } let url = format!("{}/info", self.base_url); - let request = libdd_http_client::HttpRequest::new(libdd_http_client::HttpMethod::Get, url) + let request = HttpRequest::new(HttpMethod::Get, url) .with_headers(self.static_headers.iter().cloned()); let response = self.http.send(request).await.map_err(map_http_error)?; @@ -218,13 +216,13 @@ impl AgentClient { let container_tags_hash = header("datadog-container-tags-hash"); let state_hash = header("datadog-agent-state"); - let info: InfoResponse = serde_json::from_slice(response.body()) - .map_err(|e| SendError::Encoding(e.to_string()))?; + let info: InfoResponse = + from_slice(response.body()).map_err(|e| SendError::Encoding(e.to_string()))?; Ok(Some(AgentInfo { endpoints: info.endpoints.unwrap_or_default(), client_drop_p0s: info.client_drop_p0s.unwrap_or(false), - config: info.config.unwrap_or(serde_json::Value::Null), + config: info.config.unwrap_or(Value::Null), version: info.version, container_tags_hash, state_hash, @@ -239,7 +237,7 @@ fn parse_rate_by_service(body: &Bytes) -> Option> { rate_by_service: Option>, } - serde_json::from_slice::(body) + from_slice::(body) .ok() .and_then(|r| r.rate_by_service) } @@ -268,28 +266,23 @@ fn gzip_compress(payload: Bytes) -> Result { Ok(Bytes::from(compressed)) } -/// Map a [`libdd_http_client::HttpClientError`] to a [`SendError`]. -fn map_http_error(e: libdd_http_client::HttpClientError) -> SendError { +/// Map a [`HttpClientError`] to a [`SendError`]. +fn map_http_error(e: HttpClientError) -> SendError { match e { - libdd_http_client::HttpClientError::ConnectionFailed(s) => SendError::Transport( - std::io::Error::new(std::io::ErrorKind::ConnectionRefused, s), - ), - libdd_http_client::HttpClientError::TimedOut => SendError::Transport(std::io::Error::new( - std::io::ErrorKind::TimedOut, - "request timed out", - )), - libdd_http_client::HttpClientError::IoError(s) => { - SendError::Transport(std::io::Error::other(s)) + HttpClientError::ConnectionFailed(s) => { + SendError::Transport(Error::new(ErrorKind::ConnectionRefused, s)) } - libdd_http_client::HttpClientError::InvalidConfig(s) => { - SendError::Transport(std::io::Error::new(std::io::ErrorKind::InvalidInput, s)) + HttpClientError::TimedOut => { + SendError::Transport(Error::new(ErrorKind::TimedOut, "request timed out")) } - libdd_http_client::HttpClientError::RequestFailed { status, body } => { - SendError::HttpError { - status, - body: Bytes::from(body), - } + HttpClientError::IoError(s) => SendError::Transport(Error::other(s)), + HttpClientError::InvalidConfig(s) => { + SendError::Transport(Error::new(ErrorKind::InvalidInput, s)) } + HttpClientError::RequestFailed { status, body } => SendError::HttpError { + status, + body: Bytes::from(body), + }, } } diff --git a/libdd-agent-client/src/lib.rs b/libdd-agent-client/src/lib.rs index 37ecdea220..605c6d03ca 100644 --- a/libdd-agent-client/src/lib.rs +++ b/libdd-agent-client/src/lib.rs @@ -19,7 +19,9 @@ //! //! let client = AgentClient::builder() //! .auto_detect() -//! .language_metadata(LanguageMetadata::new("python", "3.12.1", "CPython", "2.18.0")) +//! .language_metadata(LanguageMetadata::new( +//! "python", "3.12.1", "CPython", "2.18.0", +//! )) //! .build()?; //! # Ok(()) //! # } @@ -35,7 +37,9 @@ //! //! let client = AgentClient::builder() //! .http("localhost", 8126) -//! .language_metadata(LanguageMetadata::new("python", "3.12.1", "CPython", "2.18.0")) +//! .language_metadata(LanguageMetadata::new( +//! "python", "3.12.1", "CPython", "2.18.0", +//! )) //! .build()?; //! # Ok(()) //! # } From 98790be744d2c9fb8ea25fa1965eaba6bcd751a5 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Wed, 22 Apr 2026 19:06:38 +0200 Subject: [PATCH 18/32] style: inline single-use variables used immediately in the next call chain Co-Authored-By: Claude Sonnet 4.6 --- libdd-agent-client/src/client.rs | 52 +++++++++++----------- libdd-agent-client/tests/agent_info.rs | 17 ++++--- libdd-agent-client/tests/evp_event.rs | 3 +- libdd-agent-client/tests/pipeline_stats.rs | 6 +-- libdd-agent-client/tests/static_headers.rs | 3 +- libdd-agent-client/tests/stats.rs | 6 +-- libdd-agent-client/tests/telemetry.rs | 6 +-- libdd-agent-client/tests/traces.rs | 21 +++------ 8 files changed, 51 insertions(+), 63 deletions(-) diff --git a/libdd-agent-client/src/client.rs b/libdd-agent-client/src/client.rs index e3b450464c..72fc04b18b 100644 --- a/libdd-agent-client/src/client.rs +++ b/libdd-agent-client/src/client.rs @@ -75,8 +75,7 @@ impl AgentClient { TraceFormat::MsgpackV4 => ("/v0.4/traces", "application/msgpack"), }; - let url = format!("{}{}", self.base_url, path); - let mut request = HttpRequest::new(HttpMethod::Put, url) + let mut request = HttpRequest::new(HttpMethod::Put, format!("{}{}", self.base_url, path)) .with_body(payload) .with_headers(self.static_headers.iter().cloned()) .with_header("Content-Type", content_type) @@ -105,8 +104,7 @@ impl AgentClient { /// Send span stats (APM concentrator buckets) to `/v0.6/stats`. pub async fn send_stats(&self, payload: Bytes) -> Result<(), SendError> { - let url = format!("{}/v0.6/stats", self.base_url); - let request = HttpRequest::new(HttpMethod::Put, url) + let request = HttpRequest::new(HttpMethod::Put, format!("{}/v0.6/stats", self.base_url)) .with_body(payload) .with_headers(self.static_headers.iter().cloned()) .with_header("Content-Type", "application/msgpack"); @@ -120,14 +118,14 @@ impl AgentClient { /// The payload is **always** gzip-compressed regardless of the client-level compression /// setting. This is a protocol requirement of the data-streams endpoint. pub async fn send_pipeline_stats(&self, payload: Bytes) -> Result<(), SendError> { - let compressed = gzip_compress(payload)?; - - let url = format!("{}/v0.1/pipeline_stats", self.base_url); - let request = HttpRequest::new(HttpMethod::Put, url) - .with_body(compressed) - .with_headers(self.static_headers.iter().cloned()) - .with_header("Content-Type", "application/msgpack") - .with_header("Content-Encoding", "gzip"); + let request = HttpRequest::new( + HttpMethod::Put, + format!("{}/v0.1/pipeline_stats", self.base_url), + ) + .with_body(gzip_compress(payload)?) + .with_headers(self.static_headers.iter().cloned()) + .with_header("Content-Type", "application/msgpack") + .with_header("Content-Encoding", "gzip"); let response = self.http.send(request).await.map_err(map_http_error)?; check_status(response) @@ -136,17 +134,19 @@ impl AgentClient { /// Send a telemetry event to the agent's telemetry proxy /// (`telemetry/proxy/api/v2/apmtelemetry`). pub async fn send_telemetry(&self, req: TelemetryRequest) -> Result<(), SendError> { - let url = format!("{}/telemetry/proxy/api/v2/apmtelemetry", self.base_url); - let request = HttpRequest::new(HttpMethod::Post, url) - .with_body(req.body) - .with_headers(self.static_headers.iter().cloned()) - .with_header("Content-Type", "application/json") - .with_header("DD-Telemetry-Request-Type", &req.request_type) - .with_header("DD-Telemetry-API-Version", &req.api_version) - .with_header( - "DD-Telemetry-Debug-Enabled", - if req.debug { "true" } else { "false" }, - ); + let request = HttpRequest::new( + HttpMethod::Post, + format!("{}/telemetry/proxy/api/v2/apmtelemetry", self.base_url), + ) + .with_body(req.body) + .with_headers(self.static_headers.iter().cloned()) + .with_header("Content-Type", "application/json") + .with_header("DD-Telemetry-Request-Type", &req.request_type) + .with_header("DD-Telemetry-API-Version", &req.api_version) + .with_header( + "DD-Telemetry-Debug-Enabled", + if req.debug { "true" } else { "false" }, + ); let response = self.http.send(request).await.map_err(map_http_error)?; check_status(response) @@ -164,8 +164,7 @@ impl AgentClient { payload: Bytes, content_type: &str, ) -> Result<(), SendError> { - let url = format!("{}{}", self.base_url, path); - let request = HttpRequest::new(HttpMethod::Post, url) + let request = HttpRequest::new(HttpMethod::Post, format!("{}{}", self.base_url, path)) .with_body(payload) .with_headers(self.static_headers.iter().cloned()) .with_header("Content-Type", content_type) @@ -187,8 +186,7 @@ impl AgentClient { config: Option, } - let url = format!("{}/info", self.base_url); - let request = HttpRequest::new(HttpMethod::Get, url) + let request = HttpRequest::new(HttpMethod::Get, format!("{}/info", self.base_url)) .with_headers(self.static_headers.iter().cloned()); let response = self.http.send(request).await.map_err(map_http_error)?; diff --git a/libdd-agent-client/tests/agent_info.rs b/libdd-agent-client/tests/agent_info.rs index b0f26442c0..a81101a472 100644 --- a/libdd-agent-client/tests/agent_info.rs +++ b/libdd-agent-client/tests/agent_info.rs @@ -20,8 +20,11 @@ async fn parses_info_response() { ); }); - let client = common::client_for(&server); - let info = client.agent_info().await.unwrap().expect("expected Some"); + let info = common::client_for(&server) + .agent_info() + .await + .unwrap() + .expect("expected Some"); assert_eq!(info.version.as_deref(), Some("7.50.0")); assert!(info.endpoints.contains(&"/v0.5/traces".to_string())); @@ -36,8 +39,7 @@ async fn returns_none_on_404() { then.status(404).body("not found"); }); - let client = common::client_for(&server); - let result = client.agent_info().await.unwrap(); + let result = common::client_for(&server).agent_info().await.unwrap(); assert!(result.is_none()); } @@ -51,7 +53,10 @@ async fn extracts_container_tags_hash_header() { .body(r#"{"endpoints":[],"client_drop_p0s":false}"#); }); - let client = common::client_for(&server); - let info = client.agent_info().await.unwrap().unwrap(); + let info = common::client_for(&server) + .agent_info() + .await + .unwrap() + .unwrap(); assert_eq!(info.container_tags_hash.as_deref(), Some("abc123")); } diff --git a/libdd-agent-client/tests/evp_event.rs b/libdd-agent-client/tests/evp_event.rs index 7176ffe164..9df3167520 100644 --- a/libdd-agent-client/tests/evp_event.rs +++ b/libdd-agent-client/tests/evp_event.rs @@ -16,8 +16,7 @@ async fn posts_to_path_with_subdomain_header() { then.status(200).body(""); }); - let client = common::client_for(&server); - client + common::client_for(&server) .send_evp_event( "event-platform-intake", "/api/v2/exposures", diff --git a/libdd-agent-client/tests/pipeline_stats.rs b/libdd-agent-client/tests/pipeline_stats.rs index cf3b179d55..7df264b637 100644 --- a/libdd-agent-client/tests/pipeline_stats.rs +++ b/libdd-agent-client/tests/pipeline_stats.rs @@ -14,8 +14,7 @@ async fn puts_to_correct_endpoint() { then.status(200).body(""); }); - let client = common::client_for(&server); - client + common::client_for(&server) .send_pipeline_stats(Bytes::from_static(b"\x80")) .await .unwrap(); @@ -33,8 +32,7 @@ async fn sets_gzip_encoding() { then.status(200).body(""); }); - let client = common::client_for(&server); - client + common::client_for(&server) .send_pipeline_stats(Bytes::from_static(b"\x80")) .await .unwrap(); diff --git a/libdd-agent-client/tests/static_headers.rs b/libdd-agent-client/tests/static_headers.rs index c0a22831f1..3d1ef30dfd 100644 --- a/libdd-agent-client/tests/static_headers.rs +++ b/libdd-agent-client/tests/static_headers.rs @@ -21,8 +21,7 @@ async fn language_metadata_headers_injected_on_all_requests() { then.status(200).body(r#"{}"#); }); - let client = common::client_for(&server); - client + common::client_for(&server) .send_traces( Bytes::from_static(b""), 0, diff --git a/libdd-agent-client/tests/stats.rs b/libdd-agent-client/tests/stats.rs index 83aafd9168..81a3839780 100644 --- a/libdd-agent-client/tests/stats.rs +++ b/libdd-agent-client/tests/stats.rs @@ -14,8 +14,7 @@ async fn puts_to_v06_stats() { then.status(200).body(""); }); - let client = common::client_for(&server); - client + common::client_for(&server) .send_stats(Bytes::from_static(b"\x80")) .await .unwrap(); @@ -33,8 +32,7 @@ async fn sets_msgpack_content_type() { then.status(200).body(""); }); - let client = common::client_for(&server); - client + common::client_for(&server) .send_stats(Bytes::from_static(b"\x80")) .await .unwrap(); diff --git a/libdd-agent-client/tests/telemetry.rs b/libdd-agent-client/tests/telemetry.rs index f5d99e3337..e663f77e49 100644 --- a/libdd-agent-client/tests/telemetry.rs +++ b/libdd-agent-client/tests/telemetry.rs @@ -16,8 +16,7 @@ async fn posts_to_telemetry_proxy() { then.status(202).body(""); }); - let client = common::client_for(&server); - client + common::client_for(&server) .send_telemetry(TelemetryRequest { request_type: "app-started".to_string(), api_version: "v2".to_string(), @@ -42,8 +41,7 @@ async fn injects_per_request_headers() { then.status(202).body(""); }); - let client = common::client_for(&server); - client + common::client_for(&server) .send_telemetry(TelemetryRequest { request_type: "app-started".to_string(), api_version: "v2".to_string(), diff --git a/libdd-agent-client/tests/traces.rs b/libdd-agent-client/tests/traces.rs index 1f833135a0..7dd0fe3791 100644 --- a/libdd-agent-client/tests/traces.rs +++ b/libdd-agent-client/tests/traces.rs @@ -15,8 +15,7 @@ async fn v5_puts_to_correct_endpoint() { then.status(200).body(r#"{"rate_by_service":{}}"#); }); - let client = common::client_for(&server); - let resp = client + let resp = common::client_for(&server) .send_traces( Bytes::from_static(b"\x91\x90"), 1, @@ -38,8 +37,7 @@ async fn v4_puts_to_v4_endpoint() { then.status(200).body(r#"{}"#); }); - let client = common::client_for(&server); - client + common::client_for(&server) .send_traces( Bytes::from_static(b"\x91\x90"), 1, @@ -62,8 +60,7 @@ async fn injects_trace_count_header() { then.status(200).body(r#"{}"#); }); - let client = common::client_for(&server); - client + common::client_for(&server) .send_traces( Bytes::from_static(b"\x91\x90"), 42, @@ -86,8 +83,7 @@ async fn injects_send_real_http_status_header() { then.status(200).body(r#"{}"#); }); - let client = common::client_for(&server); - client + common::client_for(&server) .send_traces( Bytes::from_static(b""), 0, @@ -110,8 +106,7 @@ async fn computed_top_level_injects_header() { then.status(200).body(r#"{}"#); }); - let client = common::client_for(&server); - client + common::client_for(&server) .send_traces( Bytes::from_static(b""), 0, @@ -135,8 +130,7 @@ async fn parses_rate_by_service() { .body(r#"{"rate_by_service":{"service:env":0.75}}"#); }); - let client = common::client_for(&server); - let resp = client + let resp = common::client_for(&server) .send_traces( Bytes::from_static(b""), 0, @@ -162,8 +156,7 @@ async fn returns_http_error_on_5xx() { then.status(503).body("overloaded"); }); - let client = common::client_for(&server); - let err = client + let err = common::client_for(&server) .send_traces( Bytes::from_static(b""), 0, From b3c3310d247b0a51af8736b7ee7113fa21453075 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Fri, 24 Apr 2026 15:31:47 +0200 Subject: [PATCH 19/32] refactor: remove inline from builder, add one in other parts --- libdd-agent-client/src/builder.rs | 9 --------- libdd-agent-client/src/language_metadata.rs | 1 + 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index 69731f02b8..5753351bae 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -98,13 +98,11 @@ pub struct AgentClientBuilder { impl AgentClientBuilder { /// Create a new builder with default settings. - #[inline] pub fn new() -> Self { Self::default() } /// Set the transport configuration. - #[inline] pub fn transport(mut self, transport: AgentTransport) -> Self { self.transport = Some(transport); self @@ -120,7 +118,6 @@ impl AgentClientBuilder { /// Convenience: Unix Domain Socket. #[cfg(unix)] - #[inline] pub fn unix_socket(self, path: impl Into) -> Self { self.transport(AgentTransport::UnixSocket { path: path.into() }) } @@ -232,7 +229,6 @@ impl AgentClientBuilder { /// Set the test session token. /// /// When set, `x-datadog-test-session-token: ` is injected on every request. - #[inline] pub fn test_agent_session_token(mut self, token: impl Into) -> Self { self.test_token = Some(token.into()); self @@ -241,7 +237,6 @@ impl AgentClientBuilder { /// Set the request timeout. /// /// Defaults to [`DEFAULT_TIMEOUT_MS`] (2 000 ms) when not set. - #[inline] pub fn timeout(mut self, timeout: Duration) -> Self { self.timeout = Some(timeout); self @@ -250,14 +245,12 @@ impl AgentClientBuilder { /// Override the default retry configuration. /// /// Defaults to [`default_retry_config`]. - #[inline] pub fn retry(mut self, config: RetryConfig) -> Self { self.retry = Some(config); self } /// Set the language/runtime metadata injected into every request. Required. - #[inline] pub fn language_metadata(mut self, meta: LanguageMetadata) -> Self { self.language = Some(meta); self @@ -269,7 +262,6 @@ impl AgentClientBuilder { /// second connection when keep-alive is enabled. The default of `false` is correct for all /// periodic-flush writers (traces, stats, data streams). Set to `true` only for /// high-frequency continuous senders (e.g. a streaming profiling exporter). - #[inline] pub fn use_keep_alive(mut self, enabled: bool) -> Self { self.keep_alive = enabled; self @@ -283,7 +275,6 @@ impl AgentClientBuilder { // baked in; only the opt-in client-level `gzip(level)` builder knob is deferred. /// Additional custom headers to inject. - #[inline] pub fn extra_headers(mut self, headers: HashMap) -> Self { self.extra_headers = headers; self diff --git a/libdd-agent-client/src/language_metadata.rs b/libdd-agent-client/src/language_metadata.rs index 0e8d912c70..eb599f90ee 100644 --- a/libdd-agent-client/src/language_metadata.rs +++ b/libdd-agent-client/src/language_metadata.rs @@ -36,6 +36,7 @@ impl LanguageMetadata { /// Produces the `User-Agent` string passed to `Endpoint::to_request_builder()`. /// /// Format: `dd-trace-/`, e.g. `dd-trace-python/2.18.0`. + #[inline] pub fn user_agent(&self) -> String { format!("dd-trace-{}/{}", self.language, self.tracer_version) } From fb3b67c35bf3c3d182cb69b7ea261d54977cf425 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Fri, 24 Apr 2026 15:32:11 +0200 Subject: [PATCH 20/32] refactor: bespoke conversion method -> impl From instead --- libdd-agent-client/src/client.rs | 36 +++++++------------------------- libdd-agent-client/src/error.rs | 23 ++++++++++++++++++++ 2 files changed, 31 insertions(+), 28 deletions(-) diff --git a/libdd-agent-client/src/client.rs b/libdd-agent-client/src/client.rs index 72fc04b18b..d63cf73e60 100644 --- a/libdd-agent-client/src/client.rs +++ b/libdd-agent-client/src/client.rs @@ -7,9 +7,9 @@ use std::collections::HashMap; use bytes::Bytes; use flate2::{write::GzEncoder, Compression}; -use libdd_http_client::{HttpClient, HttpClientError, HttpMethod, HttpRequest}; +use libdd_http_client::{HttpClient, HttpMethod, HttpRequest}; use serde_json::{from_slice, Value}; -use std::io::{Error, ErrorKind, Write as _}; +use std::io::Write as _; use crate::{ agent_info::AgentInfo, @@ -86,7 +86,7 @@ impl AgentClient { request = request.with_header("Datadog-Client-Computed-Top-Level", "yes"); } - let response = self.http.send(request).await.map_err(map_http_error)?; + let response = self.http.send(request).await?; if response.status_code() >= 400 { return Err(SendError::HttpError { @@ -109,7 +109,7 @@ impl AgentClient { .with_headers(self.static_headers.iter().cloned()) .with_header("Content-Type", "application/msgpack"); - let response = self.http.send(request).await.map_err(map_http_error)?; + let response = self.http.send(request).await?; check_status(response) } @@ -127,7 +127,7 @@ impl AgentClient { .with_header("Content-Type", "application/msgpack") .with_header("Content-Encoding", "gzip"); - let response = self.http.send(request).await.map_err(map_http_error)?; + let response = self.http.send(request).await?; check_status(response) } @@ -148,7 +148,7 @@ impl AgentClient { if req.debug { "true" } else { "false" }, ); - let response = self.http.send(request).await.map_err(map_http_error)?; + let response = self.http.send(request).await?; check_status(response) } @@ -170,7 +170,7 @@ impl AgentClient { .with_header("Content-Type", content_type) .with_header("X-Datadog-EVP-Subdomain", subdomain); - let response = self.http.send(request).await.map_err(map_http_error)?; + let response = self.http.send(request).await?; check_status(response) } @@ -189,7 +189,7 @@ impl AgentClient { let request = HttpRequest::new(HttpMethod::Get, format!("{}/info", self.base_url)) .with_headers(self.static_headers.iter().cloned()); - let response = self.http.send(request).await.map_err(map_http_error)?; + let response = self.http.send(request).await?; if response.status_code() == 404 { return Ok(None); @@ -264,26 +264,6 @@ fn gzip_compress(payload: Bytes) -> Result { Ok(Bytes::from(compressed)) } -/// Map a [`HttpClientError`] to a [`SendError`]. -fn map_http_error(e: HttpClientError) -> SendError { - match e { - HttpClientError::ConnectionFailed(s) => { - SendError::Transport(Error::new(ErrorKind::ConnectionRefused, s)) - } - HttpClientError::TimedOut => { - SendError::Transport(Error::new(ErrorKind::TimedOut, "request timed out")) - } - HttpClientError::IoError(s) => SendError::Transport(Error::other(s)), - HttpClientError::InvalidConfig(s) => { - SendError::Transport(Error::new(ErrorKind::InvalidInput, s)) - } - HttpClientError::RequestFailed { status, body } => SendError::HttpError { - status, - body: Bytes::from(body), - }, - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/libdd-agent-client/src/error.rs b/libdd-agent-client/src/error.rs index 413d3a06d1..94009a7889 100644 --- a/libdd-agent-client/src/error.rs +++ b/libdd-agent-client/src/error.rs @@ -3,7 +3,9 @@ //! Error types for [`crate::AgentClient`]. +use std::io::{Error, ErrorKind}; use bytes::Bytes; +use libdd_http_client::HttpClientError; use thiserror::Error; /// Errors that can occur when building an [`crate::AgentClient`]. @@ -44,3 +46,24 @@ pub enum SendError { #[error("encoding error: {0}")] Encoding(String), } + +impl From for SendError { + fn from(err: HttpClientError) -> Self { + match err { + HttpClientError::ConnectionFailed(s) => { + SendError::Transport(Error::new(ErrorKind::ConnectionRefused, s)) + } + HttpClientError::TimedOut => { + SendError::Transport(Error::new(ErrorKind::TimedOut, "request timed out")) + } + HttpClientError::IoError(s) => SendError::Transport(Error::other(s)), + HttpClientError::InvalidConfig(s) => { + SendError::Transport(Error::new(ErrorKind::InvalidInput, s)) + } + HttpClientError::RequestFailed { status, body } => SendError::HttpError { + status, + body: Bytes::from(body), + }, + } + } +} From b2c4293a14ad5b5b3ec48f347cd5c71e1a1f4968 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 27 Apr 2026 15:48:40 +0200 Subject: [PATCH 21/32] style: formatting --- libdd-agent-client/src/error.rs | 2 +- libdd-http-client/src/request.rs | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/libdd-agent-client/src/error.rs b/libdd-agent-client/src/error.rs index 94009a7889..cacb7da46f 100644 --- a/libdd-agent-client/src/error.rs +++ b/libdd-agent-client/src/error.rs @@ -3,9 +3,9 @@ //! Error types for [`crate::AgentClient`]. -use std::io::{Error, ErrorKind}; use bytes::Bytes; use libdd_http_client::HttpClientError; +use std::io::{Error, ErrorKind}; use thiserror::Error; /// Errors that can occur when building an [`crate::AgentClient`]. diff --git a/libdd-http-client/src/request.rs b/libdd-http-client/src/request.rs index 3d38384492..5178d10558 100644 --- a/libdd-http-client/src/request.rs +++ b/libdd-http-client/src/request.rs @@ -136,8 +136,13 @@ impl HttpRequest { /// Append headers to this request. #[inline] - pub fn with_headers<'a, K, V>(mut self, it: impl IntoIterator) -> Self where K: Into, V: Into { - self.headers.extend(it.into_iter().map(|(k, v)| (k.into(), v.into()))); + pub fn with_headers<'a, K, V>(mut self, it: impl IntoIterator) -> Self + where + K: Into, + V: Into, + { + self.headers + .extend(it.into_iter().map(|(k, v)| (k.into(), v.into()))); self } From 8266ee9ed24e2080eedecffdcf9dd784ba6f6906 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 27 Apr 2026 15:52:58 +0200 Subject: [PATCH 22/32] style: fix clippy warning (useless lifetime parameter) --- libdd-http-client/src/request.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libdd-http-client/src/request.rs b/libdd-http-client/src/request.rs index 5178d10558..888eaf9c4e 100644 --- a/libdd-http-client/src/request.rs +++ b/libdd-http-client/src/request.rs @@ -136,7 +136,7 @@ impl HttpRequest { /// Append headers to this request. #[inline] - pub fn with_headers<'a, K, V>(mut self, it: impl IntoIterator) -> Self + pub fn with_headers(mut self, it: impl IntoIterator) -> Self where K: Into, V: Into, From f97c7809644e1bc41b6dc6164e537bfa1f2b2620 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 27 Apr 2026 15:57:16 +0200 Subject: [PATCH 23/32] chore: add missing entries for new agent client crate --- .github/CODEOWNERS | 1 + tools/docker/Dockerfile.build | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e42c6ce226..694119c831 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -45,6 +45,7 @@ libdd-data-pipeline*/ @DataDog/libdatadog-apm libdd-ddsketch*/ @DataDog/libdatadog-apm @DataDog/apm-common-components-core libdd-dogstatsd-client @DataDog/apm-common-components-core libdd-http-client @DataDog/apm-common-components-core +libdd-agent-client @DataDog/apm-common-components-core libdd-library-config*/ @DataDog/apm-sdk-capabilities-rust libdd-log*/ @DataDog/apm-common-components-core libdd-otel-thread-ctx/ @DataDog/apm-common-components-core diff --git a/tools/docker/Dockerfile.build b/tools/docker/Dockerfile.build index ec81632518..caae0f5a89 100644 --- a/tools/docker/Dockerfile.build +++ b/tools/docker/Dockerfile.build @@ -87,6 +87,7 @@ COPY "libdd-log/Cargo.toml" "libdd-log/" COPY "libdd-log-ffi/Cargo.toml" "libdd-log-ffi/" COPY "libdd-dogstatsd-client/Cargo.toml" "libdd-dogstatsd-client/" COPY "libdd-http-client/Cargo.toml" "libdd-http-client/" +COPY "libdd-agent-client/Cargo.toml" "libdd-agent-client/" COPY "libdd-library-config-ffi/Cargo.toml" "libdd-library-config-ffi/" COPY "libdd-library-config/Cargo.toml" "libdd-library-config/" COPY "datadog-live-debugger/Cargo.toml" "datadog-live-debugger/" From f087b95e968896ba36519b4f0417bfb5c2794954 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 27 Apr 2026 16:02:36 +0200 Subject: [PATCH 24/32] fix: fix outdated libdd-common dep version --- libdd-agent-client/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libdd-agent-client/Cargo.toml b/libdd-agent-client/Cargo.toml index f8f80c17cb..40ec5eb949 100644 --- a/libdd-agent-client/Cargo.toml +++ b/libdd-agent-client/Cargo.toml @@ -23,7 +23,7 @@ serde_json = "1.0" thiserror = "2" tokio = { version = "1.23", features = ["rt"] } libdd-http-client = { path = "../libdd-http-client" } -libdd-common = { version = "3.0.2", path = "../libdd-common", default-features = false } +libdd-common = { version = "4.0.0", path = "../libdd-common", default-features = false } [dev-dependencies] httpmock = "0.8.0-alpha.1" From 6cca9e9e46779e638b4da81a6a9872a02cb74e2d Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 27 Apr 2026 16:18:12 +0200 Subject: [PATCH 25/32] chore: update Cargo.lock --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 378adf88d6..6cd84d525d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2812,7 +2812,7 @@ checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" [[package]] name = "libdd-agent-client" -version = "31.0.0" +version = "32.0.0" dependencies = [ "bytes", "flate2", From ee3fc45e2411e8b95e98c670ca1510698be9b911 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 27 Apr 2026 16:19:57 +0200 Subject: [PATCH 26/32] fix: fix std import --- libdd-agent-client/src/builder.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index 5753351bae..f3e3ad5c12 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -3,13 +3,11 @@ //! Builder for [`crate::AgentClient`]. -use std::collections::HashMap; -use std::env; +#[cfg(windows)] +use std::ffi::OsString; #[cfg(unix)] use std::path::PathBuf; -use std::time::Duration; -#[cfg(windows)] -use OsString; +use std::{collections::HashMap, env, time::Duration}; use libdd_http_client::RetryConfig; @@ -372,6 +370,8 @@ impl AgentClientBuilder { /// Read container / entity-ID headers from the host environment. Always injects /// `Datadog-External-Env` when `DD_EXTERNAL_ENV` is set. fn container_headers() -> Vec<(String, String)> { + use libdd_common::entity_id; + let mut headers = Vec::new(); if let Ok(env) = env::var("DD_EXTERNAL_ENV") { @@ -380,8 +380,6 @@ impl AgentClientBuilder { } } - use libdd_common::entity_id; - if let Some(container_id) = entity_id::get_container_id() { headers.push(("Datadog-Container-Id".to_string(), container_id.to_owned())); } From a8764079debd6c37570090e0ae536dca1d27265b Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 27 Apr 2026 16:32:54 +0200 Subject: [PATCH 27/32] chore: fix missing version for internal dep --- libdd-agent-client/Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libdd-agent-client/Cargo.toml b/libdd-agent-client/Cargo.toml index 40ec5eb949..915355bcda 100644 --- a/libdd-agent-client/Cargo.toml +++ b/libdd-agent-client/Cargo.toml @@ -22,8 +22,8 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "2" tokio = { version = "1.23", features = ["rt"] } -libdd-http-client = { path = "../libdd-http-client" } -libdd-common = { version = "4.0.0", path = "../libdd-common", default-features = false } +libdd-http-client = { version = "32.0", path = "../libdd-http-client" } +libdd-common = { version = "4.0", path = "../libdd-common", default-features = false } [dev-dependencies] httpmock = "0.8.0-alpha.1" From 4bedf59e2c0e24cc229a9e4f5dceaa48d4c67f10 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 27 Apr 2026 17:46:44 +0200 Subject: [PATCH 28/32] fix: confusion around keep-alive and connection pooling --- libdd-agent-client/src/builder.rs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index f3e3ad5c12..a49f97c114 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -90,7 +90,7 @@ pub struct AgentClientBuilder { timeout: Option, language: Option, retry: Option, - keep_alive: bool, + allow_connection_pooling: bool, extra_headers: HashMap, } @@ -254,14 +254,20 @@ impl AgentClientBuilder { self } - /// Enable or disable HTTP keep-alive. Defaults to `false`. + /// Allow connection pooling. Defaults to `false`. + /// + /// Note that whether pooling is actually used depends on the HTTP backend of + /// [libdd_http_client], though both currently available backends (reqwest and hyper) support + /// pooling. This setting should be understood as: if set to `false`, no connection pooling will + /// happen. If set to `true`, connection pooling may happen, at the discretion of the HTTP + /// backend. /// /// The Datadog agent has a low keep-alive timeout that causes "pipe closed" errors on every - /// second connection when keep-alive is enabled. The default of `false` is correct for all - /// periodic-flush writers (traces, stats, data streams). Set to `true` only for - /// high-frequency continuous senders (e.g. a streaming profiling exporter). - pub fn use_keep_alive(mut self, enabled: bool) -> Self { - self.keep_alive = enabled; + /// second connection. The default of `false` is correct for all periodic-flush writers (traces, + /// stats, data streams). Set to `true` only for high-frequency continuous senders (e.g. a + /// streaming profiling exporter). + pub fn allow_connection_pooling(mut self, enabled: bool) -> Self { + self.allow_connection_pooling = enabled; self } @@ -421,7 +427,7 @@ mod tests { let b = AgentClientBuilder::new(); assert!(b.transport.is_none()); assert!(b.language.is_none()); - assert!(!b.keep_alive); + assert!(!b.allow_connection_pooling); } #[test] From 78f6e07d9636c7fb5adc9a83e46210c365188010 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Mon, 27 Apr 2026 17:47:06 +0200 Subject: [PATCH 29/32] feat: uniform, platform-independent auto_config() API --- libdd-agent-client/src/builder.rs | 143 +++++++++++++++++++----------- libdd-agent-client/src/lib.rs | 4 +- 2 files changed, 95 insertions(+), 52 deletions(-) diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index a49f97c114..fb229b1412 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -73,7 +73,7 @@ impl Default for AgentTransport { /// /// # Required fields /// -/// - Transport: set via [`AgentClientBuilder::auto_detect`] (reads standard env vars and probes the +/// - Transport: set via [`AgentClientBuilder::auto_config`] (reads standard env vars and probes the /// local socket) or an explicit convenience method ([`AgentClientBuilder::http`], /// [`AgentClientBuilder::unix_socket`], [`AgentClientBuilder::windows_named_pipe`], /// [`AgentClientBuilder::transport`]). @@ -129,25 +129,27 @@ impl AgentClientBuilder { /// Auto-configure transport and timeout from the environment. /// /// Transport priority: - /// 1. `DD_TRACE_AGENT_URL` — parsed as `http://host:port` or `unix:///path`. - /// 2. `DD_AGENT_HOST` / `DD_TRACE_AGENT_PORT` — explicit host and/or port. - /// 3. `/var/run/datadog/apm.socket` — Unix domain socket if the file exists. - /// 4. `localhost:8126` — HTTP fallback. + /// 1. `DD_TRACE_AGENT_URL`: parsed as `http://host:port` or `unix:///path` (Unix only). + /// 2. `DD_TRACE_PIPE_NAME`: Windows named pipe path (Windows only). + /// 3. `DD_AGENT_HOST` / `DD_TRACE_AGENT_PORT`: explicit host and/or port. + /// 4. `/var/run/datadog/apm.socket`: Unix domain socket if the file exists (Unix only). + /// 5. `localhost:8126`: HTTP fallback. /// /// Timeout is read from `DD_TRACE_AGENT_TIMEOUT_SECONDS` (seconds, float), /// defaulting to [`DEFAULT_TIMEOUT_MS`] when unset or unparseable. - #[cfg(unix)] - pub fn auto_detect(mut self) -> Self { + pub fn auto_config(mut self) -> Self { let transport = Self::transport_from_env().unwrap_or_else(|| { - let uds = PathBuf::from("/var/run/datadog/apm.socket"); - if uds.try_exists().unwrap_or(false) { - AgentTransport::UnixSocket { path: uds } - } else { - AgentTransport::Http { - host: "localhost".to_string(), - port: 8126, + #[cfg(unix)] + { + let uds = PathBuf::from("/var/run/datadog/apm.socket"); + if uds.try_exists().unwrap_or(false) { + return AgentTransport::UnixSocket { path: uds }; } } + AgentTransport::Http { + host: "localhost".to_string(), + port: 8126, + } }); self.transport = Some(transport); @@ -162,10 +164,13 @@ impl AgentClientBuilder { self } - /// Read transport from env vars (`DD_TRACE_AGENT_URL`, then - /// `DD_AGENT_HOST`/`DD_TRACE_AGENT_PORT`). Returns `None` when none of the variables are + /// Read transport from env vars. Returns `None` when none of the recognized variables are /// set. - #[cfg(unix)] + /// + /// Checks, in order: + /// 1. `DD_TRACE_AGENT_URL` (`http://`, `https://`, and `unix://` on Unix). + /// 2. `DD_TRACE_PIPE_NAME` (Windows only — full named pipe path). + /// 3. `DD_AGENT_HOST` / `DD_TRACE_AGENT_PORT`. fn transport_from_env() -> Option { if let Ok(url) = env::var("DD_TRACE_AGENT_URL") { if let Some(t) = Self::parse_agent_url(&url) { @@ -173,6 +178,15 @@ impl AgentClientBuilder { } } + #[cfg(windows)] + if let Ok(pipe_name) = env::var("DD_TRACE_PIPE_NAME") { + if !pipe_name.is_empty() { + return Some(AgentTransport::NamedPipe { + path: OsString::from(pipe_name), + }); + } + } + let host = env::var("DD_AGENT_HOST").ok(); let port = env::var("DD_TRACE_AGENT_PORT") .ok() @@ -190,9 +204,9 @@ impl AgentClientBuilder { /// Parse a Datadog agent URL into an [`AgentTransport`]. /// - /// Supported schemes: `http://`, `https://`, `unix://`. - #[cfg(unix)] + /// Supported schemes: `http://`, `https://`, and `unix://` (Unix only). fn parse_agent_url(url: &str) -> Option { + #[cfg(unix)] if let Some(after_scheme) = url.strip_prefix("unix://") { // unix:///abs/path or unix://localhost/abs/path let path = if after_scheme.starts_with('/') { @@ -454,15 +468,23 @@ mod tests { assert!(result.is_ok()); } - #[cfg(unix)] + /// Clear all env vars that `auto_config` reads, so tests don't leak state. + fn clear_auto_config_env() { + env::remove_var("DD_TRACE_AGENT_URL"); + env::remove_var("DD_AGENT_HOST"); + env::remove_var("DD_TRACE_AGENT_PORT"); + env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); + #[cfg(windows)] + env::remove_var("DD_TRACE_PIPE_NAME"); + } + #[test] #[serial_test::serial] - fn auto_detect_uses_dd_trace_agent_url_http() { + fn auto_config_uses_dd_trace_agent_url_http() { + clear_auto_config_env(); env::set_var("DD_TRACE_AGENT_URL", "http://myhost:9000"); - env::remove_var("DD_AGENT_HOST"); - env::remove_var("DD_TRACE_AGENT_PORT"); - let b = AgentClientBuilder::new().auto_detect(); - env::remove_var("DD_TRACE_AGENT_URL"); + let b = AgentClientBuilder::new().auto_config(); + clear_auto_config_env(); assert!(matches!( b.transport, Some(AgentTransport::Http { ref host, port }) @@ -473,12 +495,11 @@ mod tests { #[cfg(unix)] #[test] #[serial_test::serial] - fn auto_detect_uses_dd_trace_agent_url_unix() { + fn auto_config_uses_dd_trace_agent_url_unix() { + clear_auto_config_env(); env::set_var("DD_TRACE_AGENT_URL", "unix:///tmp/test.sock"); - env::remove_var("DD_AGENT_HOST"); - env::remove_var("DD_TRACE_AGENT_PORT"); - let b = AgentClientBuilder::new().auto_detect(); - env::remove_var("DD_TRACE_AGENT_URL"); + let b = AgentClientBuilder::new().auto_config(); + clear_auto_config_env(); assert!(matches!( b.transport, Some(AgentTransport::UnixSocket { ref path }) @@ -486,16 +507,45 @@ mod tests { )); } - #[cfg(unix)] + #[cfg(windows)] #[test] #[serial_test::serial] - fn auto_detect_uses_dd_agent_host_and_port() { - env::remove_var("DD_TRACE_AGENT_URL"); + fn auto_config_uses_dd_trace_pipe_name() { + clear_auto_config_env(); + env::set_var("DD_TRACE_PIPE_NAME", r"\\.\pipe\dd-test-pipe"); + let b = AgentClientBuilder::new().auto_config(); + clear_auto_config_env(); + assert!(matches!( + b.transport, + Some(AgentTransport::NamedPipe { ref path }) + if path == r"\\.\pipe\dd-test-pipe" + )); + } + + #[cfg(windows)] + #[test] + #[serial_test::serial] + fn auto_config_dd_trace_agent_url_takes_priority_over_pipe_name() { + clear_auto_config_env(); + env::set_var("DD_TRACE_AGENT_URL", "http://myhost:9000"); + env::set_var("DD_TRACE_PIPE_NAME", r"\\.\pipe\dd-test-pipe"); + let b = AgentClientBuilder::new().auto_config(); + clear_auto_config_env(); + assert!(matches!( + b.transport, + Some(AgentTransport::Http { ref host, port }) + if host == "myhost" && port == 9000 + )); + } + + #[test] + #[serial_test::serial] + fn auto_config_uses_dd_agent_host_and_port() { + clear_auto_config_env(); env::set_var("DD_AGENT_HOST", "remotehost"); env::set_var("DD_TRACE_AGENT_PORT", "7777"); - let b = AgentClientBuilder::new().auto_detect(); - env::remove_var("DD_AGENT_HOST"); - env::remove_var("DD_TRACE_AGENT_PORT"); + let b = AgentClientBuilder::new().auto_config(); + clear_auto_config_env(); assert!(matches!( b.transport, Some(AgentTransport::Http { ref host, port }) @@ -503,28 +553,21 @@ mod tests { )); } - #[cfg(unix)] #[test] #[serial_test::serial] - fn auto_detect_reads_timeout_from_env() { - env::remove_var("DD_TRACE_AGENT_URL"); - env::remove_var("DD_AGENT_HOST"); - env::remove_var("DD_TRACE_AGENT_PORT"); + fn auto_config_reads_timeout_from_env() { + clear_auto_config_env(); env::set_var("DD_TRACE_AGENT_TIMEOUT_SECONDS", "5"); - let b = AgentClientBuilder::new().auto_detect(); - env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); + let b = AgentClientBuilder::new().auto_config(); + clear_auto_config_env(); assert_eq!(b.timeout, Some(Duration::from_secs(5))); } - #[cfg(unix)] #[test] #[serial_test::serial] - fn auto_detect_uses_default_timeout_when_unset() { - env::remove_var("DD_TRACE_AGENT_URL"); - env::remove_var("DD_AGENT_HOST"); - env::remove_var("DD_TRACE_AGENT_PORT"); - env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); - let b = AgentClientBuilder::new().auto_detect(); + fn auto_config_uses_default_timeout_when_unset() { + clear_auto_config_env(); + let b = AgentClientBuilder::new().auto_config(); assert_eq!(b.timeout, Some(Duration::from_millis(DEFAULT_TIMEOUT_MS))); } diff --git a/libdd-agent-client/src/lib.rs b/libdd-agent-client/src/lib.rs index 605c6d03ca..a68b938963 100644 --- a/libdd-agent-client/src/lib.rs +++ b/libdd-agent-client/src/lib.rs @@ -7,7 +7,7 @@ //! //! # Quick start //! -//! Call [`AgentClientBuilder::auto_detect`] to let the client configure transport and timeout +//! Call [`AgentClientBuilder::auto_config`] to let the client configure transport and timeout //! from the standard Datadog environment variables (`DD_TRACE_AGENT_URL`, `DD_AGENT_HOST`, //! `DD_TRACE_AGENT_PORT`, `DD_TRACE_AGENT_TIMEOUT_SECONDS`), falling back to a local Unix //! socket at `/var/run/datadog/apm.socket` when it exists, and finally to `localhost:8126`. @@ -18,7 +18,7 @@ //! use libdd_agent_client::{AgentClient, LanguageMetadata}; //! //! let client = AgentClient::builder() -//! .auto_detect() +//! .auto_config() //! .language_metadata(LanguageMetadata::new( //! "python", "3.12.1", "CPython", "2.18.0", //! )) From 5e4c648080de764bd25abd3a970075229da30b87 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 28 Apr 2026 18:17:33 +0200 Subject: [PATCH 30/32] refactor: get rid of auto_config silent get_var --- libdd-agent-client/src/builder.rs | 233 +----------------------------- libdd-agent-client/src/lib.rs | 24 --- 2 files changed, 4 insertions(+), 253 deletions(-) diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index fb229b1412..649711ef15 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -7,7 +7,7 @@ use std::ffi::OsString; #[cfg(unix)] use std::path::PathBuf; -use std::{collections::HashMap, env, time::Duration}; +use std::{collections::HashMap, time::Duration}; use libdd_http_client::RetryConfig; @@ -73,10 +73,8 @@ impl Default for AgentTransport { /// /// # Required fields /// -/// - Transport: set via [`AgentClientBuilder::auto_config`] (reads standard env vars and probes the -/// local socket) or an explicit convenience method ([`AgentClientBuilder::http`], -/// [`AgentClientBuilder::unix_socket`], [`AgentClientBuilder::windows_named_pipe`], -/// [`AgentClientBuilder::transport`]). +/// - Transport: one of [`AgentClientBuilder::http`], [`AgentClientBuilder::unix_socket`], +/// [`AgentClientBuilder::windows_named_pipe`], [`AgentClientBuilder::transport`]. /// - [`AgentClientBuilder::language_metadata`]. /// /// # Test tokens @@ -126,118 +124,6 @@ impl AgentClientBuilder { self.transport(AgentTransport::NamedPipe { path: path.into() }) } - /// Auto-configure transport and timeout from the environment. - /// - /// Transport priority: - /// 1. `DD_TRACE_AGENT_URL`: parsed as `http://host:port` or `unix:///path` (Unix only). - /// 2. `DD_TRACE_PIPE_NAME`: Windows named pipe path (Windows only). - /// 3. `DD_AGENT_HOST` / `DD_TRACE_AGENT_PORT`: explicit host and/or port. - /// 4. `/var/run/datadog/apm.socket`: Unix domain socket if the file exists (Unix only). - /// 5. `localhost:8126`: HTTP fallback. - /// - /// Timeout is read from `DD_TRACE_AGENT_TIMEOUT_SECONDS` (seconds, float), - /// defaulting to [`DEFAULT_TIMEOUT_MS`] when unset or unparseable. - pub fn auto_config(mut self) -> Self { - let transport = Self::transport_from_env().unwrap_or_else(|| { - #[cfg(unix)] - { - let uds = PathBuf::from("/var/run/datadog/apm.socket"); - if uds.try_exists().unwrap_or(false) { - return AgentTransport::UnixSocket { path: uds }; - } - } - AgentTransport::Http { - host: "localhost".to_string(), - port: 8126, - } - }); - self.transport = Some(transport); - - self.timeout = Some( - env::var("DD_TRACE_AGENT_TIMEOUT_SECONDS") - .ok() - .and_then(|v| v.parse::().ok()) - .map(|secs| Duration::from_millis((secs * 1000.0) as u64)) - .unwrap_or(Duration::from_millis(DEFAULT_TIMEOUT_MS)), - ); - - self - } - - /// Read transport from env vars. Returns `None` when none of the recognized variables are - /// set. - /// - /// Checks, in order: - /// 1. `DD_TRACE_AGENT_URL` (`http://`, `https://`, and `unix://` on Unix). - /// 2. `DD_TRACE_PIPE_NAME` (Windows only — full named pipe path). - /// 3. `DD_AGENT_HOST` / `DD_TRACE_AGENT_PORT`. - fn transport_from_env() -> Option { - if let Ok(url) = env::var("DD_TRACE_AGENT_URL") { - if let Some(t) = Self::parse_agent_url(&url) { - return Some(t); - } - } - - #[cfg(windows)] - if let Ok(pipe_name) = env::var("DD_TRACE_PIPE_NAME") { - if !pipe_name.is_empty() { - return Some(AgentTransport::NamedPipe { - path: OsString::from(pipe_name), - }); - } - } - - let host = env::var("DD_AGENT_HOST").ok(); - let port = env::var("DD_TRACE_AGENT_PORT") - .ok() - .and_then(|p| p.parse::().ok()); - - if host.is_some() || port.is_some() { - return Some(AgentTransport::Http { - host: host.unwrap_or_else(|| "localhost".to_string()), - port: port.unwrap_or(8126), - }); - } - - None - } - - /// Parse a Datadog agent URL into an [`AgentTransport`]. - /// - /// Supported schemes: `http://`, `https://`, and `unix://` (Unix only). - fn parse_agent_url(url: &str) -> Option { - #[cfg(unix)] - if let Some(after_scheme) = url.strip_prefix("unix://") { - // unix:///abs/path or unix://localhost/abs/path - let path = if after_scheme.starts_with('/') { - after_scheme - } else { - &after_scheme[after_scheme.find('/')?..] - }; - return Some(AgentTransport::UnixSocket { - path: PathBuf::from(path), - }); - } - - let rest = url - .strip_prefix("http://") - .or_else(|| url.strip_prefix("https://"))?; - - // Drop any trailing path (e.g. "host:port/") - let authority = rest.split('/').next().unwrap_or(rest); - let (host, port) = if let Some(colon) = authority.rfind(':') { - let port = authority[colon + 1..].parse::().ok().unwrap_or(8126); - (&authority[..colon], port) - } else { - (authority, 8126u16) - }; - - Some(AgentTransport::Http { - host: host.to_string(), - port, - }) - } - /// Set the test session token. /// /// When set, `x-datadog-test-session-token: ` is injected on every request. @@ -387,19 +273,12 @@ impl AgentClientBuilder { headers } - /// Read container / entity-ID headers from the host environment. Always injects - /// `Datadog-External-Env` when `DD_EXTERNAL_ENV` is set. + /// Read container / entity-ID headers from the host environment. fn container_headers() -> Vec<(String, String)> { use libdd_common::entity_id; let mut headers = Vec::new(); - if let Ok(env) = env::var("DD_EXTERNAL_ENV") { - if !env.is_empty() { - headers.push(("Datadog-External-Env".to_string(), env)); - } - } - if let Some(container_id) = entity_id::get_container_id() { headers.push(("Datadog-Container-Id".to_string(), container_id.to_owned())); } @@ -415,7 +294,6 @@ impl AgentClientBuilder { #[cfg(test)] mod tests { use super::*; - use std::env; #[test] fn default_transport_is_localhost_8126() { @@ -468,109 +346,6 @@ mod tests { assert!(result.is_ok()); } - /// Clear all env vars that `auto_config` reads, so tests don't leak state. - fn clear_auto_config_env() { - env::remove_var("DD_TRACE_AGENT_URL"); - env::remove_var("DD_AGENT_HOST"); - env::remove_var("DD_TRACE_AGENT_PORT"); - env::remove_var("DD_TRACE_AGENT_TIMEOUT_SECONDS"); - #[cfg(windows)] - env::remove_var("DD_TRACE_PIPE_NAME"); - } - - #[test] - #[serial_test::serial] - fn auto_config_uses_dd_trace_agent_url_http() { - clear_auto_config_env(); - env::set_var("DD_TRACE_AGENT_URL", "http://myhost:9000"); - let b = AgentClientBuilder::new().auto_config(); - clear_auto_config_env(); - assert!(matches!( - b.transport, - Some(AgentTransport::Http { ref host, port }) - if host == "myhost" && port == 9000 - )); - } - - #[cfg(unix)] - #[test] - #[serial_test::serial] - fn auto_config_uses_dd_trace_agent_url_unix() { - clear_auto_config_env(); - env::set_var("DD_TRACE_AGENT_URL", "unix:///tmp/test.sock"); - let b = AgentClientBuilder::new().auto_config(); - clear_auto_config_env(); - assert!(matches!( - b.transport, - Some(AgentTransport::UnixSocket { ref path }) - if path.to_str() == Some("/tmp/test.sock") - )); - } - - #[cfg(windows)] - #[test] - #[serial_test::serial] - fn auto_config_uses_dd_trace_pipe_name() { - clear_auto_config_env(); - env::set_var("DD_TRACE_PIPE_NAME", r"\\.\pipe\dd-test-pipe"); - let b = AgentClientBuilder::new().auto_config(); - clear_auto_config_env(); - assert!(matches!( - b.transport, - Some(AgentTransport::NamedPipe { ref path }) - if path == r"\\.\pipe\dd-test-pipe" - )); - } - - #[cfg(windows)] - #[test] - #[serial_test::serial] - fn auto_config_dd_trace_agent_url_takes_priority_over_pipe_name() { - clear_auto_config_env(); - env::set_var("DD_TRACE_AGENT_URL", "http://myhost:9000"); - env::set_var("DD_TRACE_PIPE_NAME", r"\\.\pipe\dd-test-pipe"); - let b = AgentClientBuilder::new().auto_config(); - clear_auto_config_env(); - assert!(matches!( - b.transport, - Some(AgentTransport::Http { ref host, port }) - if host == "myhost" && port == 9000 - )); - } - - #[test] - #[serial_test::serial] - fn auto_config_uses_dd_agent_host_and_port() { - clear_auto_config_env(); - env::set_var("DD_AGENT_HOST", "remotehost"); - env::set_var("DD_TRACE_AGENT_PORT", "7777"); - let b = AgentClientBuilder::new().auto_config(); - clear_auto_config_env(); - assert!(matches!( - b.transport, - Some(AgentTransport::Http { ref host, port }) - if host == "remotehost" && port == 7777 - )); - } - - #[test] - #[serial_test::serial] - fn auto_config_reads_timeout_from_env() { - clear_auto_config_env(); - env::set_var("DD_TRACE_AGENT_TIMEOUT_SECONDS", "5"); - let b = AgentClientBuilder::new().auto_config(); - clear_auto_config_env(); - assert_eq!(b.timeout, Some(Duration::from_secs(5))); - } - - #[test] - #[serial_test::serial] - fn auto_config_uses_default_timeout_when_unset() { - clear_auto_config_env(); - let b = AgentClientBuilder::new().auto_config(); - assert_eq!(b.timeout, Some(Duration::from_millis(DEFAULT_TIMEOUT_MS))); - } - #[test] fn extra_headers_stored() { let mut headers = HashMap::new(); diff --git a/libdd-agent-client/src/lib.rs b/libdd-agent-client/src/lib.rs index a68b938963..0f0f97e736 100644 --- a/libdd-agent-client/src/lib.rs +++ b/libdd-agent-client/src/lib.rs @@ -7,30 +7,6 @@ //! //! # Quick start //! -//! Call [`AgentClientBuilder::auto_config`] to let the client configure transport and timeout -//! from the standard Datadog environment variables (`DD_TRACE_AGENT_URL`, `DD_AGENT_HOST`, -//! `DD_TRACE_AGENT_PORT`, `DD_TRACE_AGENT_TIMEOUT_SECONDS`), falling back to a local Unix -//! socket at `/var/run/datadog/apm.socket` when it exists, and finally to `localhost:8126`. -//! -//! ```rust,no_run -//! # #[cfg(unix)] -//! # fn example() -> Result<(), libdd_agent_client::BuildError> { -//! use libdd_agent_client::{AgentClient, LanguageMetadata}; -//! -//! let client = AgentClient::builder() -//! .auto_config() -//! .language_metadata(LanguageMetadata::new( -//! "python", "3.12.1", "CPython", "2.18.0", -//! )) -//! .build()?; -//! # Ok(()) -//! # } -//! ``` -//! -//! # Explicit transport -//! -//! When the host and port are known at build time, set them directly: -//! //! ```rust,no_run //! # fn example() -> Result<(), libdd_agent_client::BuildError> { //! use libdd_agent_client::{AgentClient, LanguageMetadata}; From 9496e8465b8b4da7d87a248360ab4caa20a77a28 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 28 Apr 2026 18:51:25 +0200 Subject: [PATCH 31/32] fix: properly propagate allow_connection_pooling to underlying client --- libdd-agent-client/src/builder.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/libdd-agent-client/src/builder.rs b/libdd-agent-client/src/builder.rs index 649711ef15..aa78faf56f 100644 --- a/libdd-agent-client/src/builder.rs +++ b/libdd-agent-client/src/builder.rs @@ -194,8 +194,9 @@ impl AgentClientBuilder { let retry = self.retry.unwrap_or_else(default_retry_config); // Build the underlying HTTP client. - let http = Self::build_http_client(transport, timeout, retry) - .map_err(|e| BuildError::HttpClient(e.to_string()))?; + let http = + Self::build_http_client(transport, timeout, retry, self.allow_connection_pooling) + .map_err(|e| BuildError::HttpClient(e.to_string()))?; // Pre-compute all static headers that are injected on every request. let static_headers = @@ -208,6 +209,7 @@ impl AgentClientBuilder { transport: AgentTransport, timeout: Duration, retry: RetryConfig, + allow_connection_pooling: bool, ) -> Result { let base_url = match &transport { AgentTransport::Http { host, port } => format!("http://{}:{}", host, port), @@ -224,6 +226,7 @@ impl AgentClientBuilder { // This allows methods like `agent_info` to interpret 404 as Ok(None) rather than // an error, and avoids retrying on HTTP 4xx/5xx. .treat_http_errors_as_errors(false) + .allow_connection_pooling(allow_connection_pooling) .retry(retry); match transport { From 247ad7597abeb08c887166c295897a9aa6fadd21 Mon Sep 17 00:00:00 2001 From: Yann Hamdaoui Date: Tue, 28 Apr 2026 19:11:38 +0200 Subject: [PATCH 32/32] tests: do not run http tests under Miri --- libdd-agent-client/tests/agent_info.rs | 3 +++ libdd-agent-client/tests/evp_event.rs | 1 + libdd-agent-client/tests/pipeline_stats.rs | 2 ++ libdd-agent-client/tests/static_headers.rs | 2 ++ libdd-agent-client/tests/stats.rs | 2 ++ libdd-agent-client/tests/telemetry.rs | 2 ++ libdd-agent-client/tests/traces.rs | 7 +++++++ 7 files changed, 19 insertions(+) diff --git a/libdd-agent-client/tests/agent_info.rs b/libdd-agent-client/tests/agent_info.rs index a81101a472..721ad2ce09 100644 --- a/libdd-agent-client/tests/agent_info.rs +++ b/libdd-agent-client/tests/agent_info.rs @@ -6,6 +6,7 @@ mod common; use httpmock::prelude::*; #[tokio::test] +#[cfg_attr(miri, ignore)] async fn parses_info_response() { let server = MockServer::start(); server.mock(|when, then| { @@ -32,6 +33,7 @@ async fn parses_info_response() { } #[tokio::test] +#[cfg_attr(miri, ignore)] async fn returns_none_on_404() { let server = MockServer::start(); server.mock(|when, then| { @@ -44,6 +46,7 @@ async fn returns_none_on_404() { } #[tokio::test] +#[cfg_attr(miri, ignore)] async fn extracts_container_tags_hash_header() { let server = MockServer::start(); server.mock(|when, then| { diff --git a/libdd-agent-client/tests/evp_event.rs b/libdd-agent-client/tests/evp_event.rs index 9df3167520..be8fc4e19b 100644 --- a/libdd-agent-client/tests/evp_event.rs +++ b/libdd-agent-client/tests/evp_event.rs @@ -7,6 +7,7 @@ use bytes::Bytes; use httpmock::prelude::*; #[tokio::test] +#[cfg_attr(miri, ignore)] async fn posts_to_path_with_subdomain_header() { let server = MockServer::start(); let mock = server.mock(|when, then| { diff --git a/libdd-agent-client/tests/pipeline_stats.rs b/libdd-agent-client/tests/pipeline_stats.rs index 7df264b637..5d0ff71638 100644 --- a/libdd-agent-client/tests/pipeline_stats.rs +++ b/libdd-agent-client/tests/pipeline_stats.rs @@ -7,6 +7,7 @@ use bytes::Bytes; use httpmock::prelude::*; #[tokio::test] +#[cfg_attr(miri, ignore)] async fn puts_to_correct_endpoint() { let server = MockServer::start(); let mock = server.mock(|when, then| { @@ -23,6 +24,7 @@ async fn puts_to_correct_endpoint() { } #[tokio::test] +#[cfg_attr(miri, ignore)] async fn sets_gzip_encoding() { let server = MockServer::start(); let mock = server.mock(|when, then| { diff --git a/libdd-agent-client/tests/static_headers.rs b/libdd-agent-client/tests/static_headers.rs index 3d1ef30dfd..20581c736a 100644 --- a/libdd-agent-client/tests/static_headers.rs +++ b/libdd-agent-client/tests/static_headers.rs @@ -8,6 +8,7 @@ use httpmock::prelude::*; use libdd_agent_client::{AgentClient, LanguageMetadata, TraceFormat, TraceSendOptions}; #[tokio::test] +#[cfg_attr(miri, ignore)] async fn language_metadata_headers_injected_on_all_requests() { let server = MockServer::start(); let mock = server.mock(|when, then| { @@ -35,6 +36,7 @@ async fn language_metadata_headers_injected_on_all_requests() { } #[tokio::test] +#[cfg_attr(miri, ignore)] async fn test_token_injected_when_set() { let server = MockServer::start(); let mock = server.mock(|when, then| { diff --git a/libdd-agent-client/tests/stats.rs b/libdd-agent-client/tests/stats.rs index 81a3839780..4f6ddd5061 100644 --- a/libdd-agent-client/tests/stats.rs +++ b/libdd-agent-client/tests/stats.rs @@ -7,6 +7,7 @@ use bytes::Bytes; use httpmock::prelude::*; #[tokio::test] +#[cfg_attr(miri, ignore)] async fn puts_to_v06_stats() { let server = MockServer::start(); let mock = server.mock(|when, then| { @@ -23,6 +24,7 @@ async fn puts_to_v06_stats() { } #[tokio::test] +#[cfg_attr(miri, ignore)] async fn sets_msgpack_content_type() { let server = MockServer::start(); let mock = server.mock(|when, then| { diff --git a/libdd-agent-client/tests/telemetry.rs b/libdd-agent-client/tests/telemetry.rs index e663f77e49..e96fae6c86 100644 --- a/libdd-agent-client/tests/telemetry.rs +++ b/libdd-agent-client/tests/telemetry.rs @@ -8,6 +8,7 @@ use httpmock::prelude::*; use libdd_agent_client::TelemetryRequest; #[tokio::test] +#[cfg_attr(miri, ignore)] async fn posts_to_telemetry_proxy() { let server = MockServer::start(); let mock = server.mock(|when, then| { @@ -30,6 +31,7 @@ async fn posts_to_telemetry_proxy() { } #[tokio::test] +#[cfg_attr(miri, ignore)] async fn injects_per_request_headers() { let server = MockServer::start(); let mock = server.mock(|when, then| { diff --git a/libdd-agent-client/tests/traces.rs b/libdd-agent-client/tests/traces.rs index 7dd0fe3791..c7fb309800 100644 --- a/libdd-agent-client/tests/traces.rs +++ b/libdd-agent-client/tests/traces.rs @@ -8,6 +8,7 @@ use httpmock::prelude::*; use libdd_agent_client::{TraceFormat, TraceSendOptions}; #[tokio::test] +#[cfg_attr(miri, ignore)] async fn v5_puts_to_correct_endpoint() { let server = MockServer::start(); let mock = server.mock(|when, then| { @@ -30,6 +31,7 @@ async fn v5_puts_to_correct_endpoint() { } #[tokio::test] +#[cfg_attr(miri, ignore)] async fn v4_puts_to_v4_endpoint() { let server = MockServer::start(); let mock = server.mock(|when, then| { @@ -51,6 +53,7 @@ async fn v4_puts_to_v4_endpoint() { } #[tokio::test] +#[cfg_attr(miri, ignore)] async fn injects_trace_count_header() { let server = MockServer::start(); let mock = server.mock(|when, then| { @@ -74,6 +77,7 @@ async fn injects_trace_count_header() { } #[tokio::test] +#[cfg_attr(miri, ignore)] async fn injects_send_real_http_status_header() { let server = MockServer::start(); let mock = server.mock(|when, then| { @@ -97,6 +101,7 @@ async fn injects_send_real_http_status_header() { } #[tokio::test] +#[cfg_attr(miri, ignore)] async fn computed_top_level_injects_header() { let server = MockServer::start(); let mock = server.mock(|when, then| { @@ -122,6 +127,7 @@ async fn computed_top_level_injects_header() { } #[tokio::test] +#[cfg_attr(miri, ignore)] async fn parses_rate_by_service() { let server = MockServer::start(); server.mock(|when, then| { @@ -149,6 +155,7 @@ async fn parses_rate_by_service() { } #[tokio::test] +#[cfg_attr(miri, ignore)] async fn returns_http_error_on_5xx() { let server = MockServer::start(); server.mock(|when, then| {