Skip to content

Commit 929a466

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

11 files changed

Lines changed: 756 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: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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+
/// SHA-256 of the `/info` response body.
26+
///
27+
/// Clients that need to detect agent state changes (e.g. configuration reloads) can
28+
/// compare this value against the `datadog-agent-state` response header on subsequent
29+
/// probes, re-fetching only when the hash changes.
30+
pub state_hash: String,
31+
}

libdd-agent-client/src/builder.rs

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

0 commit comments

Comments
 (0)