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(Debug, Default)]
27pub struct TelemetryEndpoint {
28 pub url: Option<String>,
29 pub api_key: Option<String>,
30 pub timeout_ms: u64,
31 pub test_token: Option<String>,
33 pub use_system_resolver: bool,
36}
37
38#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
39pub struct Config {
40 pub(crate) endpoint: Option<Endpoint>,
44 pub telemetry_debug_logging_enabled: bool,
46 pub telemetry_heartbeat_interval: Duration,
47 pub telemetry_extended_heartbeat_interval: Duration,
48 pub direct_submission_enabled: bool,
49 pub restartable: bool,
52
53 pub debug_enabled: bool,
54
55 #[serde(default)]
56 pub session_id: Option<String>,
57 #[serde(default)]
58 pub parent_session_id: Option<String>,
59 #[serde(default)]
60 pub root_session_id: Option<String>,
61
62 #[serde(default = "default_true")]
65 pub emit_app_lifecycle: bool,
66
67 #[serde(default = "default_endpoints_message_limit")]
71 pub endpoints_message_limit: u32,
72}
73
74fn default_true() -> bool {
75 true
76}
77
78fn default_endpoints_message_limit() -> u32 {
79 300
80}
81
82fn endpoint_with_telemetry_path(
83 mut endpoint: Endpoint,
84 direct_submission_enabled: bool,
85) -> anyhow::Result<Endpoint> {
86 let mut uri_parts = endpoint.url.into_parts();
87 if uri_parts
88 .scheme
89 .as_ref()
90 .is_some_and(|scheme| scheme.as_str() != "file")
91 {
92 uri_parts.path_and_query = Some(PathAndQuery::from_static(
93 if endpoint.api_key.is_some() && direct_submission_enabled {
94 DIRECT_TELEMETRY_URL_PATH
95 } else {
96 AGENT_TELEMETRY_URL_PATH
97 },
98 ));
99 }
100
101 endpoint.url = Uri::from_parts(uri_parts)?;
102 Ok(endpoint)
103}
104
105#[derive(Debug)]
108pub struct Settings {
109 pub agent_host: Option<String>,
111 pub trace_agent_port: Option<u16>,
112 pub trace_agent_url: Option<String>,
113 pub trace_pipe_name: Option<String>,
114 pub direct_submission_enabled: bool,
115 pub api_key: Option<String>,
116 pub site: Option<String>,
117 pub telemetry_dd_url: Option<String>,
118 pub telemetry_heartbeat_interval: Duration,
119 pub telemetry_extended_heartbeat_interval: Duration,
120 pub endpoints_message_limit: u32,
121 pub shared_lib_debug: bool,
122
123 pub agent_uds_socket_found: bool,
125}
126
127impl Default for Settings {
128 fn default() -> Self {
129 Self {
130 agent_host: None,
131 trace_agent_port: None,
132 trace_agent_url: None,
133 trace_pipe_name: None,
134 direct_submission_enabled: false,
135 api_key: None,
136 site: None,
137 telemetry_dd_url: None,
138 telemetry_heartbeat_interval: Duration::from_secs(60),
139 telemetry_extended_heartbeat_interval: Duration::from_secs(60 * 60 * 24),
140 endpoints_message_limit: default_endpoints_message_limit(),
141 shared_lib_debug: false,
142
143 agent_uds_socket_found: false,
144 }
145 }
146}
147
148impl Settings {
149 const DD_TRACE_AGENT_URL: &'static str = "DD_TRACE_AGENT_URL";
151 const DD_AGENT_HOST: &'static str = "DD_AGENT_HOST";
152 const DD_TRACE_AGENT_PORT: &'static str = "DD_TRACE_AGENT_PORT";
153 const DD_TRACE_PIPE_NAME: &'static str = "DD_TRACE_PIPE_NAME";
155
156 const _DD_DIRECT_SUBMISSION_ENABLED: &'static str = "_DD_DIRECT_SUBMISSION_ENABLED";
158 const DD_API_KEY: &'static str = "DD_API_KEY";
159 const DD_SITE: &'static str = "DD_SITE";
160 const DD_APM_TELEMETRY_DD_URL: &'static str = "DD_APM_TELEMETRY_DD_URL";
161
162 const DD_TELEMETRY_HEARTBEAT_INTERVAL: &'static str = "DD_TELEMETRY_HEARTBEAT_INTERVAL";
164 const DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL: &'static str =
165 "DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL";
166 const DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT: &'static str =
167 "DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT";
168 const _DD_SHARED_LIB_DEBUG: &'static str = "_DD_SHARED_LIB_DEBUG";
169
170 pub fn from_env() -> Self {
171 debug!(
172 config.source = "environment",
173 "Loading telemetry settings from environment variables"
174 );
175 let default = Self::default();
176 Self {
177 agent_host: parse_env::str_not_empty(Self::DD_AGENT_HOST),
178 trace_agent_port: parse_env::int(Self::DD_TRACE_AGENT_PORT),
179 trace_agent_url: parse_env::str_not_empty(Self::DD_TRACE_AGENT_URL)
180 .or(default.trace_agent_url),
181 trace_pipe_name: parse_env::str_not_empty(Self::DD_TRACE_PIPE_NAME)
182 .or(default.trace_pipe_name),
183 direct_submission_enabled: parse_env::bool(Self::_DD_DIRECT_SUBMISSION_ENABLED)
184 .unwrap_or(default.direct_submission_enabled),
185 api_key: parse_env::str_not_empty(Self::DD_API_KEY),
186 site: parse_env::str_not_empty(Self::DD_SITE),
187 telemetry_dd_url: parse_env::str_not_empty(Self::DD_APM_TELEMETRY_DD_URL),
188 telemetry_heartbeat_interval: parse_env::duration(
189 Self::DD_TELEMETRY_HEARTBEAT_INTERVAL,
190 )
191 .unwrap_or(Duration::from_secs(60)),
192 telemetry_extended_heartbeat_interval: parse_env::duration(
193 Self::DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL,
194 )
195 .unwrap_or(Duration::from_secs(60 * 60 * 24)),
196 shared_lib_debug: parse_env::bool(Self::_DD_SHARED_LIB_DEBUG).unwrap_or(false),
197 endpoints_message_limit: parse_env::int(
198 Self::DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT,
199 )
200 .unwrap_or(default_endpoints_message_limit()),
201
202 agent_uds_socket_found: (|| {
203 #[cfg(unix)]
204 return std::fs::metadata(TRACE_SOCKET_PATH).is_ok();
205 #[cfg(not(unix))]
206 return false;
207 })(),
208 }
209 }
210}
211
212impl Default for Config {
213 fn default() -> Self {
214 Self {
215 endpoint: None,
216 telemetry_debug_logging_enabled: false,
217 telemetry_heartbeat_interval: Duration::from_secs(60),
218 telemetry_extended_heartbeat_interval: Duration::from_secs(60 * 60 * 24),
219 direct_submission_enabled: false,
220 restartable: false,
221 debug_enabled: false,
222 session_id: None,
223 parent_session_id: None,
224 root_session_id: None,
225 emit_app_lifecycle: true,
226 endpoints_message_limit: default_endpoints_message_limit(),
227 }
228 }
229}
230
231impl Config {
232 fn trace_agent_url_from_setting(settings: &Settings) -> String {
235 None.or_else(|| {
236 settings
237 .trace_agent_url
238 .as_deref()
239 .filter(|u| {
240 u.starts_with("unix://")
241 || u.starts_with("http://")
242 || u.starts_with("https://")
243 })
244 .map(ToString::to_string)
245 })
246 .or_else(|| {
247 #[cfg(windows)]
248 return settings
249 .trace_pipe_name
250 .as_ref()
251 .map(|pipe_name| format!("windows:{pipe_name}"));
252 #[cfg(not(windows))]
253 return None;
254 })
255 .or_else(|| match (&settings.agent_host, settings.trace_agent_port) {
256 (None, None) => None,
257 _ => Some(format!(
258 "http://{}:{}",
259 settings.agent_host.as_deref().unwrap_or(DEFAULT_AGENT_HOST),
260 settings.trace_agent_port.unwrap_or(DEFAULT_AGENT_PORT),
261 )),
262 })
263 .or_else(|| {
264 #[cfg(unix)]
265 return settings
266 .agent_uds_socket_found
267 .then(|| format!("unix://{TRACE_SOCKET_PATH}"));
268 #[cfg(not(unix))]
269 return None;
270 })
271 .unwrap_or_else(|| format!("http://{DEFAULT_AGENT_HOST}:{DEFAULT_AGENT_PORT}"))
272 }
273
274 fn api_key_from_settings(settings: &Settings) -> Option<Cow<'static, str>> {
275 if !settings.direct_submission_enabled {
276 return None;
277 }
278 settings.api_key.clone().map(Cow::Owned)
279 }
280
281 pub fn endpoint(&self) -> Option<&Endpoint> {
282 self.endpoint.as_ref()
283 }
284
285 fn apply_telemetry_path(&mut self) -> anyhow::Result<()> {
289 if let Some(endpoint) = self.endpoint.take() {
290 self.endpoint = Some(endpoint_with_telemetry_path(
291 endpoint,
292 self.direct_submission_enabled,
293 )?);
294 }
295 Ok(())
296 }
297
298 pub fn set_endpoint(&mut self, endpoint: TelemetryEndpoint) -> anyhow::Result<()> {
304 let url = endpoint.url.as_deref().map(parse_uri).transpose()?;
307
308 let inner = self.endpoint.get_or_insert_with(Endpoint::default);
309 if let Some(url) = url {
310 inner.url = url;
311 }
312
313 if let Some(api_key) = endpoint.api_key {
314 inner.api_key = Some(Cow::from(api_key));
315 }
316
317 if let Some(test_token) = endpoint.test_token {
318 inner.test_token = Some(Cow::from(test_token));
319 }
320
321 if endpoint.timeout_ms != 0 {
322 inner.timeout_ms = endpoint.timeout_ms;
323 }
324
325 inner.use_system_resolver = endpoint.use_system_resolver;
326
327 self.apply_telemetry_path()
328 }
329
330 pub fn set_endpoint_uri(&mut self, uri: Uri) -> anyhow::Result<()> {
337 self.endpoint.get_or_insert_with(Endpoint::default).url = uri;
338 self.apply_telemetry_path()
339 }
340
341 pub fn set_endpoint_test_token<T: Into<Cow<'static, str>>>(&mut self, test_token: Option<T>) {
346 if let Some(endpoint) = &mut self.endpoint {
347 endpoint.test_token = test_token.map(|token| token.into());
348 }
349 }
350
351 pub fn from_settings(settings: &Settings) -> Self {
352 let trace_agent_url = Self::trace_agent_url_from_setting(settings);
353 let api_key = Self::api_key_from_settings(settings);
354
355 let mut this = Self {
356 endpoint: None,
357 telemetry_debug_logging_enabled: settings.shared_lib_debug,
358 telemetry_heartbeat_interval: settings.telemetry_heartbeat_interval,
359 telemetry_extended_heartbeat_interval: settings.telemetry_extended_heartbeat_interval,
360 direct_submission_enabled: settings.direct_submission_enabled,
361 restartable: false,
362 debug_enabled: false,
363 session_id: None,
364 parent_session_id: None,
365 root_session_id: None,
366 emit_app_lifecycle: true,
367 endpoints_message_limit: settings.endpoints_message_limit,
368 };
369
370 _ = this.set_endpoint(TelemetryEndpoint {
371 url: Some(trace_agent_url),
372 api_key: api_key.map(Cow::into_owned),
373 ..Default::default()
374 });
375 this
376 }
377
378 pub fn from_env() -> Self {
380 let settings = Settings::from_env();
381 Self::from_settings(&settings)
382 }
383}
384
385#[cfg(test)]
386mod tests {
387 use std::path::Path;
388
389 #[cfg(unix)]
390 use libdd_common::connector::uds;
391
392 use libdd_common::connector::named_pipe;
393
394 use super::{Config, Settings, TelemetryEndpoint};
395
396 fn set_host_from_url(cfg: &mut Config, host_url: &str) -> anyhow::Result<()> {
399 cfg.set_endpoint(TelemetryEndpoint {
400 url: Some(host_url.to_owned()),
401 ..Default::default()
402 })
403 }
404
405 #[test]
406 fn test_agent_host_detection_trace_agent_url_should_take_precedence() {
407 let cases = [
408 (
409 "http://localhost:1234",
410 "http://localhost:1234/telemetry/proxy/api/v2/apmtelemetry",
411 ),
412 (
413 "unix://./here",
414 "unix://2e2f68657265/telemetry/proxy/api/v2/apmtelemetry",
415 ),
416 ];
417 for (trace_agent_url, expected) in cases {
418 let settings = Settings {
419 trace_agent_url: Some(trace_agent_url.to_owned()),
420 agent_host: Some("example.org".to_owned()),
421 trace_agent_port: Some(1),
422 trace_pipe_name: Some("C:\\foo".to_owned()),
423 agent_uds_socket_found: true,
424 ..Default::default()
425 };
426 let cfg = Config::from_settings(&settings);
427 assert_eq!(cfg.endpoint.unwrap().url.to_string(), expected);
428 }
429 }
430
431 #[test]
432 fn test_agent_host_detection_agent_host_and_port() {
433 let cases = [
434 (
435 Some("example.org"),
436 Some(1),
437 "http://example.org:1/telemetry/proxy/api/v2/apmtelemetry",
438 ),
439 (
440 Some("example.org"),
441 None,
442 "http://example.org:8126/telemetry/proxy/api/v2/apmtelemetry",
443 ),
444 (
445 None,
446 Some(1),
447 "http://localhost:1/telemetry/proxy/api/v2/apmtelemetry",
448 ),
449 ];
450 for (agent_host, trace_agent_port, expected) in cases {
451 let settings = Settings {
452 trace_agent_url: None,
453 agent_host: agent_host.map(ToString::to_string),
454 trace_agent_port,
455 trace_pipe_name: None,
456 agent_uds_socket_found: true,
457 ..Default::default()
458 };
459 let cfg = Config::from_settings(&settings);
460 assert_eq!(cfg.endpoint.unwrap().url.to_string(), expected);
461 }
462 }
463
464 #[test]
465 #[cfg(unix)]
466 fn test_agent_host_detection_socket_found() {
467 let settings = Settings {
468 trace_agent_url: None,
469 agent_host: None,
470 trace_agent_port: None,
471 trace_pipe_name: None,
472 agent_uds_socket_found: true,
473 ..Default::default()
474 };
475 let cfg = Config::from_settings(&settings);
476 assert_eq!(
477 cfg.endpoint.unwrap().url.to_string(),
478 "unix://2f7661722f72756e2f64617461646f672f61706d2e736f636b6574/telemetry/proxy/api/v2/apmtelemetry"
479 );
480 }
481
482 #[test]
483 fn test_agent_host_detection_fallback() {
484 let settings = Settings {
485 trace_agent_url: None,
486 agent_host: None,
487 trace_agent_port: None,
488 trace_pipe_name: None,
489 agent_uds_socket_found: false,
490 ..Default::default()
491 };
492
493 let cfg = Config::from_settings(&settings);
494 assert_eq!(
495 cfg.endpoint.unwrap().url.to_string(),
496 "http://localhost:8126/telemetry/proxy/api/v2/apmtelemetry"
497 );
498 }
499
500 #[test]
501 fn test_config_set_url() {
502 let mut cfg = Config::default();
503
504 set_host_from_url(&mut cfg, "http://example.com/any_path_will_be_ignored").unwrap();
505
506 assert_eq!(
507 "http://example.com/telemetry/proxy/api/v2/apmtelemetry",
508 cfg.clone().endpoint.unwrap().url
509 );
510 }
511
512 #[test]
513 fn test_config_set_url_file() {
514 let cases = [
515 ("file:///absolute/path", "/absolute/path"),
516 ("file://./relative/path", "./relative/path"),
517 ("file://relative/path", "relative/path"),
518 (
519 "file://c://temp//with space\\foo.json",
520 "c://temp//with space\\foo.json",
521 ),
522 ];
523
524 for (input, expected) in cases {
525 let mut cfg = Config::default();
526 set_host_from_url(&mut cfg, input).unwrap();
527
528 assert_eq!(
529 "file",
530 cfg.clone()
531 .endpoint
532 .unwrap()
533 .url
534 .scheme()
535 .unwrap()
536 .to_string()
537 );
538 assert_eq!(
539 Path::new(expected),
540 libdd_common::decode_uri_path_in_authority(&cfg.endpoint.unwrap().url).unwrap(),
541 );
542 }
543 }
544
545 #[test]
546 fn test_config_set_parsed_file_uri_does_not_reencode_path() {
547 let mut cfg = Config::default();
548 let uri = libdd_common::parse_uri("file:///absolute/path").unwrap();
549
550 cfg.set_endpoint_uri(uri).unwrap();
551
552 let endpoint = cfg.endpoint().unwrap();
553 assert_eq!(
554 Path::new("/absolute/path"),
555 libdd_common::decode_uri_path_in_authority(&endpoint.url).unwrap()
556 );
557 }
558
559 #[test]
560 #[cfg(unix)]
561 fn test_config_set_url_unix_socket() {
562 let mut cfg = Config::default();
563
564 set_host_from_url(&mut cfg, "unix:///compatiliby/path").unwrap();
565 assert_eq!(
566 "unix://2f636f6d706174696c6962792f70617468/telemetry/proxy/api/v2/apmtelemetry",
567 cfg.clone().endpoint.unwrap().url.to_string()
568 );
569 assert_eq!(
570 "/compatiliby/path",
571 uds::socket_path_from_uri(&cfg.clone().endpoint.unwrap().url)
572 .unwrap()
573 .to_string_lossy()
574 );
575 }
576
577 #[test]
578 fn test_config_set_url_windows_pipe() {
579 let mut cfg = Config::default();
580
581 set_host_from_url(&mut cfg, "windows:C:\\system32\\foo").unwrap();
582 assert_eq!(
583 "windows://433a5c73797374656d33325c666f6f/telemetry/proxy/api/v2/apmtelemetry",
584 cfg.clone().endpoint.unwrap().url.to_string()
585 );
586 assert_eq!(
587 "C:\\system32\\foo",
588 named_pipe::named_pipe_path_from_uri(&cfg.clone().endpoint.unwrap().url)
589 .unwrap()
590 .to_string_lossy()
591 );
592 }
593
594 #[test]
595 fn test_from_settings_propagates_extended_heartbeat_interval() {
596 use std::time::Duration;
597
598 let custom_interval = Duration::from_secs(120);
599 let settings = Settings {
600 telemetry_extended_heartbeat_interval: custom_interval,
601 ..Default::default()
602 };
603 let cfg = Config::from_settings(&settings);
604 assert_eq!(cfg.telemetry_extended_heartbeat_interval, custom_interval);
605 }
606
607 #[test]
608 fn test_from_settings_default_extended_heartbeat_interval() {
609 use std::time::Duration;
610
611 let settings = Settings::default();
612 let cfg = Config::from_settings(&settings);
613 assert_eq!(
614 cfg.telemetry_extended_heartbeat_interval,
615 Duration::from_secs(60 * 60 * 24)
616 );
617 }
618
619 #[test]
620 fn test_extended_heartbeat_interval_from_env() {
621 use libdd_common::test_utils::EnvGuard;
622 use std::time::Duration;
623
624 let _guard = EnvGuard::set("DD_TELEMETRY_EXTENDED_HEARTBEAT_INTERVAL", "5");
625 let settings = Settings::from_env();
626 assert_eq!(
627 settings.telemetry_extended_heartbeat_interval,
628 Duration::from_secs(5)
629 );
630 }
631}