Skip to main content

toolkit/telemetry/
config.rs

1//! OpenTelemetry tracing and metrics configuration types
2//!
3//! These types define the configuration structure for OpenTelemetry distributed
4//! tracing and metrics.
5
6use serde::{Deserialize, Serialize};
7use std::collections::{BTreeMap, HashMap};
8
9/// Top-level OpenTelemetry configuration grouping resource identity,
10/// a shared default exporter, tracing settings and metrics settings.
11#[derive(Debug, Clone, Deserialize, Serialize, Default)]
12#[serde(deny_unknown_fields)]
13pub struct OpenTelemetryConfig {
14    #[serde(default)]
15    pub resource: OpenTelemetryResource,
16    /// Default exporter shared by tracing and metrics. Per-signal `exporter`
17    /// fields override this when present.
18    pub exporter: Option<Exporter>,
19    #[serde(default)]
20    pub tracing: TracingConfig,
21    #[serde(default)]
22    pub metrics: MetricsConfig,
23}
24
25impl OpenTelemetryConfig {
26    /// Resolve the effective exporter for tracing (per-signal or shared fallback).
27    #[must_use]
28    pub fn tracing_exporter(&self) -> Option<&Exporter> {
29        self.tracing.exporter.as_ref().or(self.exporter.as_ref())
30    }
31    /// Resolve the effective exporter for metrics (per-signal or shared fallback).
32    #[must_use]
33    pub fn metrics_exporter(&self) -> Option<&Exporter> {
34        self.metrics.exporter.as_ref().or(self.exporter.as_ref())
35    }
36    /// Whether JSON log records should carry top-level `trace_id` / `span_id`.
37    ///
38    /// On by default: a log line an operator cannot join to the `trace_id` an
39    /// error response handed the caller is a broken incident trail, and that
40    /// join is the whole point of the correlation. The cost is a span-context
41    /// lookup per event; set this to `false` explicitly to opt out.
42    #[must_use]
43    pub fn inject_trace_ids_into_logs(&self) -> bool {
44        self.tracing
45            .logs_correlation
46            .as_ref()
47            .and_then(|c| c.inject_trace_ids_into_logs)
48            .unwrap_or(true)
49    }
50}
51
52/// OpenTelemetry resource identity — attached to all traces and metrics.
53#[derive(Debug, Clone, Deserialize, Serialize)]
54#[serde(deny_unknown_fields)]
55pub struct OpenTelemetryResource {
56    /// Logical service name.
57    #[serde(default = "default_service_name")]
58    pub service_name: String,
59    /// Extra resource attributes added to every span and metric data point.
60    #[serde(default)]
61    pub attributes: BTreeMap<String, String>,
62}
63
64/// Return the default OpenTelemetry service name used when none is configured.
65fn default_service_name() -> String {
66    "cf-gears".to_owned()
67}
68
69impl Default for OpenTelemetryResource {
70    fn default() -> Self {
71        Self {
72            service_name: default_service_name(),
73            attributes: BTreeMap::default(),
74        }
75    }
76}
77
78/// Tracing configuration for OpenTelemetry distributed tracing
79#[derive(Debug, Clone, Deserialize, Serialize, Default)]
80#[serde(deny_unknown_fields)]
81pub struct TracingConfig {
82    pub enabled: bool,
83    /// Per-signal exporter override. When `None`, the shared
84    /// [`OpenTelemetryConfig::exporter`] is used instead.
85    pub exporter: Option<Exporter>,
86    pub sampler: Option<Sampler>,
87    pub logs_correlation: Option<LogsCorrelation>,
88}
89
90/// Metrics configuration for OpenTelemetry metrics collection
91#[derive(Debug, Clone, Deserialize, Serialize, Default)]
92#[serde(deny_unknown_fields)]
93pub struct MetricsConfig {
94    pub enabled: bool,
95    /// Per-signal exporter override. When `None`, the shared
96    /// [`OpenTelemetryConfig::exporter`] is used instead.
97    pub exporter: Option<Exporter>,
98    /// Maximum number of distinct attribute combinations per instrument.
99    /// When the limit is reached, new combinations are folded into an
100    /// overflow data point.  `None` means the SDK default is used.
101    #[serde(skip_serializing_if = "Option::is_none")]
102    pub cardinality_limit: Option<usize>,
103}
104
105#[derive(Debug, Default, Clone, Deserialize, Serialize, PartialEq, Eq, Copy)]
106#[serde(rename_all = "snake_case")]
107pub enum ExporterKind {
108    #[default]
109    OtlpGrpc,
110    OtlpHttp,
111}
112
113#[derive(Debug, Clone, Deserialize, Serialize)]
114pub struct Exporter {
115    /// Defaults to `otlp_grpc`, matching `extract_exporter_config`. Without a
116    /// default, overriding only the endpoint (e.g. via
117    /// `APP__OPENTELEMETRY__EXPORTER__ENDPOINT`) would fail to load because
118    /// `kind` would be missing from the partially-built map.
119    #[serde(default)]
120    pub kind: ExporterKind,
121    pub endpoint: Option<String>,
122    pub headers: Option<HashMap<String, String>>,
123    pub timeout_ms: Option<u64>,
124}
125
126#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)]
127#[serde(rename_all = "snake_case")]
128pub enum Sampler {
129    ParentBasedAlwaysOn {},
130    ParentBasedRatio {
131        #[serde(skip_serializing_if = "Option::is_none")]
132        ratio: Option<f64>,
133    },
134    AlwaysOn {},
135    AlwaysOff {},
136}
137
138#[derive(Debug, Clone, Deserialize, Serialize)]
139pub struct LogsCorrelation {
140    pub inject_trace_ids_into_logs: Option<bool>,
141}
142
143#[cfg(test)]
144#[cfg_attr(coverage_nightly, coverage(off))]
145mod tests {
146    use super::{LogsCorrelation, OpenTelemetryConfig, TracingConfig};
147
148    /// A config whose `logs_correlation` section is exactly `lc`.
149    fn with_logs_correlation(lc: Option<LogsCorrelation>) -> OpenTelemetryConfig {
150        OpenTelemetryConfig {
151            tracing: TracingConfig {
152                logs_correlation: lc,
153                ..TracingConfig::default()
154            },
155            ..OpenTelemetryConfig::default()
156        }
157    }
158
159    fn flag(inject: Option<bool>) -> LogsCorrelation {
160        LogsCorrelation {
161            inject_trace_ids_into_logs: inject,
162        }
163    }
164
165    /// The default is ON: a default config, an absent `logs_correlation`
166    /// section, and a section that omits the flag all resolve to `true`; only an
167    /// explicit `false` opts out. No prior test pinned this.
168    #[test]
169    fn inject_trace_ids_default_is_on_unless_explicitly_disabled() {
170        assert!(
171            OpenTelemetryConfig::default().inject_trace_ids_into_logs(),
172            "the default config must splice ids into logs"
173        );
174        assert!(
175            with_logs_correlation(None).inject_trace_ids_into_logs(),
176            "an absent logs_correlation section must default ON"
177        );
178        assert!(
179            with_logs_correlation(Some(flag(None))).inject_trace_ids_into_logs(),
180            "a logs_correlation section without the flag must default ON"
181        );
182        assert!(
183            with_logs_correlation(Some(flag(Some(true)))).inject_trace_ids_into_logs(),
184            "an explicit true stays ON"
185        );
186        assert!(
187            !with_logs_correlation(Some(flag(Some(false)))).inject_trace_ids_into_logs(),
188            "an explicit false is the only way to opt out"
189        );
190    }
191
192    /// The bootstrap layers resolve the flag over an `Option<&OpenTelemetryConfig>`,
193    /// absent when a gear ships no `[opentelemetry]` section. That absent case must
194    /// land ON, matching the in-process default — `is_none_or`, not `is_some_and`.
195    #[test]
196    fn absent_opentelemetry_section_resolves_the_splice_on() {
197        let absent: Option<&OpenTelemetryConfig> = None;
198        assert!(
199            absent.is_none_or(OpenTelemetryConfig::inject_trace_ids_into_logs),
200            "no [opentelemetry] section must resolve the splice ON"
201        );
202
203        let disabled = with_logs_correlation(Some(flag(Some(false))));
204        assert!(
205            !Some(&disabled).is_none_or(OpenTelemetryConfig::inject_trace_ids_into_logs),
206            "an explicit false must still opt out when a section is present"
207        );
208    }
209}