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    /// Whether to emit the `app-started`/`app-closing` lifecycle payloads.
63    /// Forked processes may not be required to emit these.
64    #[serde(default = "default_true")]
65    pub emit_app_lifecycle: bool,
66
67    /// Maximum number of endpoints serialized into one payload
68    /// (`DD_API_SECURITY_ENDPOINT_COLLECTION_MESSAGE_LIMIT` in the tracers).
69    /// Endpoints sending is batched per heartbeat interval.
70    #[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/// Settings gathers configuration options we receive from the environment
106/// (either through env variable, or that could be set from the )
107#[derive(Debug)]
108pub struct Settings {
109    // Env parameter
110    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    // Filesystem check
124    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    // Agent connection configuration
150    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    // Location of the named pipe on windows. Dotnet specific
154    const DD_TRACE_PIPE_NAME: &'static str = "DD_TRACE_PIPE_NAME";
155
156    // Direct submission configuration
157    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    // Development and test env variables - should not be used by customers
163    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    // Implemented following
233    // https://github.com/DataDog/architecture/blob/master/rfcs/apm/integrations/trace-autodetect-agent-config/rfc.md
234    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    /// Rewrites the endpoint path to the telemetry path appropriate for the
286    /// current scheme, API key and direct-submission setting. Called by
287    /// [`Config::set_endpoint`] after the endpoint fields have been updated.
288    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    /// Applies a [`TelemetryEndpoint`] patch to the endpoint, then rewrites the
299    /// path to the telemetry path appropriate for the resulting scheme and API
300    /// key. This accepts original, human-facing URL strings; callers that
301    /// already hold a parsed [`Uri`] must use [`Config::set_endpoint_uri`] to
302    /// avoid re-parsing encoded `file`, `unix`, or `windows` URIs.
303    pub fn set_endpoint(&mut self, endpoint: TelemetryEndpoint) -> anyhow::Result<()> {
304        // Parse the URL before touching `self.endpoint` so a parse error leaves
305        // the existing endpoint untouched.
306        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    /// Sets the endpoint URL from an already-parsed [`Uri`].
331    ///
332    /// This is the non-stringifying counterpart to the `url` field in
333    /// [`TelemetryEndpoint`]. It is intended for integrations that already hold
334    /// a parsed URI and therefore must not pass its string representation back
335    /// through [`parse_uri`]. Other endpoint properties remain unchanged.
336    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    /// Sets (or, with `None`, clears) the `X-Datadog-Test-Session-Token` header
342    /// sent with requests. Unlike [`Config::set_endpoint`], `None` clears the
343    /// token rather than leaving it unchanged, and an absent endpoint is left
344    /// absent (no default is inserted).
345    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    /// Get the configuration of the telemetry worker from env variables
379    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    /// Test helper mirroring the old `set_host_from_url`: set only the URL and
397    /// let `set_endpoint` resolve the telemetry path.
398    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}