1#[cfg(feature = "metrics")]
5pub const DEFAULT_METRICS_BUDGET: usize = 8192;
6
7pub const DEFAULT_SERVICE_VERSION: &str = "0.0.0";
9
10pub const DEFAULT_LOG_LEVEL: &str = "info";
13
14pub const DEFAULT_SAMPLE_RATE: f32 = 1.0;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
19#[non_exhaustive]
20pub enum LogFormat {
21 #[default]
26 Json,
27 Pretty,
30}
31
32#[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 #[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 #[must_use]
73 pub fn version(mut self, version: impl Into<String>) -> Self {
74 self.service_version = version.into();
75 self
76 }
77
78 #[must_use]
80 pub fn log_format(mut self, format: LogFormat) -> Self {
81 self.log_format = format;
82 self
83 }
84
85 #[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 #[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 #[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 #[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 #![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 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}