1use http::{uri::PathAndQuery, Uri};
5use libdd_common::{config::parse_env, parse_uri, Endpoint};
6use std::{borrow::Cow, time::Duration};
7use tracing::debug;
8
9pub const DEFAULT_DD_SITE: &str = "datadoghq.com";
10pub const PROD_INTAKE_SUBDOMAIN: &str = "instrumentation-telemetry-intake";
11
12const DIRECT_TELEMETRY_URL_PATH: &str = "/api/v2/apmtelemetry";
13const AGENT_TELEMETRY_URL_PATH: &str = "/telemetry/proxy/api/v2/apmtelemetry";
14
15#[cfg(unix)]
16const TRACE_SOCKET_PATH: &str = "/var/run/datadog/apm.socket";
17
18const DEFAULT_AGENT_HOST: &str = "localhost";
19const DEFAULT_AGENT_PORT: u16 = 8126;
20
21#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
22pub struct Config {
23 pub(crate) endpoint: Option<Endpoint>,
27 pub telemetry_debug_logging_enabled: bool,
29 pub telemetry_heartbeat_interval: Duration,
30 pub telemetry_extended_heartbeat_interval: Duration,
31 pub direct_submission_enabled: bool,
32 pub restartable: bool,
35
36 pub debug_enabled: bool,
37
38 #[serde(default)]
39 pub session_id: Option<String>,
40 #[serde(default)]
41 pub parent_session_id: Option<String>,
42 #[serde(default)]
43 pub root_session_id: Option<String>,
44}
45
46fn endpoint_with_telemetry_path(
47 mut endpoint: Endpoint,
48 direct_submission_enabled: bool,
49) -> anyhow::Result<Endpoint> {
50 let mut uri_parts = endpoint.url.into_parts();
51 if uri_parts
52 .scheme
53 .as_ref()
54 .is_some_and(|scheme| scheme.as_str() != "file")
55 {
56 uri_parts.path_and_query = Some(PathAndQuery::from_static(
57 if endpoint.api_key.is_some() && direct_submission_enabled {
58 DIRECT_TELEMETRY_URL_PATH
59 } else {
60 AGENT_TELEMETRY_URL_PATH
61 },
62 ));
63 }
64
65 endpoint.url = Uri::from_parts(uri_parts)?;
66 Ok(endpoint)
67}
68
69#[derive(Debug)]
72pub struct Settings {
73 pub agent_host: Option<String>,
75 pub trace_agent_port: Option<u16>,
76 pub trace_agent_url: Option<String>,
77 pub trace_pipe_name: Option<String>,
78 pub direct_submission_enabled: bool,
79 pub api_key: Option<String>,
80 pub site: Option<String>,
81 pub telemetry_dd_url: Option<String>,
82 pub telemetry_heartbeat_interval: Duration,
83 pub telemetry_extended_heartbeat_interval: Duration,
84 pub shared_lib_debug: bool,
85
86 pub agent_uds_socket_found: bool,
88}
89
90impl Default for Settings {
91 fn default() -> Self {
92 Self {
93 agent_host: None,
94 trace_agent_port: None,
95 trace_agent_url: None,
96 trace_pipe_name: None,
97 direct_submission_enabled: false,
98 api_key: None,
99 site: None,
100 telemetry_dd_url: None,
101 telemetry_heartbeat_interval: Duration::from_secs(60),
102 telemetry_extended_heartbeat_interval: Duration::from_secs(60 * 60 * 24),
103 shared_lib_debug: false,
104
105 agent_uds_socket_found: false,
106 }
107 }
108}
109
110impl Settings {
111 const DD_TRACE_AGENT_URL: &'static str = "DD_TRACE_AGENT_URL";
113 const DD_AGENT_HOST: &'static str = "DD_AGENT_HOST";
114 const DD_TRACE_AGENT_PORT: &'static str = "DD_TRACE_AGENT_PORT";
115 const DD_TRACE_PIPE_NAME: &'static str = "DD_TRACE_PIPE_NAME";
117
118 const _DD_DIRECT_SUBMISSION_ENABLED: &'static str = "_DD_DIRECT_SUBMISSION_ENABLED";
120 const DD_API_KEY: &'static str = "DD_API_KEY";
121 const DD_SITE: &'static str = "DD_SITE";
122 const DD_APM_TELEMETRY_DD_URL: &'static str = "DD_APM_TELEMETRY_DD_URL";
123
124 const DD_TELEMETRY_HEARTBEAT_INTERVAL: &'static str = "DD_TELEMETRY_HEARTBEAT_INTERVAL";
126 const DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL: &'static str =
127 "DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL";
128 const _DD_SHARED_LIB_DEBUG: &'static str = "_DD_SHARED_LIB_DEBUG";
129
130 pub fn from_env() -> Self {
131 debug!(
132 config.source = "environment",
133 "Loading telemetry settings from environment variables"
134 );
135 let default = Self::default();
136 Self {
137 agent_host: parse_env::str_not_empty(Self::DD_AGENT_HOST),
138 trace_agent_port: parse_env::int(Self::DD_TRACE_AGENT_PORT),
139 trace_agent_url: parse_env::str_not_empty(Self::DD_TRACE_AGENT_URL)
140 .or(default.trace_agent_url),
141 trace_pipe_name: parse_env::str_not_empty(Self::DD_TRACE_PIPE_NAME)
142 .or(default.trace_pipe_name),
143 direct_submission_enabled: parse_env::bool(Self::_DD_DIRECT_SUBMISSION_ENABLED)
144 .unwrap_or(default.direct_submission_enabled),
145 api_key: parse_env::str_not_empty(Self::DD_API_KEY),
146 site: parse_env::str_not_empty(Self::DD_SITE),
147 telemetry_dd_url: parse_env::str_not_empty(Self::DD_APM_TELEMETRY_DD_URL),
148 telemetry_heartbeat_interval: parse_env::duration(
149 Self::DD_TELEMETRY_HEARTBEAT_INTERVAL,
150 )
151 .unwrap_or(Duration::from_secs(60)),
152 telemetry_extended_heartbeat_interval: parse_env::duration(
153 Self::DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL,
154 )
155 .unwrap_or(Duration::from_secs(60 * 60 * 24)),
156 shared_lib_debug: parse_env::bool(Self::_DD_SHARED_LIB_DEBUG).unwrap_or(false),
157
158 agent_uds_socket_found: (|| {
159 #[cfg(unix)]
160 return std::fs::metadata(TRACE_SOCKET_PATH).is_ok();
161 #[cfg(not(unix))]
162 return false;
163 })(),
164 }
165 }
166}
167
168impl Default for Config {
169 fn default() -> Self {
170 Self {
171 endpoint: None,
172 telemetry_debug_logging_enabled: false,
173 telemetry_heartbeat_interval: Duration::from_secs(60),
174 telemetry_extended_heartbeat_interval: Duration::from_secs(60 * 60 * 24),
175 direct_submission_enabled: false,
176 restartable: false,
177 debug_enabled: false,
178 session_id: None,
179 parent_session_id: None,
180 root_session_id: None,
181 }
182 }
183}
184
185impl Config {
186 fn trace_agent_url_from_setting(settings: &Settings) -> String {
189 None.or_else(|| {
190 settings
191 .trace_agent_url
192 .as_deref()
193 .filter(|u| {
194 u.starts_with("unix://")
195 || u.starts_with("http://")
196 || u.starts_with("https://")
197 })
198 .map(ToString::to_string)
199 })
200 .or_else(|| {
201 #[cfg(windows)]
202 return settings
203 .trace_pipe_name
204 .as_ref()
205 .map(|pipe_name| format!("windows:{pipe_name}"));
206 #[cfg(not(windows))]
207 return None;
208 })
209 .or_else(|| match (&settings.agent_host, settings.trace_agent_port) {
210 (None, None) => None,
211 _ => Some(format!(
212 "http://{}:{}",
213 settings.agent_host.as_deref().unwrap_or(DEFAULT_AGENT_HOST),
214 settings.trace_agent_port.unwrap_or(DEFAULT_AGENT_PORT),
215 )),
216 })
217 .or_else(|| {
218 #[cfg(unix)]
219 return settings
220 .agent_uds_socket_found
221 .then(|| format!("unix://{TRACE_SOCKET_PATH}"));
222 #[cfg(not(unix))]
223 return None;
224 })
225 .unwrap_or_else(|| format!("http://{DEFAULT_AGENT_HOST}:{DEFAULT_AGENT_PORT}"))
226 }
227
228 fn api_key_from_settings(settings: &Settings) -> Option<Cow<'static, str>> {
229 if !settings.direct_submission_enabled {
230 return None;
231 }
232 settings.api_key.clone().map(Cow::Owned)
233 }
234
235 pub fn endpoint(&self) -> Option<&Endpoint> {
236 self.endpoint.as_ref()
237 }
238
239 pub fn set_endpoint(&mut self, endpoint: Endpoint) -> anyhow::Result<()> {
240 self.endpoint = Some(endpoint_with_telemetry_path(
241 endpoint,
242 self.direct_submission_enabled,
243 )?);
244 Ok(())
245 }
246
247 pub fn set_endpoint_test_token<T: Into<Cow<'static, str>>>(&mut self, test_token: Option<T>) {
248 if let Some(endpoint) = &mut self.endpoint {
249 endpoint.test_token = test_token.map(|t| t.into());
250 }
251 }
252
253 pub fn from_settings(settings: &Settings) -> Self {
254 let trace_agent_url = Self::trace_agent_url_from_setting(settings);
255 let api_key = Self::api_key_from_settings(settings);
256
257 let mut this = Self {
258 endpoint: None,
259 telemetry_debug_logging_enabled: settings.shared_lib_debug,
260 telemetry_heartbeat_interval: settings.telemetry_heartbeat_interval,
261 telemetry_extended_heartbeat_interval: settings.telemetry_extended_heartbeat_interval,
262 direct_submission_enabled: settings.direct_submission_enabled,
263 restartable: false,
264 debug_enabled: false,
265 session_id: None,
266 parent_session_id: None,
267 root_session_id: None,
268 };
269 if let Ok(url) = parse_uri(&trace_agent_url) {
270 let _res = this.set_endpoint(Endpoint {
271 url,
272 api_key,
273 ..Default::default()
274 });
275 }
276
277 this
278 }
279
280 pub fn from_env() -> Self {
282 let settings = Settings::from_env();
283 Self::from_settings(&settings)
284 }
285
286 pub fn set_host_from_url(&mut self, host_url: &str) -> anyhow::Result<()> {
297 let endpoint = self.endpoint.take().unwrap_or_default();
298
299 self.set_endpoint(Endpoint {
300 url: parse_uri(host_url)?,
301 ..endpoint
302 })
303 }
304}
305
306#[cfg(test)]
307mod tests {
308 use std::path::Path;
309
310 #[cfg(unix)]
311 use libdd_common::connector::uds;
312
313 use libdd_common::connector::named_pipe;
314
315 use super::{Config, Settings};
316
317 #[test]
318 fn test_agent_host_detection_trace_agent_url_should_take_precedence() {
319 let cases = [
320 (
321 "http://localhost:1234",
322 "http://localhost:1234/telemetry/proxy/api/v2/apmtelemetry",
323 ),
324 (
325 "unix://./here",
326 "unix://2e2f68657265/telemetry/proxy/api/v2/apmtelemetry",
327 ),
328 ];
329 for (trace_agent_url, expected) in cases {
330 let settings = Settings {
331 trace_agent_url: Some(trace_agent_url.to_owned()),
332 agent_host: Some("example.org".to_owned()),
333 trace_agent_port: Some(1),
334 trace_pipe_name: Some("C:\\foo".to_owned()),
335 agent_uds_socket_found: true,
336 ..Default::default()
337 };
338 let cfg = Config::from_settings(&settings);
339 assert_eq!(cfg.endpoint.unwrap().url.to_string(), expected);
340 }
341 }
342
343 #[test]
344 fn test_agent_host_detection_agent_host_and_port() {
345 let cases = [
346 (
347 Some("example.org"),
348 Some(1),
349 "http://example.org:1/telemetry/proxy/api/v2/apmtelemetry",
350 ),
351 (
352 Some("example.org"),
353 None,
354 "http://example.org:8126/telemetry/proxy/api/v2/apmtelemetry",
355 ),
356 (
357 None,
358 Some(1),
359 "http://localhost:1/telemetry/proxy/api/v2/apmtelemetry",
360 ),
361 ];
362 for (agent_host, trace_agent_port, expected) in cases {
363 let settings = Settings {
364 trace_agent_url: None,
365 agent_host: agent_host.map(ToString::to_string),
366 trace_agent_port,
367 trace_pipe_name: None,
368 agent_uds_socket_found: true,
369 ..Default::default()
370 };
371 let cfg = Config::from_settings(&settings);
372 assert_eq!(cfg.endpoint.unwrap().url.to_string(), expected);
373 }
374 }
375
376 #[test]
377 #[cfg(unix)]
378 fn test_agent_host_detection_socket_found() {
379 let settings = Settings {
380 trace_agent_url: None,
381 agent_host: None,
382 trace_agent_port: None,
383 trace_pipe_name: None,
384 agent_uds_socket_found: true,
385 ..Default::default()
386 };
387 let cfg = Config::from_settings(&settings);
388 assert_eq!(
389 cfg.endpoint.unwrap().url.to_string(),
390 "unix://2f7661722f72756e2f64617461646f672f61706d2e736f636b6574/telemetry/proxy/api/v2/apmtelemetry"
391 );
392 }
393
394 #[test]
395 fn test_agent_host_detection_fallback() {
396 let settings = Settings {
397 trace_agent_url: None,
398 agent_host: None,
399 trace_agent_port: None,
400 trace_pipe_name: None,
401 agent_uds_socket_found: false,
402 ..Default::default()
403 };
404
405 let cfg = Config::from_settings(&settings);
406 assert_eq!(
407 cfg.endpoint.unwrap().url.to_string(),
408 "http://localhost:8126/telemetry/proxy/api/v2/apmtelemetry"
409 );
410 }
411
412 #[test]
413 fn test_config_set_url() {
414 let mut cfg = Config::default();
415
416 cfg.set_host_from_url("http://example.com/any_path_will_be_ignored")
417 .unwrap();
418
419 assert_eq!(
420 "http://example.com/telemetry/proxy/api/v2/apmtelemetry",
421 cfg.clone().endpoint.unwrap().url
422 );
423 }
424
425 #[test]
426 fn test_config_set_url_file() {
427 let cases = [
428 ("file:///absolute/path", "/absolute/path"),
429 ("file://./relative/path", "./relative/path"),
430 ("file://relative/path", "relative/path"),
431 (
432 "file://c://temp//with space\\foo.json",
433 "c://temp//with space\\foo.json",
434 ),
435 ];
436
437 for (input, expected) in cases {
438 let mut cfg = Config::default();
439 cfg.set_host_from_url(input).unwrap();
440
441 assert_eq!(
442 "file",
443 cfg.clone()
444 .endpoint
445 .unwrap()
446 .url
447 .scheme()
448 .unwrap()
449 .to_string()
450 );
451 assert_eq!(
452 Path::new(expected),
453 libdd_common::decode_uri_path_in_authority(&cfg.endpoint.unwrap().url).unwrap(),
454 );
455 }
456 }
457
458 #[test]
459 #[cfg(unix)]
460 fn test_config_set_url_unix_socket() {
461 let mut cfg = Config::default();
462
463 cfg.set_host_from_url("unix:///compatiliby/path").unwrap();
464 assert_eq!(
465 "unix://2f636f6d706174696c6962792f70617468/telemetry/proxy/api/v2/apmtelemetry",
466 cfg.clone().endpoint.unwrap().url.to_string()
467 );
468 assert_eq!(
469 "/compatiliby/path",
470 uds::socket_path_from_uri(&cfg.clone().endpoint.unwrap().url)
471 .unwrap()
472 .to_string_lossy()
473 );
474 }
475
476 #[test]
477 fn test_config_set_url_windows_pipe() {
478 let mut cfg = Config::default();
479
480 cfg.set_host_from_url("windows:C:\\system32\\foo").unwrap();
481 assert_eq!(
482 "windows://433a5c73797374656d33325c666f6f/telemetry/proxy/api/v2/apmtelemetry",
483 cfg.clone().endpoint.unwrap().url.to_string()
484 );
485 assert_eq!(
486 "C:\\system32\\foo",
487 named_pipe::named_pipe_path_from_uri(&cfg.clone().endpoint.unwrap().url)
488 .unwrap()
489 .to_string_lossy()
490 );
491 }
492
493 #[test]
494 fn test_from_settings_propagates_extended_heartbeat_interval() {
495 use std::time::Duration;
496
497 let custom_interval = Duration::from_secs(120);
498 let settings = Settings {
499 telemetry_extended_heartbeat_interval: custom_interval,
500 ..Default::default()
501 };
502 let cfg = Config::from_settings(&settings);
503 assert_eq!(cfg.telemetry_extended_heartbeat_interval, custom_interval);
504 }
505
506 #[test]
507 fn test_from_settings_default_extended_heartbeat_interval() {
508 use std::time::Duration;
509
510 let settings = Settings::default();
511 let cfg = Config::from_settings(&settings);
512 assert_eq!(
513 cfg.telemetry_extended_heartbeat_interval,
514 Duration::from_secs(60 * 60 * 24)
515 );
516 }
517
518 #[test]
519 fn test_extended_heartbeat_interval_from_env() {
520 use libdd_common::test_utils::EnvGuard;
521 use std::time::Duration;
522
523 let _guard = EnvGuard::set("DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL", "5");
524 let settings = Settings::from_env();
525 assert_eq!(
526 settings.telemetry_extended_heartbeat_interval,
527 Duration::from_secs(5)
528 );
529 }
530}