Skip to main content

libdd_library_config/
lib.rs

1// Copyright 2021-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3pub mod otel_process_ctx;
4pub mod tracer_metadata;
5
6use std::borrow::Cow;
7use std::cell::OnceCell;
8use std::collections::HashMap;
9use std::ops::Deref;
10use std::path::Path;
11use std::{env, fs, io, mem};
12
13/// This struct holds maps used to match and template configurations.
14///
15/// They are computed lazily so that if the templating feature is not necessary, we don't
16/// have to create the maps.
17///
18/// These maps come from one of three origins:
19///  * tags: This one is fairly simple, the format is tag_key: tag_value
20///  * envs: Splits env variables with format KEY=VALUE
21///  * args: Splits args with format key=value. If the arg doesn't contain an '=', skip it
22struct MatchMaps<'a> {
23    tags: &'a HashMap<String, String>,
24    env_map: OnceCell<HashMap<&'a str, &'a str>>,
25    args_map: OnceCell<HashMap<&'a str, &'a str>>,
26}
27
28impl<'a> MatchMaps<'a> {
29    fn env(&self, process_info: &'a ProcessInfo) -> &HashMap<&'a str, &'a str> {
30        self.env_map.get_or_init(|| {
31            let mut map = HashMap::new();
32            for e in &process_info.envp {
33                let Ok(s) = std::str::from_utf8(e.deref()) else {
34                    continue;
35                };
36                let (k, v) = match s.split_once('=') {
37                    Some((k, v)) => (k, v),
38                    None => (s, ""),
39                };
40                map.insert(k, v);
41            }
42            map
43        })
44    }
45
46    fn args(&self, process_info: &'a ProcessInfo) -> &HashMap<&str, &str> {
47        self.args_map.get_or_init(|| {
48            let mut map = HashMap::new();
49            for arg in &process_info.args {
50                let Ok(arg) = std::str::from_utf8(arg.deref()) else {
51                    continue;
52                };
53                // Split args between key and value on '='
54                if let Some((k, v)) = arg.split_once('=') {
55                    map.insert(k, v);
56                }
57            }
58            map
59        })
60    }
61}
62
63struct Matcher<'a> {
64    process_info: &'a ProcessInfo,
65    match_maps: MatchMaps<'a>,
66}
67
68impl<'a> Matcher<'a> {
69    fn new(process_info: &'a ProcessInfo, tags: &'a HashMap<String, String>) -> Self {
70        Self {
71            process_info,
72            match_maps: MatchMaps {
73                tags,
74                env_map: OnceCell::new(),
75                args_map: OnceCell::new(),
76            },
77        }
78    }
79
80    /// Returns the first set of configurations that match the current process
81    fn find_stable_config<'b>(&'a self, cfg: &'b StableConfig) -> Option<&'b ConfigMap> {
82        for rule in &cfg.rules {
83            if rule.selectors.iter().all(|s| self.selector_match(s)) {
84                return Some(&rule.configuration);
85            }
86        }
87        None
88    }
89
90    /// Returns true if the selector matches the process
91    ///
92    /// Any element in the "matches" section of the selector must match, they are ORed,
93    /// as selectors are ANDed.
94    fn selector_match(&'a self, selector: &Selector) -> bool {
95        match selector.origin {
96            Origin::Language => string_selector(selector, self.process_info.language.deref()),
97            Origin::ProcessArguments => match &selector.key {
98                Some(key) => {
99                    let arg_map = self.match_maps.args(self.process_info);
100                    map_operator_match(selector, arg_map, key)
101                }
102                None => string_list_selector(selector, &self.process_info.args),
103            },
104            Origin::EnvironmentVariables => match &selector.key {
105                Some(key) => {
106                    let env_map = self.match_maps.env(self.process_info);
107                    map_operator_match(selector, env_map, key)
108                }
109                None => string_list_selector(selector, &self.process_info.envp),
110            },
111            Origin::Tags => match &selector.key {
112                Some(key) => map_operator_match(selector, self.match_maps.tags, key),
113                None => false,
114            },
115        }
116    }
117
118    /// Templates a config string.
119    ///
120    /// variables are enclosed in double curly brackets "{{" and "}}"
121    ///
122    /// For instance:
123    ///
124    /// with the following varriable definition, var = "abc" var2 = "def", this transforms \
125    /// "foo_{{ var }}_bar_{{ var2 }}" -> "foo_abc_bar_def"
126    fn template_config(&'a self, config_val: &str) -> anyhow::Result<String> {
127        let mut rest = config_val;
128        let mut templated = String::with_capacity(config_val.len());
129        loop {
130            let Some((head, after_bracket)) = rest.split_once("{{") else {
131                templated.push_str(rest);
132                return Ok(templated);
133            };
134            templated.push_str(head);
135            let Some((template_var, tail)) = after_bracket.split_once("}}") else {
136                anyhow::bail!("unterminated template in config")
137            };
138            let (template_var, index) = parse_template_var(template_var.trim());
139            let val = match template_var {
140                "language" => String::from_utf8_lossy(self.process_info.language.deref()),
141                "environment_variables" => {
142                    template_map_key(index, self.match_maps.env(self.process_info))
143                }
144                "process_arguments" => {
145                    template_map_key(index, self.match_maps.args(self.process_info))
146                }
147                "tags" => template_map_key(index, self.match_maps.tags),
148                _ => std::borrow::Cow::Borrowed("UNDEFINED"),
149            };
150            templated.push_str(&val);
151            rest = tail;
152        }
153    }
154}
155
156fn map_operator_match(selector: &Selector, map: &impl Get, key: &str) -> bool {
157    let Some(val) = map.get(key) else {
158        return false;
159    };
160    string_selector(selector, val.as_bytes())
161}
162
163fn parse_template_var(template_var: &str) -> (&str, Option<&str>) {
164    match template_var.trim().split_once('[') {
165        Some((template_var, idx)) => {
166            let Some((index, _)) = idx.split_once(']') else {
167                return (template_var, None);
168            };
169            (template_var, Some(index.trim()))
170        }
171        None => (template_var, None),
172    }
173}
174
175fn template_map_key<'a>(key: Option<&str>, map: &'a impl Get) -> Cow<'a, str> {
176    let Some(key) = key else {
177        return Cow::Borrowed("UNDEFINED");
178    };
179    Cow::Borrowed(map.get(key).unwrap_or("UNDEFINED"))
180}
181
182#[repr(C)]
183pub struct ProcessInfo {
184    pub args: Vec<Vec<u8>>,
185    pub envp: Vec<Vec<u8>>,
186    pub language: Vec<u8>,
187}
188
189fn process_envp() -> Vec<Vec<u8>> {
190    #[allow(clippy::unnecessary_filter_map)]
191    env::vars_os()
192        .filter_map(|(k, v)| {
193            #[cfg(not(unix))]
194            {
195                let mut env = Vec::new();
196                env.extend(k.to_str()?.as_bytes());
197                env.push(b'=');
198                env.extend(v.to_str()?.as_bytes());
199                Some(env)
200            }
201            #[cfg(unix)]
202            {
203                use std::os::unix::ffi::OsStrExt;
204                let mut env = Vec::new();
205                env.extend(k.as_bytes());
206                env.push(b'=');
207                env.extend(v.as_bytes());
208                Some(env)
209            }
210        })
211        .collect()
212}
213
214fn process_args() -> Vec<Vec<u8>> {
215    #[allow(clippy::unnecessary_filter_map)]
216    env::args_os()
217        .filter_map(|a| {
218            #[cfg(not(unix))]
219            {
220                Some(a.into_string().ok()?.into_bytes())
221            }
222            #[cfg(unix)]
223            {
224                use std::os::unix::ffi::OsStringExt;
225                Some(a.into_vec())
226            }
227        })
228        .collect()
229}
230
231impl ProcessInfo {
232    pub fn detect_global(language: String) -> Self {
233        let envp = process_envp();
234        let args = process_args();
235        Self {
236            args,
237            envp,
238            language: language.into_bytes(),
239        }
240    }
241}
242
243/// A (key, value) struct
244///
245/// This type has a custom serde Deserialize implementation from maps:
246/// * It skips invalid/unknown keys in the map
247/// * Since the storage is a Boxed slice and not a Hashmap, it doesn't over-allocate
248#[derive(Debug, Default, PartialEq, Eq)]
249struct ConfigMap(Box<[(String, String)]>);
250
251impl<'de> serde::Deserialize<'de> for ConfigMap {
252    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
253    where
254        D: serde::Deserializer<'de>,
255    {
256        struct ConfigMapVisitor;
257        impl<'de> serde::de::Visitor<'de> for ConfigMapVisitor {
258            type Value = ConfigMap;
259
260            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
261                formatter.write_str("struct ConfigMap(HashMap<String, String>)")
262            }
263
264            fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
265            where
266                A: serde::de::MapAccess<'de>,
267            {
268                let mut configs = Vec::new();
269                configs.reserve_exact(map.size_hint().unwrap_or(0));
270                loop {
271                    let k = match map.next_key::<String>() {
272                        Ok(Some(k)) => k,
273                        Ok(None) => break,
274                        Err(_) => {
275                            map.next_value::<serde::de::IgnoredAny>()?;
276                            continue;
277                        }
278                    };
279                    let v = map.next_value::<String>()?;
280                    configs.push((k, v));
281                }
282                Ok(ConfigMap(configs.into_boxed_slice()))
283            }
284        }
285        deserializer.deserialize_map(ConfigMapVisitor)
286    }
287}
288
289#[repr(C)]
290#[derive(Clone, Copy, serde::Deserialize, Debug, PartialEq, Eq, Hash)]
291#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
292#[allow(clippy::enum_variant_names)]
293pub enum LibraryConfigSource {
294    // Order matters, as it is used to determine the priority of the source.
295    //  The higher the value, the higher the priority.
296    LocalStableConfig = 0,
297    FleetStableConfig = 1,
298}
299
300impl LibraryConfigSource {
301    pub fn to_str(&self) -> &'static str {
302        use LibraryConfigSource::*;
303        match self {
304            LocalStableConfig => "local_stable_config",
305            FleetStableConfig => "fleet_stable_config",
306        }
307    }
308}
309
310#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
311#[serde(rename_all = "snake_case")]
312enum Origin {
313    ProcessArguments,
314    EnvironmentVariables,
315    Language,
316    Tags,
317}
318
319#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
320#[serde(rename_all = "snake_case")]
321#[serde(tag = "operator")]
322enum Operator {
323    Exists,
324    Equals { matches: Vec<String> },
325    PrefixMatches { matches: Vec<String> },
326    SuffixMatches { matches: Vec<String> },
327    // todo
328    // WildcardMatches,
329}
330
331#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
332struct Selector {
333    origin: Origin,
334    #[serde(default)]
335    key: Option<String>,
336    #[serde(flatten)]
337    operator: Operator,
338}
339
340#[derive(serde::Deserialize, Debug, PartialEq, Eq)]
341struct Rule {
342    selectors: Vec<Selector>,
343    configuration: ConfigMap,
344}
345
346#[derive(serde::Deserialize, Default, Debug, PartialEq, Eq)]
347struct StableConfig {
348    // Phase 1
349    #[serde(default)]
350    config_id: Option<String>,
351    #[serde(default)]
352    apm_configuration_default: ConfigMap,
353
354    // Phase 2
355    #[serde(default)]
356    tags: HashMap<String, String>,
357    #[serde(default)]
358    rules: Vec<Rule>,
359}
360
361fn string_list_selector<B: Deref<Target = [u8]>>(selector: &Selector, l: &[B]) -> bool {
362    l.iter().any(|v| string_selector(selector, v.deref()))
363}
364
365fn string_selector(selector: &Selector, value: &[u8]) -> bool {
366    let matches = match &selector.operator {
367        Operator::Exists => return true,
368        Operator::Equals { matches } => matches,
369        Operator::PrefixMatches { matches } => matches,
370        Operator::SuffixMatches { matches } => matches,
371    };
372    matches
373        .iter()
374        .any(|m| string_operator_match(&selector.operator, m.as_bytes(), value))
375}
376
377fn string_operator_match(op: &Operator, matches: &[u8], value: &[u8]) -> bool {
378    match op {
379        Operator::Equals { .. } => matches == value,
380        Operator::PrefixMatches { .. } => value.starts_with(matches),
381        Operator::SuffixMatches { .. } => value.ends_with(matches),
382        Operator::Exists => true,
383        // Operator::WildcardMatches => todo!("Wildcard matches is not implemented"),
384    }
385}
386
387#[derive(Debug, PartialEq, Eq)]
388/// LibraryConfig represent a configuration item and is part of the public API
389/// of this module
390pub struct LibraryConfig {
391    pub name: String,
392    pub value: String,
393    pub source: LibraryConfigSource,
394    pub config_id: Option<String>,
395}
396
397#[derive(Debug)]
398/// This struct is used to hold configuration item data in a Hashmap, while the name of
399/// the configuration is the key used for deduplication
400struct LibraryConfigVal {
401    value: String,
402    source: LibraryConfigSource,
403    config_id: Option<String>,
404}
405
406#[derive(Debug)]
407pub struct Configurator {
408    debug_logs: bool,
409}
410
411pub enum Target {
412    Linux,
413    Macos,
414    Windows,
415}
416
417impl Target {
418    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
419    const fn current() -> Self {
420        #[cfg(target_os = "linux")]
421        {
422            Self::Linux
423        }
424        #[cfg(target_os = "macos")]
425        {
426            Self::Macos
427        }
428        #[cfg(windows)]
429        {
430            Self::Windows
431        }
432    }
433}
434
435#[derive(Debug)]
436pub enum LoggedResult<T, E> {
437    Ok(T, Vec<String>),
438    Err(E),
439}
440
441impl<T, E> LoggedResult<T, E> {
442    pub fn data(self) -> Result<T, E> {
443        match self {
444            LoggedResult::Ok(value, _) => Ok(value),
445            LoggedResult::Err(err) => Err(err),
446        }
447    }
448
449    pub fn logs(&self) -> &[String] {
450        match self {
451            LoggedResult::Ok(_, logs) => logs,
452            LoggedResult::Err(_) => &[],
453        }
454    }
455
456    pub fn into_logs(self) -> Vec<String> {
457        match self {
458            LoggedResult::Ok(_, logs) => logs,
459            LoggedResult::Err(_) => Vec::new(),
460        }
461    }
462
463    pub fn logs_as_string(&self) -> String {
464        self.logs().join("\n")
465    }
466}
467
468impl Configurator {
469    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
470    pub const FLEET_STABLE_CONFIGURATION_PATH: &'static str =
471        Self::fleet_stable_configuration_path(Target::current());
472
473    #[cfg(any(target_os = "linux", target_os = "macos", windows))]
474    pub const LOCAL_STABLE_CONFIGURATION_PATH: &'static str =
475        Self::local_stable_configuration_path(Target::current());
476
477    pub const fn local_stable_configuration_path(target: Target) -> &'static str {
478        match target {
479            Target::Linux => "/etc/datadog-agent/application_monitoring.yaml",
480            Target::Macos => "/opt/datadog-agent/etc/application_monitoring.yaml",
481            Target::Windows => "C:\\ProgramData\\Datadog\\application_monitoring.yaml",
482        }
483    }
484
485    pub const fn fleet_stable_configuration_path(target: Target) -> &'static str {
486        match target {
487            Target::Linux => "/etc/datadog-agent/managed/datadog-agent/stable/application_monitoring.yaml",
488            Target::Macos => "/opt/datadog-agent/etc/stable/application_monitoring.yaml",
489            Target::Windows => "C:\\ProgramData\\Datadog\\managed\\datadog-agent\\stable\\application_monitoring.yaml",
490        }
491    }
492
493    pub fn new(debug_logs: bool) -> Self {
494        Self { debug_logs }
495    }
496
497    fn parse_stable_config_slice(&self, buf: &[u8]) -> LoggedResult<StableConfig, anyhow::Error> {
498        let stable_config = if buf.is_empty() {
499            StableConfig::default()
500        } else {
501            match serde_yaml::from_slice(buf) {
502                Ok(config) => config,
503                Err(e) => return LoggedResult::Err(e.into()),
504            }
505        };
506
507        let messages = if self.debug_logs {
508            vec![format!(
509                "Read the following static config: {stable_config:?}"
510            )]
511        } else {
512            Vec::new()
513        };
514
515        LoggedResult::Ok(stable_config, messages)
516    }
517
518    fn parse_stable_config_file<F: io::Read>(
519        &self,
520        mut f: F,
521    ) -> LoggedResult<StableConfig, anyhow::Error> {
522        let mut buffer = Vec::new();
523        match f.read_to_end(&mut buffer) {
524            Ok(_) => {}
525            Err(e) => return LoggedResult::Err(e.into()),
526        }
527        self.parse_stable_config_slice(utils::trim_bytes(&buffer))
528    }
529
530    pub fn get_config_from_file(
531        &self,
532        path_local: &Path,
533        path_managed: &Path,
534        process_info: &ProcessInfo,
535    ) -> LoggedResult<Vec<LibraryConfig>, anyhow::Error> {
536        let mut debug_messages = Vec::new();
537        if self.debug_logs {
538            debug_messages.push("Reading stable configuration from files:".to_string());
539            debug_messages.push(format!("\tlocal: {path_local:?}"));
540            debug_messages.push(format!("\tfleet: {path_managed:?}"));
541        }
542
543        let local_config = match fs::File::open(path_local) {
544            Ok(file) => {
545                match file.metadata() {
546                    Ok(metadata) => {
547                        // Fail if the file is > 100mb
548                        if metadata.len() > 1024 * 1024 * 100 {
549                            debug_messages.push(
550                                "failed to read local config file: file is too large (> 100mb)"
551                                    .to_string(),
552                            );
553                            StableConfig::default()
554                        } else {
555                            match self.parse_stable_config_file(file) {
556                                LoggedResult::Ok(config, logs) => {
557                                    debug_messages.extend(logs);
558                                    config
559                                }
560                                LoggedResult::Err(e) => return LoggedResult::Err(e),
561                            }
562                        }
563                    }
564                    Err(e) => {
565                        return LoggedResult::Err(
566                            anyhow::Error::from(e).context("failed to get file metadata"),
567                        )
568                    }
569                }
570            }
571            Err(e) if e.kind() == io::ErrorKind::NotFound => StableConfig::default(),
572            Err(e) => {
573                return LoggedResult::Err(
574                    anyhow::Error::from(e).context("failed to open config file"),
575                )
576            }
577        };
578        let fleet_config = match fs::File::open(path_managed) {
579            Ok(file) => {
580                match file.metadata() {
581                    Ok(metadata) => {
582                        // Fail if the file is > 100mb
583                        if metadata.len() > 1024 * 1024 * 100 {
584                            debug_messages.push(
585                                "failed to read fleet config file: file is too large (> 100mb)"
586                                    .to_string(),
587                            );
588                            StableConfig::default()
589                        } else {
590                            match self.parse_stable_config_file(file) {
591                                LoggedResult::Ok(config, logs) => {
592                                    debug_messages.extend(logs);
593                                    config
594                                }
595                                LoggedResult::Err(e) => return LoggedResult::Err(e),
596                            }
597                        }
598                    }
599                    Err(e) => {
600                        return LoggedResult::Err(
601                            anyhow::Error::from(e).context("failed to get file metadata"),
602                        )
603                    }
604                }
605            }
606            Err(e) if e.kind() == io::ErrorKind::NotFound => StableConfig::default(),
607            Err(e) => {
608                return LoggedResult::Err(
609                    anyhow::Error::from(e).context("failed to open config file"),
610                )
611            }
612        };
613
614        match self.get_config(local_config, fleet_config, process_info) {
615            LoggedResult::Ok(configs, msgs) => {
616                debug_messages.extend(msgs);
617                LoggedResult::Ok(configs, debug_messages)
618            }
619            LoggedResult::Err(e) => LoggedResult::Err(e),
620        }
621    }
622
623    pub fn get_config_from_bytes(
624        &self,
625        s_local: &[u8],
626        s_managed: &[u8],
627        process_info: ProcessInfo,
628    ) -> anyhow::Result<Vec<LibraryConfig>> {
629        let local_config = match self.parse_stable_config_slice(s_local) {
630            LoggedResult::Ok(config, _) => config,
631            LoggedResult::Err(e) => return Err(e),
632        };
633        let fleet_config = match self.parse_stable_config_slice(s_managed) {
634            LoggedResult::Ok(config, _) => config,
635            LoggedResult::Err(e) => return Err(e),
636        };
637
638        match self.get_config(local_config, fleet_config, &process_info) {
639            LoggedResult::Ok(configs, _) => Ok(configs),
640            LoggedResult::Err(e) => Err(e),
641        }
642    }
643
644    fn get_config(
645        &self,
646        local_config: StableConfig,
647        fleet_config: StableConfig,
648        process_info: &ProcessInfo,
649    ) -> LoggedResult<Vec<LibraryConfig>, anyhow::Error> {
650        let mut debug_messages = Vec::new();
651        if self.debug_logs {
652            debug_messages.push("\tProcess args:".to_string());
653
654            for arg in &process_info.args {
655                let arg_str = String::from_utf8_lossy(arg);
656                debug_messages.push(format!("\t\t{:?}", arg_str.as_ref()));
657            }
658
659            debug_messages.push(format!(
660                "\tProcess language: {:?}",
661                String::from_utf8_lossy(&process_info.language).as_ref()
662            ));
663        }
664
665        let mut cfg = HashMap::new();
666        // First get local configuration
667        match self.get_single_source_config(
668            local_config,
669            LibraryConfigSource::LocalStableConfig,
670            process_info,
671            &mut cfg,
672        ) {
673            LoggedResult::Ok(_, msgs) => debug_messages.extend(msgs),
674            LoggedResult::Err(e) => return LoggedResult::Err(e),
675        }
676
677        if self.debug_logs {
678            debug_messages.push("Called library_config_common_component:".to_string());
679            debug_messages.push(format!(
680                "\tsource: {:?}",
681                LibraryConfigSource::LocalStableConfig
682            ));
683            debug_messages.push(format!("\tconfigurator: {self:?}"));
684        }
685
686        // Merge with fleet config override
687        match self.get_single_source_config(
688            fleet_config,
689            LibraryConfigSource::FleetStableConfig,
690            process_info,
691            &mut cfg,
692        ) {
693            LoggedResult::Ok(_, msgs) => debug_messages.extend(msgs),
694            LoggedResult::Err(e) => return LoggedResult::Err(e),
695        }
696
697        if self.debug_logs {
698            debug_messages.push("Called library_config_common_component:".to_string());
699            debug_messages.push(format!(
700                "\tsource: {:?}",
701                LibraryConfigSource::FleetStableConfig
702            ));
703            debug_messages.push(format!("\tconfigurator: {self:?}"));
704        }
705
706        let configs = cfg
707            .into_iter()
708            .map(|(k, v)| LibraryConfig {
709                name: k,
710                value: v.value,
711                source: v.source,
712                config_id: v.config_id,
713            })
714            .collect();
715
716        LoggedResult::Ok(configs, debug_messages)
717    }
718
719    /// Get config from a stable config file and associate them with the file origin
720    ///
721    /// This is done in two steps:
722    ///     * First take the global host config
723    ///     * Merge the global config with the process specific config
724    fn get_single_source_config(
725        &self,
726        mut stable_config: StableConfig,
727        source: LibraryConfigSource,
728        process_info: &ProcessInfo,
729        cfg: &mut HashMap<String, LibraryConfigVal>,
730    ) -> LoggedResult<(), anyhow::Error> {
731        // Phase 1: take host default config
732        cfg.extend(
733            mem::take(&mut stable_config.apm_configuration_default)
734                .0
735                // TODO(paullgdc): use Box<[I]>::into_iter when we can use rust 1.80
736                .into_vec()
737                .into_iter()
738                .map(|(k, v)| {
739                    (
740                        k,
741                        LibraryConfigVal {
742                            value: v,
743                            source,
744                            config_id: stable_config.config_id.clone(),
745                        },
746                    )
747                }),
748        );
749
750        // Phase 2: process specific config
751        self.get_single_source_process_config(stable_config, source, process_info, cfg)
752    }
753
754    /// Get config from a stable config using process matching rules
755    fn get_single_source_process_config(
756        &self,
757        stable_config: StableConfig,
758        source: LibraryConfigSource,
759        process_info: &ProcessInfo,
760        library_config: &mut HashMap<String, LibraryConfigVal>,
761    ) -> LoggedResult<(), anyhow::Error> {
762        let matcher = Matcher::new(process_info, &stable_config.tags);
763        let Some(configs) = matcher.find_stable_config(&stable_config) else {
764            let messages = if self.debug_logs {
765                vec![format!("No selector matched for source {source:?}")]
766            } else {
767                Vec::new()
768            };
769            return LoggedResult::Ok((), messages);
770        };
771
772        for (name, config_val) in configs.0.iter() {
773            let value = match matcher.template_config(config_val) {
774                Ok(v) => v,
775                Err(e) => return LoggedResult::Err(e),
776            };
777            library_config.insert(
778                name.clone(),
779                LibraryConfigVal {
780                    value,
781                    source,
782                    config_id: stable_config.config_id.clone(),
783                },
784            );
785        }
786
787        let messages = if self.debug_logs {
788            vec![format!("Will apply the following configuration:\n\tsource {source:?}\n\t{library_config:?}")]
789        } else {
790            Vec::new()
791        };
792
793        LoggedResult::Ok((), messages)
794    }
795}
796
797use utils::Get;
798mod utils {
799    use std::collections::HashMap;
800
801    /// Removes leading and trailing ascci whitespaces from a byte slice
802    pub(crate) fn trim_bytes(mut b: &[u8]) -> &[u8] {
803        while b.first().map(u8::is_ascii_whitespace).unwrap_or(false) {
804            b = &b[1..];
805        }
806        while b.last().map(u8::is_ascii_whitespace).unwrap_or(false) {
807            b = &b[..b.len() - 1];
808        }
809        b
810    }
811
812    /// Helper trait so we don't have to duplicate code for
813    /// HashMap<&str, &str> and HashMap<String, String>
814    pub(crate) trait Get {
815        fn get(&self, k: &str) -> Option<&str>;
816    }
817
818    impl Get for HashMap<&str, &str> {
819        fn get(&self, k: &str) -> Option<&str> {
820            self.get(k).copied()
821        }
822    }
823
824    impl Get for HashMap<String, String> {
825        fn get(&self, k: &str) -> Option<&str> {
826            self.get(k).map(|v| v.as_str())
827        }
828    }
829}
830
831#[cfg(test)]
832mod tests {
833    use std::{collections::HashMap, io::Write, path::Path};
834
835    use super::{Configurator, LoggedResult, ProcessInfo};
836    use crate::{
837        ConfigMap, LibraryConfig, LibraryConfigSource, Matcher, Operator, Origin, Rule, Selector,
838        StableConfig,
839    };
840
841    fn test_config(local_cfg: &[u8], fleet_cfg: &[u8], expected: Vec<LibraryConfig>) {
842        let process_info: ProcessInfo = ProcessInfo {
843            args: vec![
844                b"-Djava_config_key=my_config".to_vec(),
845                b"-jar".to_vec(),
846                b"HelloWorld.jar".to_vec(),
847            ],
848            envp: vec![b"ENV=VAR".to_vec()],
849            language: b"java".to_vec(),
850        };
851        let configurator = Configurator::new(true);
852        let mut actual = configurator
853            .get_config_from_bytes(local_cfg, fleet_cfg, process_info)
854            .unwrap();
855
856        // Sort by name for determinism
857        actual.sort_by_key(|c| c.name.clone());
858        assert_eq!(actual, expected);
859    }
860
861    #[test]
862    fn test_empty_configs() {
863        test_config(b"", b"", vec![]);
864    }
865
866    #[test]
867    fn test_missing_files() {
868        let configurator = Configurator::new(true);
869        let result = configurator.get_config_from_file(
870            "/file/is/missing".as_ref(),
871            "/file/is/missing_too".as_ref(),
872            &ProcessInfo {
873                args: vec![b"-jar HelloWorld.jar".to_vec()],
874                envp: vec![b"ENV=VAR".to_vec()],
875                language: b"java".to_vec(),
876            },
877        );
878        match result {
879            LoggedResult::Ok(configs, logs) => {
880                assert_eq!(configs, vec![]);
881                assert_eq!(
882                    logs,
883                    vec![
884                        "Reading stable configuration from files:",
885                        "\tlocal: \"/file/is/missing\"",
886                        "\tfleet: \"/file/is/missing_too\"",
887                        "\tProcess args:",
888                        "\t\t\"-jar HelloWorld.jar\"",
889                        "\tProcess language: \"java\"",
890                        "No selector matched for source LocalStableConfig",
891                        "Called library_config_common_component:",
892                        "\tsource: LocalStableConfig",
893                        "\tconfigurator: Configurator { debug_logs: true }",
894                        "No selector matched for source FleetStableConfig",
895                        "Called library_config_common_component:",
896                        "\tsource: FleetStableConfig",
897                        "\tconfigurator: Configurator { debug_logs: true }"
898                    ]
899                );
900            }
901            LoggedResult::Err(_) => panic!("Expected success"),
902        }
903    }
904
905    #[test]
906    fn test_large_files() {
907        let configurator = Configurator::new(true);
908        let mut temp_local_file = tempfile::NamedTempFile::new().unwrap();
909        let mut temp_fleet_file = tempfile::NamedTempFile::new().unwrap();
910        let large_file = vec![b'a'; 1024 * 1024 * 100 + 1];
911        temp_local_file.write_all(&large_file).unwrap();
912        temp_fleet_file.write_all(&large_file).unwrap();
913        let temp_local_path = temp_local_file.into_temp_path();
914        let temp_fleet_path = temp_fleet_file.into_temp_path();
915        let result = configurator.get_config_from_file(
916            temp_local_path.to_str().unwrap().as_ref(),
917            temp_fleet_path.to_str().unwrap().as_ref(),
918            &ProcessInfo {
919                args: vec![b"-jar HelloWorld.jar".to_vec()],
920                envp: vec![b"ENV=VAR".to_vec()],
921                language: b"java".to_vec(),
922            },
923        );
924        let local_path: &Path = temp_local_path.to_str().unwrap().as_ref();
925        let fleet_path: &Path = temp_fleet_path.to_str().unwrap().as_ref();
926        match result {
927            LoggedResult::Ok(configs, logs) => {
928                assert_eq!(configs, vec![]);
929                assert_eq!(
930                    logs,
931                    vec![
932                        "Reading stable configuration from files:",
933                        format!("\tlocal: {local_path:?}").as_str(),
934                        format!("\tfleet: {fleet_path:?}").as_str(),
935                        "failed to read local config file: file is too large (> 100mb)",
936                        "failed to read fleet config file: file is too large (> 100mb)",
937                        "\tProcess args:",
938                        "\t\t\"-jar HelloWorld.jar\"",
939                        "\tProcess language: \"java\"",
940                        "No selector matched for source LocalStableConfig",
941                        "Called library_config_common_component:",
942                        "\tsource: LocalStableConfig",
943                        "\tconfigurator: Configurator { debug_logs: true }",
944                        "No selector matched for source FleetStableConfig",
945                        "Called library_config_common_component:",
946                        "\tsource: FleetStableConfig",
947                        "\tconfigurator: Configurator { debug_logs: true }"
948                    ]
949                );
950            }
951            LoggedResult::Err(_) => panic!("Expected success"),
952        }
953    }
954
955    #[test]
956    fn test_local_host_global_config() {
957        use LibraryConfigSource::*;
958        test_config(
959            b"
960apm_configuration_default:
961  DD_APM_TRACING_ENABLED: true
962  DD_RUNTIME_METRICS_ENABLED: true
963  DD_LOGS_INJECTION: true
964  DD_PROFILING_ENABLED: true
965  DD_DATA_STREAMS_ENABLED: true
966  DD_APPSEC_ENABLED: true
967  DD_IAST_ENABLED: true
968  DD_DYNAMIC_INSTRUMENTATION_ENABLED: true
969  DD_DATA_JOBS_ENABLED: true
970  DD_APPSEC_SCA_ENABLED: true
971    ",
972            b"",
973            vec![
974                LibraryConfig {
975                    name: "DD_APM_TRACING_ENABLED".to_owned(),
976                    value: "true".to_owned(),
977                    source: LocalStableConfig,
978                    config_id: None,
979                },
980                LibraryConfig {
981                    name: "DD_APPSEC_ENABLED".to_owned(),
982                    value: "true".to_owned(),
983                    source: LocalStableConfig,
984                    config_id: None,
985                },
986                LibraryConfig {
987                    name: "DD_APPSEC_SCA_ENABLED".to_owned(),
988                    value: "true".to_owned(),
989                    source: LocalStableConfig,
990                    config_id: None,
991                },
992                LibraryConfig {
993                    name: "DD_DATA_JOBS_ENABLED".to_owned(),
994                    value: "true".to_owned(),
995                    source: LocalStableConfig,
996                    config_id: None,
997                },
998                LibraryConfig {
999                    name: "DD_DATA_STREAMS_ENABLED".to_owned(),
1000                    value: "true".to_owned(),
1001                    source: LocalStableConfig,
1002                    config_id: None,
1003                },
1004                LibraryConfig {
1005                    name: "DD_DYNAMIC_INSTRUMENTATION_ENABLED".to_owned(),
1006                    value: "true".to_owned(),
1007                    source: LocalStableConfig,
1008                    config_id: None,
1009                },
1010                LibraryConfig {
1011                    name: "DD_IAST_ENABLED".to_owned(),
1012                    value: "true".to_owned(),
1013                    source: LocalStableConfig,
1014                    config_id: None,
1015                },
1016                LibraryConfig {
1017                    name: "DD_LOGS_INJECTION".to_owned(),
1018                    value: "true".to_owned(),
1019                    source: LocalStableConfig,
1020                    config_id: None,
1021                },
1022                LibraryConfig {
1023                    name: "DD_PROFILING_ENABLED".to_owned(),
1024                    value: "true".to_owned(),
1025                    source: LocalStableConfig,
1026                    config_id: None,
1027                },
1028                LibraryConfig {
1029                    name: "DD_RUNTIME_METRICS_ENABLED".to_owned(),
1030                    value: "true".to_owned(),
1031                    source: LocalStableConfig,
1032                    config_id: None,
1033                },
1034            ],
1035        );
1036    }
1037
1038    #[test]
1039    fn test_fleet_host_global_config() {
1040        use LibraryConfigSource::*;
1041        test_config(
1042            b"",
1043            b"
1044config_id: abc
1045apm_configuration_default:
1046  DD_APM_TRACING_ENABLED: true
1047  DD_RUNTIME_METRICS_ENABLED: true
1048  DD_LOGS_INJECTION: true
1049  DD_PROFILING_ENABLED: true
1050  DD_DATA_STREAMS_ENABLED: true
1051  DD_APPSEC_ENABLED: true
1052  DD_IAST_ENABLED: true
1053  DD_DYNAMIC_INSTRUMENTATION_ENABLED: true
1054  FOO_BAR: quoicoubeh
1055  DD_DATA_JOBS_ENABLED: true
1056  DD_APPSEC_SCA_ENABLED: true
1057wtf:
1058- 1
1059    ",
1060            vec![
1061                LibraryConfig {
1062                    name: "DD_APM_TRACING_ENABLED".to_owned(),
1063                    value: "true".to_owned(),
1064                    source: FleetStableConfig,
1065                    config_id: Some("abc".to_owned()),
1066                },
1067                LibraryConfig {
1068                    name: "DD_APPSEC_ENABLED".to_owned(),
1069                    value: "true".to_owned(),
1070                    source: FleetStableConfig,
1071                    config_id: Some("abc".to_owned()),
1072                },
1073                LibraryConfig {
1074                    name: "DD_APPSEC_SCA_ENABLED".to_owned(),
1075                    value: "true".to_owned(),
1076                    source: FleetStableConfig,
1077                    config_id: Some("abc".to_owned()),
1078                },
1079                LibraryConfig {
1080                    name: "DD_DATA_JOBS_ENABLED".to_owned(),
1081                    value: "true".to_owned(),
1082                    source: FleetStableConfig,
1083                    config_id: Some("abc".to_owned()),
1084                },
1085                LibraryConfig {
1086                    name: "DD_DATA_STREAMS_ENABLED".to_owned(),
1087                    value: "true".to_owned(),
1088                    source: FleetStableConfig,
1089                    config_id: Some("abc".to_owned()),
1090                },
1091                LibraryConfig {
1092                    name: "DD_DYNAMIC_INSTRUMENTATION_ENABLED".to_owned(),
1093                    value: "true".to_owned(),
1094                    source: FleetStableConfig,
1095                    config_id: Some("abc".to_owned()),
1096                },
1097                LibraryConfig {
1098                    name: "DD_IAST_ENABLED".to_owned(),
1099                    value: "true".to_owned(),
1100                    source: FleetStableConfig,
1101                    config_id: Some("abc".to_owned()),
1102                },
1103                LibraryConfig {
1104                    name: "DD_LOGS_INJECTION".to_owned(),
1105                    value: "true".to_owned(),
1106                    source: FleetStableConfig,
1107                    config_id: Some("abc".to_owned()),
1108                },
1109                LibraryConfig {
1110                    name: "DD_PROFILING_ENABLED".to_owned(),
1111                    value: "true".to_owned(),
1112                    source: FleetStableConfig,
1113                    config_id: Some("abc".to_owned()),
1114                },
1115                LibraryConfig {
1116                    name: "DD_RUNTIME_METRICS_ENABLED".to_owned(),
1117                    value: "true".to_owned(),
1118                    source: FleetStableConfig,
1119                    config_id: Some("abc".to_owned()),
1120                },
1121                LibraryConfig {
1122                    name: "FOO_BAR".to_owned(),
1123                    value: "quoicoubeh".to_owned(),
1124                    source: FleetStableConfig,
1125                    config_id: Some("abc".to_owned()),
1126                },
1127            ],
1128        );
1129    }
1130
1131    #[test]
1132    fn test_merge_local_fleet() {
1133        use LibraryConfigSource::*;
1134
1135        test_config(
1136            b"
1137apm_configuration_default:
1138  DD_APM_TRACING_ENABLED: true
1139  DD_RUNTIME_METRICS_ENABLED: true
1140  DD_PROFILING_ENABLED: true
1141        ",
1142            b"
1143config_id: abc
1144apm_configuration_default:
1145  DD_APM_TRACING_ENABLED: true
1146  DD_LOGS_INJECTION: true
1147  DD_PROFILING_ENABLED: false
1148",
1149            vec![
1150                LibraryConfig {
1151                    name: "DD_APM_TRACING_ENABLED".to_owned(),
1152                    value: "true".to_owned(),
1153                    source: FleetStableConfig,
1154                    config_id: Some("abc".to_owned()),
1155                },
1156                LibraryConfig {
1157                    name: "DD_LOGS_INJECTION".to_owned(),
1158                    value: "true".to_owned(),
1159                    source: FleetStableConfig,
1160                    config_id: Some("abc".to_owned()),
1161                },
1162                LibraryConfig {
1163                    name: "DD_PROFILING_ENABLED".to_owned(),
1164                    value: "false".to_owned(),
1165                    source: FleetStableConfig,
1166                    config_id: Some("abc".to_owned()),
1167                },
1168                LibraryConfig {
1169                    name: "DD_RUNTIME_METRICS_ENABLED".to_owned(),
1170                    value: "true".to_owned(),
1171                    source: LocalStableConfig,
1172                    config_id: None,
1173                },
1174            ],
1175        );
1176    }
1177
1178    #[test]
1179    fn test_process_config() {
1180        test_config(
1181    b"
1182config_id: abc
1183tags:
1184  cluster_name: my_cluster 
1185rules:
1186- selectors:
1187  - origin: language
1188    matches: [\"java\"]
1189    operator: equals
1190  - origin: process_arguments
1191    key: \"-Djava_config_key\"
1192    operator: exists
1193  - origin: process_arguments
1194    matches: [\"HelloWorld.jar\"]
1195    operator: equals
1196  configuration:
1197    DD_SERVICE: my_service_{{ tags[cluster_name] }}_{{ process_arguments[-Djava_config_key] }}_{{ language }}
1198    ",
1199    b"", 
1200    vec![LibraryConfig {
1201            name: "DD_SERVICE".to_string(),
1202            value: "my_service_my_cluster_my_config_java".to_string(),
1203            source: LibraryConfigSource::LocalStableConfig,
1204            config_id: Some("abc".to_string()),
1205        }],
1206        );
1207    }
1208
1209    #[test]
1210    fn test_parse_static_config() {
1211        let mut tmp = tempfile::NamedTempFile::new().unwrap();
1212        tmp.reopen()
1213            .unwrap()
1214            .write_all(
1215                b"
1216rules:
1217- selectors:
1218  - origin: language
1219    matches: [\"java\"]
1220    operator: equals
1221  configuration:
1222    DD_PROFILING_ENABLED: true
1223    DD_SERVICE: my-service
1224    # extra keys should be skipped without errors
1225    FOOBAR: maybe??
1226",
1227            )
1228            .unwrap();
1229        let configurator = Configurator::new(true);
1230        let cfg = configurator.parse_stable_config_file(tmp.as_file_mut());
1231        let config = match cfg {
1232            LoggedResult::Ok(config, _) => config,
1233            LoggedResult::Err(_) => panic!("Expected success"),
1234        };
1235        assert_eq!(
1236            config,
1237            StableConfig {
1238                config_id: None,
1239                apm_configuration_default: ConfigMap::default(),
1240                tags: HashMap::default(),
1241                rules: vec![Rule {
1242                    selectors: vec![Selector {
1243                        origin: Origin::Language,
1244                        operator: Operator::Equals {
1245                            matches: vec!["java".to_owned()]
1246                        },
1247                        key: None,
1248                    }],
1249                    configuration: ConfigMap(
1250                        vec![
1251                            ("DD_PROFILING_ENABLED".to_owned(), "true".to_owned()),
1252                            ("DD_SERVICE".to_owned(), "my-service".to_owned()),
1253                            ("FOOBAR".to_owned(), "maybe??".to_owned()),
1254                        ]
1255                        .into_boxed_slice()
1256                    ),
1257                }]
1258            }
1259        )
1260    }
1261
1262    #[test]
1263    fn test_selector_match() {
1264        let process_info = ProcessInfo {
1265            args: vec![b"-jar HelloWorld.jar".to_vec()],
1266            envp: vec![b"ENV=VAR".to_vec()],
1267            language: b"java".to_vec(),
1268        };
1269        let tags = HashMap::new();
1270        let matcher = Matcher::new(&process_info, &tags);
1271
1272        let test_cases = &[
1273            (
1274                Selector {
1275                    key: None,
1276                    origin: Origin::Language,
1277                    operator: Operator::Equals {
1278                        matches: vec!["java".to_owned()],
1279                    },
1280                },
1281                true,
1282            ),
1283            (
1284                Selector {
1285                    key: None,
1286                    origin: Origin::ProcessArguments,
1287                    operator: Operator::Equals {
1288                        matches: vec!["-jar HelloWorld.jar".to_owned()],
1289                    },
1290                },
1291                true,
1292            ),
1293            (
1294                Selector {
1295                    key: None,
1296                    origin: Origin::EnvironmentVariables,
1297                    operator: Operator::Equals {
1298                        matches: vec!["ENV=VAR".to_owned()],
1299                    },
1300                },
1301                true,
1302            ),
1303            (
1304                Selector {
1305                    key: None,
1306                    origin: Origin::Language,
1307                    operator: Operator::Equals {
1308                        matches: vec!["python".to_owned()],
1309                    },
1310                },
1311                false,
1312            ),
1313        ];
1314        for (i, (selector, matches)) in test_cases.iter().enumerate() {
1315            assert_eq!(matcher.selector_match(selector), *matches, "case {i}");
1316        }
1317    }
1318
1319    #[test]
1320    fn test_fleet_over_local() {
1321        let process_info: ProcessInfo = ProcessInfo {
1322            args: vec![
1323                b"-Djava_config_key=my_config".to_vec(),
1324                b"-jar".to_vec(),
1325                b"HelloWorld.jar".to_vec(),
1326            ],
1327            envp: vec![b"ENV=VAR".to_vec()],
1328            language: b"java".to_vec(),
1329        };
1330        let configurator = Configurator::new(true);
1331        let config = configurator
1332            .get_config_from_bytes(
1333                b"
1334config_id: abc
1335tags:
1336  cluster_name: my_cluster 
1337rules:
1338- selectors:
1339  - origin: language
1340    matches: [\"java\"]
1341    operator: equals
1342  configuration:
1343    DD_SERVICE: local
1344",
1345                b"
1346config_id: def
1347rules:
1348- selectors:
1349  - origin: language
1350    matches: [\"java\"]
1351    operator: equals
1352  configuration:
1353    DD_SERVICE: managed",
1354                process_info,
1355            )
1356            .unwrap();
1357        assert_eq!(
1358            config,
1359            vec![LibraryConfig {
1360                name: "DD_SERVICE".to_string(),
1361                value: "managed".to_string(),
1362                source: LibraryConfigSource::FleetStableConfig,
1363                config_id: Some("def".to_string()),
1364            }]
1365        );
1366    }
1367}