1pub mod format;
35pub mod helpers;
36mod masking;
37pub mod security;
38
39use std::io;
40use std::sync::OnceLock;
41
42use thiserror::Error;
43use tracing::Level;
44use tracing_subscriber::EnvFilter;
45use tracing_subscriber::Layer as _;
46use tracing_subscriber::fmt::format::FmtSpan;
47use tracing_subscriber::fmt::time::UtcTime;
48use tracing_subscriber::layer::SubscriberExt;
49use tracing_subscriber::util::SubscriberInitExt;
50
51use tracing_throttle::{Policy, TracingRateLimitLayer};
52
53pub use helpers::{log_debounced, log_sampled, log_state_change};
54pub use masking::{MaskingLayer, MaskingWriter, default_sensitive_fields, mask_sensitive_string};
55pub use security::{SecurityEvent, SecurityOutcome};
56
57static LOGGER_INIT: OnceLock<()> = OnceLock::new();
59
60#[derive(Debug, Error)]
62pub enum LoggerError {
63 #[error("logger already initialised")]
65 AlreadyInitialised,
66
67 #[error("failed to set global subscriber: {0}")]
69 SetGlobalError(String),
70
71 #[error("invalid log level: {0}")]
73 InvalidLevel(String),
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
78pub enum LogFormat {
79 Json,
81 Text,
83 #[default]
85 Auto,
86}
87
88impl LogFormat {
89 #[must_use]
91 pub fn resolve(self) -> Self {
92 match self {
93 Self::Auto => {
94 if is_terminal() && !is_no_color() {
95 Self::Text
96 } else {
97 Self::Json
98 }
99 }
100 other => other,
101 }
102 }
103}
104
105impl std::str::FromStr for LogFormat {
106 type Err = LoggerError;
107
108 fn from_str(s: &str) -> Result<Self, Self::Err> {
109 match s.to_lowercase().as_str() {
110 "json" => Ok(Self::Json),
111 "text" | "pretty" | "human" => Ok(Self::Text),
112 "auto" => Ok(Self::Auto),
113 _ => Err(LoggerError::InvalidLevel(s.to_string())),
114 }
115 }
116}
117
118#[derive(Debug, Clone)]
123pub struct ThrottleConfig {
124 pub enabled: bool,
126 pub burst: f64,
128 pub rate: f64,
130 pub max_signatures: usize,
132 pub excluded_fields: Vec<String>,
135}
136
137impl Default for ThrottleConfig {
138 fn default() -> Self {
139 Self {
140 enabled: false,
141 burst: 50.0,
142 rate: 1.0,
143 max_signatures: 10_000,
144 excluded_fields: vec![
145 "request_id".to_string(),
146 "trace_id".to_string(),
147 "span_id".to_string(),
148 ],
149 }
150 }
151}
152
153#[derive(Debug, Clone)]
155pub struct LoggerOptions {
156 pub level: Level,
158 pub format: LogFormat,
160 pub add_source: bool,
162 pub enable_masking: bool,
164 pub sensitive_fields: Vec<String>,
166 pub span_events: bool,
168 pub throttle: ThrottleConfig,
170 pub service_name: Option<String>,
173 pub service_version: Option<String>,
176}
177
178impl Default for LoggerOptions {
179 fn default() -> Self {
180 Self {
181 level: Level::INFO,
182 format: LogFormat::Auto,
183 add_source: true,
184 enable_masking: true,
185 sensitive_fields: default_sensitive_fields(),
186 span_events: false,
187 throttle: ThrottleConfig::default(),
188 service_name: None,
189 service_version: None,
190 }
191 }
192}
193
194pub fn setup(opts: LoggerOptions) -> Result<(), LoggerError> {
200 if LOGGER_INIT.get().is_some() {
201 return Err(LoggerError::AlreadyInitialised);
202 }
203
204 let format = opts.format.resolve();
205
206 let filter = EnvFilter::try_from_default_env()
208 .unwrap_or_else(|_| EnvFilter::new(opts.level.to_string()));
209
210 let timer = UtcTime::rfc_3339();
212
213 let span_events = if opts.span_events {
214 FmtSpan::NEW | FmtSpan::CLOSE
215 } else {
216 FmtSpan::NONE
217 };
218
219 let sensitive: std::collections::HashSet<String> = if opts.enable_masking {
221 opts.sensitive_fields
222 .iter()
223 .map(|s| s.to_lowercase())
224 .collect()
225 } else {
226 std::collections::HashSet::new()
227 };
228
229 let throttle_filter = build_throttle_filter(&opts.throttle);
231
232 match format {
233 LogFormat::Json => {
234 let writer = masking::make_masking_writer(
235 sensitive,
236 true,
237 opts.service_name.clone(),
238 opts.service_version.clone(),
239 );
240 let layer = tracing_subscriber::fmt::layer()
241 .json()
242 .with_timer(timer)
243 .with_file(opts.add_source)
244 .with_line_number(opts.add_source)
245 .with_target(true)
246 .with_span_events(span_events)
247 .with_writer(writer);
248
249 if let Some(throttle) = throttle_filter {
250 tracing_subscriber::registry()
251 .with(filter)
252 .with(layer.with_filter(throttle))
253 .try_init()
254 .map_err(|e| LoggerError::SetGlobalError(e.to_string()))?;
255 } else {
256 tracing_subscriber::registry()
257 .with(filter)
258 .with(layer)
259 .try_init()
260 .map_err(|e| LoggerError::SetGlobalError(e.to_string()))?;
261 }
262 }
263 LogFormat::Text => {
264 let writer = masking::make_masking_writer(sensitive, false, None, None);
265 let ansi = !is_no_color();
266 let formatter = format::ColouredFormatter::new(ansi)
267 .with_file(opts.add_source)
268 .with_line_number(opts.add_source);
269 let layer = tracing_subscriber::fmt::layer()
270 .with_ansi(ansi)
271 .with_span_events(span_events)
272 .event_format(formatter)
273 .with_writer(writer);
274
275 if let Some(throttle) = throttle_filter {
276 tracing_subscriber::registry()
277 .with(filter)
278 .with(layer.with_filter(throttle))
279 .try_init()
280 .map_err(|e| LoggerError::SetGlobalError(e.to_string()))?;
281 } else {
282 tracing_subscriber::registry()
283 .with(filter)
284 .with(layer)
285 .try_init()
286 .map_err(|e| LoggerError::SetGlobalError(e.to_string()))?;
287 }
288 }
289 LogFormat::Auto => unreachable!("Auto should be resolved"),
290 }
291
292 let _ = LOGGER_INIT.set(());
293 Ok(())
294}
295
296pub fn setup_default() -> Result<(), LoggerError> {
310 let level = std::env::var("LOG_LEVEL")
311 .or_else(|_| std::env::var("RUST_LOG"))
312 .ok()
313 .and_then(|s| s.parse().ok())
314 .unwrap_or(Level::INFO);
315
316 let format = std::env::var("LOG_FORMAT")
317 .ok()
318 .and_then(|s| s.parse().ok())
319 .unwrap_or(LogFormat::Auto);
320
321 let throttle_enabled = std::env::var("LOG_THROTTLE_ENABLED")
322 .ok()
323 .is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("true"));
324
325 let throttle_burst = std::env::var("LOG_THROTTLE_BURST")
326 .ok()
327 .and_then(|v| v.parse().ok())
328 .unwrap_or(50.0);
329
330 let throttle_rate = std::env::var("LOG_THROTTLE_RATE")
331 .ok()
332 .and_then(|v| v.parse().ok())
333 .unwrap_or(1.0);
334
335 let service_name = std::env::var("SERVICE_NAME").ok();
336 let service_version = std::env::var("SERVICE_VERSION").ok();
337
338 setup(LoggerOptions {
339 level,
340 format,
341 throttle: ThrottleConfig {
342 enabled: throttle_enabled,
343 burst: throttle_burst,
344 rate: throttle_rate,
345 ..Default::default()
346 },
347 service_name,
348 service_version,
349 ..Default::default()
350 })
351}
352
353fn build_throttle_filter(config: &ThrottleConfig) -> Option<TracingRateLimitLayer> {
355 if !config.enabled {
356 return None;
357 }
358
359 let policy = Policy::token_bucket(config.burst, config.rate)
360 .unwrap_or_else(|_| Policy::token_bucket(50.0, 1.0).expect("default policy is valid"));
361
362 let mut builder = TracingRateLimitLayer::builder()
363 .with_policy(policy)
364 .with_max_signatures(config.max_signatures);
365
366 if !config.excluded_fields.is_empty() {
367 builder = builder.with_excluded_fields(config.excluded_fields.clone());
368 }
369
370 match builder.build() {
371 Ok(layer) => Some(layer),
372 Err(e) => {
373 eprintln!("Failed to build log throttle layer: {e}");
374 None
375 }
376 }
377}
378
379fn is_terminal() -> bool {
381 use std::io::IsTerminal;
382 io::stderr().is_terminal()
383}
384
385fn is_no_color() -> bool {
387 std::env::var("NO_COLOR").is_ok()
388}
389
390#[cfg(test)]
391mod tests {
392 use super::*;
393
394 #[test]
395 fn test_log_format_from_str() {
396 assert_eq!("json".parse::<LogFormat>().unwrap(), LogFormat::Json);
397 assert_eq!("text".parse::<LogFormat>().unwrap(), LogFormat::Text);
398 assert_eq!("pretty".parse::<LogFormat>().unwrap(), LogFormat::Text);
399 assert_eq!("auto".parse::<LogFormat>().unwrap(), LogFormat::Auto);
400 }
401
402 #[test]
403 fn test_log_format_resolve() {
404 assert_eq!(LogFormat::Json.resolve(), LogFormat::Json);
406 assert_eq!(LogFormat::Text.resolve(), LogFormat::Text);
407
408 let resolved = LogFormat::Auto.resolve();
410 assert!(matches!(resolved, LogFormat::Json | LogFormat::Text));
411 }
412
413 #[test]
414 fn test_logger_options_default() {
415 let opts = LoggerOptions::default();
416 assert_eq!(opts.level, Level::INFO);
417 assert_eq!(opts.format, LogFormat::Auto);
418 assert!(opts.add_source);
419 assert!(opts.enable_masking);
420 assert!(!opts.sensitive_fields.is_empty());
421 }
422
423 #[test]
424 fn test_is_no_color() {
425 temp_env::with_var("NO_COLOR", None::<&str>, || assert!(!is_no_color()));
426 temp_env::with_var("NO_COLOR", Some("1"), || assert!(is_no_color()));
427 }
428}