Skip to content

Commit f6111ed

Browse files
committed
wip: draft API for agent HTTP client component
1 parent 0718b6f commit f6111ed

11 files changed

Lines changed: 759 additions & 0 deletions

File tree

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ members = [
4646
"libdd-tinybytes",
4747
"libdd-dogstatsd-client",
4848
"libdd-http-client",
49+
"libdd-agent-client",
4950
"libdd-log",
5051
"libdd-log-ffi", "libdd-libunwind-sys",
5152
]

libdd-agent-client/Cargo.toml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
[package]
5+
name = "libdd-agent-client"
6+
version.workspace = true
7+
edition.workspace = true
8+
rust-version.workspace = true
9+
license.workspace = true
10+
authors.workspace = true
11+
description = "Datadog-agent-specialized HTTP client: language metadata injection, per-endpoint send methods, retry, and compression"
12+
homepage = "https://github.com/DataDog/libdatadog/tree/main/libdd-agent-client"
13+
repository = "https://github.com/DataDog/libdatadog/tree/main/libdd-agent-client"
14+
15+
[lib]
16+
bench = false
17+
18+
[dependencies]
19+
bytes = "1.4"
20+
serde = { version = "1.0", features = ["derive"] }
21+
serde_json = "1.0"
22+
thiserror = "2"
23+
tokio = { version = "1.23", features = ["rt"] }
24+
libdd-http-client = { path = "../libdd-http-client" }
25+
26+
[dev-dependencies]
27+
tokio = { version = "1.23", features = ["rt", "macros"] }
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Types for [`crate::AgentClient::agent_info`].
5+
6+
/// Parsed response from a `GET /info` probe.
7+
///
8+
/// Returned by [`crate::AgentClient::agent_info`]. Contains agent capabilities and the
9+
/// headers that dd-trace-py currently processes via the side-effectful `process_info_headers`
10+
/// function (`agent.py:17-23`) — here they are explicit typed fields instead.
11+
#[derive(Debug, Clone)]
12+
pub struct AgentInfo {
13+
/// Available agent endpoints, e.g. `["/v0.4/traces", "/v0.5/traces"]`.
14+
pub endpoints: Vec<String>,
15+
/// Whether the agent supports client-side P0 dropping.
16+
pub client_drop_p0s: bool,
17+
/// Raw agent configuration block.
18+
pub config: serde_json::Value,
19+
/// Agent version string, if reported.
20+
pub version: Option<String>,
21+
/// Parsed from the `Datadog-Container-Tags-Hash` response header.
22+
///
23+
/// Used by dd-trace-py to compute the base tag hash (`agent.py:17-23`).
24+
pub container_tags_hash: Option<String>,
25+
/// Value of the `Datadog-Agent-State` response header from the last `/info` fetch.
26+
///
27+
/// The agent updates this opaque token whenever its internal state changes (e.g. a
28+
/// configuration reload). Clients that poll `/info` periodically can skip re-parsing
29+
/// the response body by comparing this value to the one returned by the previous call
30+
/// and only acting when it differs.
31+
pub state_hash: Option<String>,
32+
}

libdd-agent-client/src/builder.rs

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
// Copyright 2026-Present Datadog, Inc. https://www.datadoghq.com/
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
//! Builder for [`crate::AgentClient`].
5+
6+
use std::collections::HashMap;
7+
use std::time::Duration;
8+
9+
use libdd_http_client::RetryConfig;
10+
11+
use crate::{error::BuildError, language_metadata::LanguageMetadata, AgentClient};
12+
13+
/// Default timeout for agent requests: 2 000 ms.
14+
///
15+
/// Matches dd-trace-py's `DEFAULT_TIMEOUT = 2.0 s` (`constants.py:97`).
16+
pub const DEFAULT_TIMEOUT_MS: u64 = 2_000;
17+
18+
/// Default retry configuration: 2 retries (3 total attempts), 100 ms initial delay,
19+
/// exponential backoff with full jitter.
20+
///
21+
/// This approximates dd-trace-py's `fibonacci_backoff_with_jitter` pattern used in
22+
/// `writer.py:245-249`, `stats.py:123-126`, and `datastreams/processor.py:140-143`.
23+
pub fn default_retry_config() -> RetryConfig {
24+
todo!()
25+
}
26+
27+
/// Transport configuration for the agent client.
28+
///
29+
/// Determines how the client connects to the Datadog agent (or an intake endpoint).
30+
/// Set via [`AgentClientBuilder::transport`] or the convenience helpers
31+
/// [`AgentClientBuilder::http`], [`AgentClientBuilder::https`],
32+
/// [`AgentClientBuilder::unix_socket`], etc.
33+
#[derive(Debug, Clone)]
34+
pub enum AgentTransport {
35+
/// HTTP over TCP to `http://{host}:{port}`.
36+
Http {
37+
/// Hostname or IP address.
38+
host: String,
39+
/// Port number.
40+
port: u16,
41+
},
42+
/// HTTPS over TCP to `https://{host}:{port}` (e.g. for intake endpoints).
43+
Https {
44+
/// Hostname or IP address.
45+
host: String,
46+
/// Port number.
47+
port: u16,
48+
},
49+
/// Unix Domain Socket.
50+
///
51+
/// HTTP requests are still formed with `Host: localhost`; the socket path
52+
/// governs only the transport layer.
53+
#[cfg(unix)]
54+
UnixSocket {
55+
/// Filesystem path to the socket file.
56+
path: std::path::PathBuf,
57+
},
58+
/// Windows Named Pipe.
59+
#[cfg(windows)]
60+
NamedPipe {
61+
/// Named pipe path, e.g. `\\.\pipe\DD_APM_DRIVER`.
62+
path: std::ffi::OsString,
63+
},
64+
/// Probe at build time: use UDS if the socket file exists, otherwise fall back to HTTP.
65+
///
66+
/// Mirrors the auto-detect logic in dd-trace-py's `_agent.py:32-49`.
67+
#[cfg(unix)]
68+
AutoDetect {
69+
/// UDS path to probe.
70+
uds_path: std::path::PathBuf,
71+
/// Fallback host when the socket is absent.
72+
fallback_host: String,
73+
/// Fallback port when the socket is absent (typically 8126).
74+
fallback_port: u16,
75+
},
76+
}
77+
78+
impl Default for AgentTransport {
79+
fn default() -> Self {
80+
todo!()
81+
}
82+
}
83+
84+
/// Connection mode for the underlying HTTP client.
85+
///
86+
/// # Correctness note
87+
///
88+
/// The Datadog agent has a low keep-alive timeout that causes "pipe closed" errors on every
89+
/// second connection when connection reuse is enabled. [`ClientMode::Periodic`] (the default)
90+
/// disables connection pooling and is **correct** for all periodic-flush writers (traces, stats,
91+
/// data streams). Only high-frequency continuous senders (e.g. a streaming profiling exporter)
92+
/// should opt into [`ClientMode::Persistent`].
93+
#[derive(Debug, Clone, Default, PartialEq, Eq)]
94+
pub enum ClientMode {
95+
/// No connection pooling. Correct for periodic flushes to the agent.
96+
#[default]
97+
Periodic,
98+
/// Keep connections alive across requests.
99+
///
100+
/// Use only for high-frequency continuous senders.
101+
Persistent,
102+
}
103+
104+
/// Builder for [`AgentClient`].
105+
///
106+
/// Obtain via [`AgentClient::builder`].
107+
///
108+
/// # Required fields
109+
///
110+
/// - Transport: set via [`AgentClientBuilder::transport`] or a convenience method
111+
/// ([`AgentClientBuilder::http`], [`AgentClientBuilder::https`],
112+
/// [`AgentClientBuilder::unix_socket`], [`AgentClientBuilder::windows_named_pipe`],
113+
/// [`AgentClientBuilder::auto_detect`]).
114+
/// - [`AgentClientBuilder::language_metadata`].
115+
///
116+
/// # Agentless mode
117+
///
118+
/// Call [`AgentClientBuilder::api_key`] with your Datadog API key and point the transport to
119+
/// the intake endpoint via [`AgentClientBuilder::https`]. The client injects `dd-api-key` on
120+
/// every request.
121+
///
122+
/// # Testing
123+
///
124+
/// Call [`AgentClientBuilder::test_token`] to inject `x-datadog-test-session-token` on every
125+
/// request. This replaces dd-trace-py's `AgentWriter.set_test_session_token` (`writer.py:754-755`).
126+
///
127+
/// # Fork safety
128+
///
129+
/// The underlying `libdd-http-client` uses `hickory-dns` by default — an in-process, fork-safe
130+
/// DNS resolver that avoids the class of bugs where a forked child inherits open sockets from a
131+
/// parent's DNS thread pool. This is important for host processes that fork (Django, Flask,
132+
/// Celery workers, PHP-FPM, etc.).
133+
#[derive(Debug, Default)]
134+
pub struct AgentClientBuilder {
135+
transport: Option<AgentTransport>,
136+
api_key: Option<String>,
137+
test_token: Option<String>,
138+
timeout: Option<Duration>,
139+
language: Option<LanguageMetadata>,
140+
retry: Option<RetryConfig>,
141+
client_mode: ClientMode,
142+
extra_headers: HashMap<String, String>,
143+
}
144+
145+
impl AgentClientBuilder {
146+
/// Create a new builder with default settings.
147+
pub fn new() -> Self {
148+
todo!()
149+
}
150+
151+
// ── Transport ─────────────────────────────────────────────────────────────
152+
153+
/// Set the transport configuration.
154+
pub fn transport(self, transport: AgentTransport) -> Self {
155+
todo!()
156+
}
157+
158+
/// Convenience: HTTP over TCP.
159+
pub fn http(self, host: impl Into<String>, port: u16) -> Self {
160+
todo!()
161+
}
162+
163+
/// Convenience: HTTPS over TCP.
164+
pub fn https(self, host: impl Into<String>, port: u16) -> Self {
165+
todo!()
166+
}
167+
168+
/// Convenience: Unix Domain Socket.
169+
#[cfg(unix)]
170+
pub fn unix_socket(self, path: impl Into<std::path::PathBuf>) -> Self {
171+
todo!()
172+
}
173+
174+
/// Convenience: Windows Named Pipe.
175+
#[cfg(windows)]
176+
pub fn windows_named_pipe(self, path: impl Into<std::ffi::OsString>) -> Self {
177+
todo!()
178+
}
179+
180+
/// Convenience: auto-detect transport (UDS if socket file exists, else HTTP).
181+
///
182+
/// Mirrors the logic in dd-trace-py's `_agent.py:32-49`.
183+
#[cfg(unix)]
184+
pub fn auto_detect(
185+
self,
186+
uds_path: impl Into<std::path::PathBuf>,
187+
fallback_host: impl Into<String>,
188+
fallback_port: u16,
189+
) -> Self {
190+
todo!()
191+
}
192+
193+
// ── Authentication / routing ──────────────────────────────────────────────
194+
195+
/// Set the Datadog API key (agentless mode).
196+
///
197+
/// When set, `dd-api-key: <key>` is injected on every request.
198+
/// Point the transport to the intake endpoint via [`AgentClientBuilder::https`].
199+
pub fn api_key(self, key: impl Into<String>) -> Self {
200+
todo!()
201+
}
202+
203+
/// Set the test session token.
204+
///
205+
/// When set, `x-datadog-test-session-token: <token>` is injected on every request.
206+
/// Replaces dd-trace-py's `AgentWriter.set_test_session_token` (`writer.py:754-755`).
207+
pub fn test_token(self, token: impl Into<String>) -> Self {
208+
todo!()
209+
}
210+
211+
// ── Timeout / retries ─────────────────────────────────────────────────────
212+
213+
/// Set the request timeout.
214+
///
215+
/// Defaults to [`DEFAULT_TIMEOUT_MS`] (2 000 ms) when not set.
216+
pub fn timeout(self, timeout: Duration) -> Self {
217+
todo!()
218+
}
219+
220+
/// Read the timeout from `DD_TRACE_AGENT_TIMEOUT_SECONDS`, falling back to
221+
/// [`DEFAULT_TIMEOUT_MS`] if the variable is unset or unparseable.
222+
pub fn timeout_from_env(self) -> Self {
223+
todo!()
224+
}
225+
226+
/// Override the default retry configuration.
227+
///
228+
/// Defaults to [`default_retry_config`]: 2 retries, 100 ms initial delay, exponential
229+
/// backoff with full jitter.
230+
pub fn retry(self, config: RetryConfig) -> Self {
231+
todo!()
232+
}
233+
234+
// ── Language metadata ─────────────────────────────────────────────────────
235+
236+
/// Set the language/runtime metadata injected into every request.
237+
///
238+
/// Required. Drives `Datadog-Meta-Lang`, `Datadog-Meta-Lang-Version`,
239+
/// `Datadog-Meta-Lang-Interpreter`, `Datadog-Meta-Tracer-Version`, and `User-Agent`.
240+
pub fn language_metadata(self, meta: LanguageMetadata) -> Self {
241+
todo!()
242+
}
243+
244+
// ── Connection pooling ────────────────────────────────────────────────────
245+
246+
/// Set the connection mode. Defaults to [`ClientMode::Periodic`].
247+
///
248+
/// See [`ClientMode`] for the correctness rationale behind the default.
249+
pub fn client_mode(self, mode: ClientMode) -> Self {
250+
todo!()
251+
}
252+
253+
// ── Compression ───────────────────────────────────────────────────────────
254+
//
255+
// Not exposed in v1. Gzip compression (level 6, matching dd-trace-py's trace writer at
256+
// `writer.py:490`) will be added in a follow-up once the core send paths are stable.
257+
// Per-method defaults (e.g. unconditional gzip for `send_pipeline_stats`) are already
258+
// baked in; only the opt-in client-level `gzip(level)` builder knob is deferred.
259+
260+
// ── Extra headers ─────────────────────────────────────────────────────────
261+
262+
/// Merge additional headers into every request.
263+
///
264+
/// Intended for `_DD_TRACE_WRITER_ADDITIONAL_HEADERS` in dd-trace-py.
265+
pub fn extra_headers(self, headers: HashMap<String, String>) -> Self {
266+
todo!()
267+
}
268+
269+
// ── Build ─────────────────────────────────────────────────────────────────
270+
271+
/// Build the [`AgentClient`].
272+
///
273+
/// # Errors
274+
///
275+
/// - [`BuildError::MissingTransport`] — no transport was configured.
276+
/// - [`BuildError::MissingLanguageMetadata`] — no language metadata was configured.
277+
pub fn build(self) -> Result<AgentClient, BuildError> {
278+
todo!()
279+
}
280+
}

0 commit comments

Comments
 (0)