Skip to main content

libdd_telemetry/
config.rs

1// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4use 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/// Partial endpoint configuration applied through [`Config::set_endpoint`].
22///
23/// A `None` (or, for `timeout_ms`, `0`) field leaves the corresponding endpoint
24/// value untouched, so the struct doubles as a patch. `use_system_resolver` is
25/// always applied.
26#[derive(Debug, Default)]
27pub struct TelemetryEndpoint {
28    pub url: Option<String>,
29    pub api_key: Option<String>,
30    pub timeout_ms: u64,
31    /// Sets X-Datadog-Test-Session-Token header on any request
32    pub test_token: Option<String>,
33    /// Use the system DNS resolver when building the HTTP client. If false, the default
34    /// in-process resolver is used.
35    pub use_system_resolver: bool,
36}
37
38#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
39pub struct Config {
40    /// Endpoint to send the data to
41    /// This is private and should be interacted with through the set_endpoint function
42    /// to ensure the url path is properly set
43    pub(crate) endpoint: Option<Endpoint>,
44    /// Enables debug logging
45    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    /// Prevents LifecycleAction::Stop from terminating the worker (except if the WorkerHandle is
50    /// dropped)
51    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/// Settings gathers configuration options we receive from the environment
87/// (either through env variable, or that could be set from the )
88#[derive(Debug)]
89pub struct Settings {
90    // Env parameter
91    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    // Filesystem check
104    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    // Agent connection configuration
129    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    // Location of the named pipe on windows. Dotnet specific
133    const DD_TRACE_PIPE_NAME: &'static str = "DD_TRACE_PIPE_NAME";
134
135    // Direct submission configuration
136    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    // Development and test env variables - should not be used by customers
142    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    // Implemented following
204    // https://github.com/DataDog/architecture/blob/master/rfcs/apm/integrations/trace-autodetect-agent-config/rfc.md
205    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    /// Rewrites the endpoint path to the telemetry path appropriate for the
257    /// current scheme, API key and direct-submission setting. Called by
258    /// [`Config::set_endpoint`] after the endpoint fields have been updated.
259    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    /// Applies a [`TelemetryEndpoint`] patch to the endpoint, then rewrites the
270    /// path to the telemetry path appropriate for the resulting scheme and API
271    /// key. This is the single entry point for endpoint configuration so the URL
272    /// path invariant always holds.
273    pub fn set_endpoint(&mut self, endpoint: TelemetryEndpoint) -> anyhow::Result<()> {
274        // Parse the URL before touching `self.endpoint` so a parse error leaves
275        // the existing endpoint untouched.
276        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        // Move the owned Strings into the `Cow<'static, str>` fields — `Cow::from(String)`
284        // yields `Cow::Owned`, so no copy happens.
285        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    /// Sets (or, with `None`, clears) the `X-Datadog-Test-Session-Token` header
303    /// sent with requests. Unlike [`Config::set_endpoint`], `None` clears the
304    /// token rather than leaving it unchanged, and an absent endpoint is left
305    /// absent (no default is inserted).
306    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    /// Get the configuration of the telemetry worker from env variables
338    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    /// Test helper mirroring the old `set_host_from_url`: set only the URL and
356    /// let `set_endpoint` resolve the telemetry path.
357    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}