Skip to main content

saluki_components/config/
agent_env.rs

1//! The agent environment of the trace pipeline.
2
3use saluki_common::strings::StringBuilder;
4use stringtheory::MetaString;
5use tracing::{debug, warn};
6
7use crate::common::otlp::traces::normalize::normalize_tag_value_into_unchecked;
8
9/// Environment used when no source supplies a nonempty one.
10const FALLBACK_ENV: &str = "none";
11
12/// Prefix of a global tag that supplies the environment. Case-sensitive.
13const ENV_TAG_PREFIX: &str = "env:";
14
15/// The resolved, normalized environment of the trace pipeline.
16///
17/// Sampling and stats aggregation use it for traces that carry no environment of their own, and the trace and stats
18/// encoders send it as the agent environment of each payload. The trace pipeline resolves it once, when it is built.
19#[derive(Clone, Debug, Eq, PartialEq)]
20pub struct AgentEnv(MetaString);
21
22impl AgentEnv {
23    /// Resolves the agent environment from its configured sources.
24    ///
25    /// `apm_env` and `env` are `None` when not configured. The first configured source is selected, even when its value
26    /// is empty:
27    ///
28    /// 1. `apm_env` (`apm_config.env`)
29    /// 2. `env`
30    /// 3. the value of the first `env:` entry in `tags`, then in `extra_tags`
31    ///
32    /// When nothing is selected, or the selected value is empty, the environment is `none`. The result is then
33    /// normalized as a tag value, with no further fallback: a value such as `!!!` resolves to an empty environment, and
34    /// logs a warning.
35    pub fn resolve(apm_env: Option<&str>, env: Option<&str>, tags: &[String], extra_tags: &[String]) -> Self {
36        let (source, configured) = apm_env
37            .map(|value| ("apm_config.env", value))
38            .or_else(|| env.map(|value| ("env", value)))
39            .or_else(|| {
40                let tags = tags.iter().map(|tag| ("tags", tag));
41                let extra_tags = extra_tags.iter().map(|tag| ("extra_tags", tag));
42                tags.chain(extra_tags)
43                    .find_map(|(source, tag)| tag.strip_prefix(ENV_TAG_PREFIX).map(|value| (source, value)))
44            })
45            .unwrap_or(("default", ""));
46
47        let selected = if configured.is_empty() {
48            FALLBACK_ENV
49        } else {
50            configured
51        };
52        let mut normalized = StringBuilder::new();
53        normalize_tag_value_into_unchecked(selected, &mut normalized);
54        if normalized.as_str().is_empty() {
55            // Stats aggregation then drops traces without their own environment, logging an error per span. Explain why
56            // once, at startup.
57            warn!(
58                source,
59                configured,
60                "The trace agent environment normalizes to an empty value. Set `apm_config.env`, `env`, or the \
61                 `env:` global tag to a valid tag value, such as `prod`."
62            );
63        } else {
64            debug!(
65                source,
66                configured,
67                env = normalized.as_str(),
68                "Resolved the trace agent environment."
69            );
70        }
71
72        Self(MetaString::from(normalized.as_str()))
73    }
74
75    /// Returns the environment as a string slice.
76    pub fn as_str(&self) -> &str {
77        &self.0
78    }
79}
80
81impl From<AgentEnv> for MetaString {
82    fn from(env: AgentEnv) -> Self {
83        env.0
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use super::*;
90
91    #[track_caller]
92    fn assert_resolves(apm_env: Option<&str>, env: Option<&str>, tags: &[&str], extra_tags: &[&str], expected: &str) {
93        let tags: Vec<String> = tags.iter().map(|tag| tag.to_string()).collect();
94        let extra_tags: Vec<String> = extra_tags.iter().map(|tag| tag.to_string()).collect();
95        assert_eq!(AgentEnv::resolve(apm_env, env, &tags, &extra_tags).as_str(), expected);
96    }
97
98    // Expected values follow the Datadog Agent's `GetTraceAgentDefaultEnv` and the normalization that
99    // follows it:
100    // https://github.com/DataDog/datadog-agent/blob/17ecddf4e3e83ccbb0e68aeb99461d1a1d902927/pkg/config/utils/trace.go#L16-L35
101    // https://github.com/DataDog/datadog-agent/blob/17ecddf4e3e83ccbb0e68aeb99461d1a1d902927/comp/trace/config/impl/setup.go#L291-L299
102    #[test]
103    fn resolve_follows_agent_precedence() {
104        assert_resolves(None, None, &[], &[], "none");
105
106        // Precedence: `apm_config.env`, then `env`, then the first `env:` tag in `tags`, then in `extra_tags`.
107        assert_resolves(Some("Staging"), Some("prod"), &["env:t"], &[], "staging");
108        assert_resolves(None, Some("Prod"), &["env:t"], &[], "prod");
109        assert_resolves(None, None, &["a:b", "env:first", "env:second"], &["env:x"], "first");
110        assert_resolves(None, None, &["a:b"], &["env:x"], "x");
111        assert_resolves(None, None, &["ENV:up", "env:low"], &[], "low");
112
113        // A configured empty value is still selected, then replaced by `none`.
114        assert_resolves(None, None, &["env:", "env:x"], &[], "none");
115        assert_resolves(Some(""), Some("prod"), &[], &[], "none");
116        assert_resolves(None, Some(""), &["env:t"], &[], "none");
117
118        // `none` is not applied again after normalization.
119        assert_resolves(Some("!!!"), None, &[], &[], "");
120
121        // Empty `DD_APM_ENV` and `DD_ENV` load as not configured; `DD_TAGS="a:b env:tagged"` loads as two tags.
122        assert_resolves(None, None, &["a:b", "env:tagged"], &[], "tagged");
123
124        // A streamed default `apm_config.env` loads as not configured.
125        assert_resolves(None, Some("prod"), &[], &[], "prod");
126    }
127}