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#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
22pub struct Config {
23    /// Endpoint to send the data to
24    /// This is private and should be interacted with through the set_endpoint function
25    /// to ensure the url path is properly set
26    pub(crate) endpoint: Option<Endpoint>,
27    /// Enables debug logging
28    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    /// Prevents LifecycleAction::Stop from terminating the worker (except if the WorkerHandle is
33    /// dropped)
34    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/// Settings gathers configuration options we receive from the environment
70/// (either through env variable, or that could be set from the )
71#[derive(Debug)]
72pub struct Settings {
73    // Env parameter
74    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    // Filesystem check
87    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    // Agent connection configuration
112    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    // Location of the named pipe on windows. Dotnet specific
116    const DD_TRACE_PIPE_NAME: &'static str = "DD_TRACE_PIPE_NAME";
117
118    // Direct submission configuration
119    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    // Development and test env variables - should not be used by customers
125    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    // Implemented following
187    // https://github.com/DataDog/architecture/blob/master/rfcs/apm/integrations/trace-autodetect-agent-config/rfc.md
188    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    /// Get the configuration of the telemetry worker from env variables
281    pub fn from_env() -> Self {
282        let settings = Settings::from_env();
283        Self::from_settings(&settings)
284    }
285
286    /// set_host sets the host telemetry should connect to.
287    ///
288    /// It handles the following schemes
289    /// * http/https
290    /// * unix sockets unix://\<path to the socket>
291    /// * windows pipes of the format windows:\<pipe name>
292    /// * files, with the format file://\<path to the file>
293    ///
294    ///  If the host_url is http/https, any path will be ignored and replaced by the
295    /// appropriate telemetry endpoint path
296    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}