Skip to main content

telemetry_init/
config.rs

1//! [`TelemetryConfig`] — the builder that captures every estate default.
2
3/// Default series budget handed to the metrics-kit registry.
4#[cfg(feature = "metrics")]
5pub const DEFAULT_METRICS_BUDGET: usize = 8192;
6
7/// Default service version recorded when none is supplied.
8pub const DEFAULT_SERVICE_VERSION: &str = "0.0.0";
9
10/// Default `env-filter` directive when neither `RUST_LOG` nor an explicit
11/// `log_level` override applies.
12pub const DEFAULT_LOG_LEVEL: &str = "info";
13
14/// Default trace sample rate: record every span.
15pub const DEFAULT_SAMPLE_RATE: f32 = 1.0;
16
17/// Log output format for the subscriber's `fmt` layer.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19#[non_exhaustive]
20pub enum LogFormat {
21    /// Single-line JSON objects — the estate default for services, so
22    /// collectors can parse without regex. Requires the `json` feature
23    /// (enabled by default); requesting it without the feature is a
24    /// configuration error, never a silent fallback.
25    #[default]
26    Json,
27    /// Human-oriented multi-line format with ANSI color when stderr is a
28    /// TTY — for local development.
29    Pretty,
30}
31
32/// Bootstrap configuration for the estate telemetry stack.
33///
34/// Built with [`TelemetryConfig::new`] and consumed by
35/// [`Telemetry::init`](crate::Telemetry::init). Every setter is
36/// chainable; unset fields keep the documented defaults.
37#[derive(Debug, Clone)]
38pub struct TelemetryConfig {
39    #[cfg_attr(not(feature = "otlp"), allow(dead_code))]
40    pub(crate) service_name: String,
41    pub(crate) service_version: String,
42    pub(crate) log_level: String,
43    pub(crate) log_format: LogFormat,
44    #[cfg(feature = "metrics")]
45    pub(crate) metrics_budget: usize,
46    #[cfg(feature = "otlp")]
47    pub(crate) otlp_endpoint: Option<String>,
48    pub(crate) sample_rate: f32,
49}
50
51impl TelemetryConfig {
52    /// Create a config for `service_name` with estate defaults:
53    /// version `"0.0.0"`, `env-filter` `"info"` (overridden by `RUST_LOG`),
54    /// JSON logs, a 8192-series metrics budget, no OTLP endpoint (no trace
55    /// export), and a 1.0 sample rate.
56    #[must_use]
57    pub fn new(service_name: impl Into<String>) -> Self {
58        Self {
59            service_name: service_name.into(),
60            service_version: DEFAULT_SERVICE_VERSION.to_owned(),
61            log_level: DEFAULT_LOG_LEVEL.to_owned(),
62            log_format: LogFormat::default(),
63            #[cfg(feature = "metrics")]
64            metrics_budget: DEFAULT_METRICS_BUDGET,
65            #[cfg(feature = "otlp")]
66            otlp_endpoint: None,
67            sample_rate: DEFAULT_SAMPLE_RATE,
68        }
69    }
70
71    /// Set the service version attached to OTLP resource attributes.
72    #[must_use]
73    pub fn version(mut self, version: impl Into<String>) -> Self {
74        self.service_version = version.into();
75        self
76    }
77
78    /// Set the log output format.
79    #[must_use]
80    pub fn log_format(mut self, format: LogFormat) -> Self {
81        self.log_format = format;
82        self
83    }
84
85    /// Set the `env-filter` directive string (e.g. `"info"`,
86    /// `"debug,hyper=warn"`).
87    ///
88    /// `RUST_LOG`, when set and non-empty, always wins over this value.
89    #[must_use]
90    pub fn log_level(mut self, level: impl Into<String>) -> Self {
91        self.log_level = level.into();
92        self
93    }
94
95    /// Set the metrics-kit series budget (cardinality guard).
96    #[cfg(feature = "metrics")]
97    #[must_use]
98    pub fn metrics_budget(mut self, budget: usize) -> Self {
99        self.metrics_budget = budget;
100        self
101    }
102
103    /// Set the OTLP endpoint (e.g. `"http://localhost:4317"`). Requires the
104    /// `otlp` feature. With no endpoint configured, spans are never
105    /// exported.
106    #[cfg(feature = "otlp")]
107    #[must_use]
108    pub fn otlp_endpoint(mut self, endpoint: impl Into<String>) -> Self {
109        self.otlp_endpoint = Some(endpoint.into());
110        self
111    }
112
113    /// Set the trace sample rate in `0.0..=1.0`; values outside the range
114    /// are clamped. A `NaN` falls back to the default `1.0` rather than
115    /// silently dropping every span.
116    #[must_use]
117    pub fn sample_rate(mut self, rate: f32) -> Self {
118        self.sample_rate = if rate.is_nan() {
119            DEFAULT_SAMPLE_RATE
120        } else {
121            rate.clamp(0.0, 1.0)
122        };
123        self
124    }
125}
126
127#[cfg(test)]
128mod tests {
129    // Exact binary fractions (0.0, 0.5, 1.0, …) are compared — equality
130    // is exact for these values, so float_cmp is a false positive here.
131    #![allow(clippy::unwrap_used, clippy::expect_used, clippy::float_cmp)]
132    use super::*;
133    use crate::error::TelemetryError;
134
135    #[test]
136    fn defaults_match_estate_documentation() {
137        let cfg = TelemetryConfig::new("payments-api");
138        assert_eq!(cfg.service_name, "payments-api");
139        assert_eq!(cfg.service_version, DEFAULT_SERVICE_VERSION);
140        assert_eq!(cfg.log_level, DEFAULT_LOG_LEVEL);
141        assert_eq!(cfg.log_format, LogFormat::Json);
142        assert_eq!(cfg.sample_rate, 1.0);
143        #[cfg(feature = "metrics")]
144        assert_eq!(cfg.metrics_budget, DEFAULT_METRICS_BUDGET);
145        #[cfg(feature = "otlp")]
146        assert!(cfg.otlp_endpoint.is_none());
147    }
148
149    #[test]
150    fn builder_chain_sets_every_field() {
151        let cfg = TelemetryConfig::new("svc")
152            .version("1.2.3")
153            .log_level("debug,hyper=warn")
154            .log_format(LogFormat::Pretty)
155            .sample_rate(0.25);
156        assert_eq!(cfg.service_version, "1.2.3");
157        assert_eq!(cfg.log_level, "debug,hyper=warn");
158        assert_eq!(cfg.log_format, LogFormat::Pretty);
159        assert_eq!(cfg.sample_rate, 0.25);
160
161        #[cfg(feature = "metrics")]
162        let cfg = cfg.metrics_budget(1024);
163        #[cfg(feature = "otlp")]
164        let cfg = cfg.otlp_endpoint("http://localhost:4317");
165        #[cfg(feature = "metrics")]
166        assert_eq!(cfg.metrics_budget, 1024);
167        #[cfg(feature = "otlp")]
168        assert_eq!(cfg.otlp_endpoint.as_deref(), Some("http://localhost:4317"));
169        let _ = cfg;
170    }
171
172    #[test]
173    fn setters_overwrite_previous_values() {
174        let cfg = TelemetryConfig::new("svc")
175            .log_level("info")
176            .log_level("warn")
177            .version("a")
178            .version("b")
179            .log_format(LogFormat::Pretty)
180            .log_format(LogFormat::Json);
181        assert_eq!(cfg.log_level, "warn");
182        assert_eq!(cfg.service_version, "b");
183        assert_eq!(cfg.log_format, LogFormat::Json);
184    }
185
186    #[test]
187    fn sample_rate_clamps_into_unit_range() {
188        assert_eq!(TelemetryConfig::new("s").sample_rate(2.0).sample_rate, 1.0);
189        assert_eq!(TelemetryConfig::new("s").sample_rate(-0.5).sample_rate, 0.0);
190        assert_eq!(
191            TelemetryConfig::new("s").sample_rate(0.125).sample_rate,
192            0.125
193        );
194        assert_eq!(TelemetryConfig::new("s").sample_rate(0.0).sample_rate, 0.0);
195        assert_eq!(TelemetryConfig::new("s").sample_rate(1.0).sample_rate, 1.0);
196    }
197
198    #[test]
199    fn sample_rate_nan_falls_back_to_full_sampling() {
200        assert_eq!(
201            TelemetryConfig::new("s").sample_rate(f32::NAN).sample_rate,
202            DEFAULT_SAMPLE_RATE
203        );
204    }
205
206    #[test]
207    fn log_format_traits() {
208        assert_eq!(LogFormat::default(), LogFormat::Json);
209        assert_ne!(LogFormat::Json, LogFormat::Pretty);
210        assert_eq!(format!("{:?}", LogFormat::Pretty), "Pretty");
211        let copied = LogFormat::Json;
212        assert_eq!(copied, LogFormat::Json);
213    }
214
215    #[test]
216    fn config_is_clone_and_debug() {
217        let cfg = TelemetryConfig::new("svc").version("9.9.9");
218        let cloned = cfg.clone();
219        assert_eq!(cloned.service_name, "svc");
220        let debug = format!("{cfg:?}");
221        assert!(debug.contains("TelemetryConfig"));
222        assert!(debug.contains("svc"));
223    }
224
225    #[test]
226    fn empty_service_name_is_accepted() {
227        // The service name flows into OTLP resource attributes; the
228        // collector, not this crate, owns name validation.
229        let cfg = TelemetryConfig::new("");
230        assert_eq!(cfg.service_name, "");
231    }
232
233    #[test]
234    fn unparseable_log_level_is_a_typed_error() {
235        assert!(matches!(
236            crate::telemetry::build_filter("a=b=c"),
237            Err(TelemetryError::InitFailed(_))
238        ));
239    }
240
241    #[test]
242    fn composite_directives_parse() {
243        crate::telemetry::build_filter("info,hyper=warn").unwrap();
244    }
245}