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