1pub 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
13struct 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 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 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 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 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#[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 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 }
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 #[serde(default)]
350 config_id: Option<String>,
351 #[serde(default)]
352 apm_configuration_default: ConfigMap,
353
354 #[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 }
385}
386
387#[derive(Debug, PartialEq, Eq)]
388pub 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)]
398struct 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 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 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 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 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 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 cfg.extend(
733 mem::take(&mut stable_config.apm_configuration_default)
734 .0
735 .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 self.get_single_source_process_config(stable_config, source, process_info, cfg)
752 }
753
754 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 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 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 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}