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 #[error("invalid log format: {0}")]
77 InvalidFormat(String),
78}
79
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
82pub enum LogFormat {
83 Json,
85 Text,
87 #[default]
89 Auto,
90}
91
92impl LogFormat {
93 #[must_use]
95 pub fn resolve(self) -> Self {
96 match self {
97 Self::Auto => {
98 if is_terminal() && !is_no_color() {
99 Self::Text
100 } else {
101 Self::Json
102 }
103 }
104 other => other,
105 }
106 }
107}
108
109impl std::str::FromStr for LogFormat {
110 type Err = LoggerError;
111
112 fn from_str(s: &str) -> Result<Self, Self::Err> {
113 match s.to_lowercase().as_str() {
114 "json" => Ok(Self::Json),
115 "text" | "pretty" | "human" => Ok(Self::Text),
116 "auto" => Ok(Self::Auto),
117 _ => Err(LoggerError::InvalidFormat(s.to_string())),
118 }
119 }
120}
121
122#[derive(Debug, Clone)]
127pub struct ThrottleConfig {
128 pub enabled: bool,
130 pub burst: f64,
132 pub rate: f64,
134 pub max_signatures: usize,
136 pub excluded_fields: Vec<String>,
139}
140
141impl Default for ThrottleConfig {
142 fn default() -> Self {
143 Self {
144 enabled: false,
145 burst: 50.0,
146 rate: 1.0,
147 max_signatures: 10_000,
148 excluded_fields: vec![
149 "request_id".to_string(),
150 "trace_id".to_string(),
151 "span_id".to_string(),
152 ],
153 }
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct LoggerOptions {
160 pub level: Level,
162 pub format: LogFormat,
164 pub add_source: bool,
166 pub enable_masking: bool,
168 pub sensitive_fields: Vec<String>,
170 pub span_events: bool,
172 pub throttle: ThrottleConfig,
174 pub service_name: Option<String>,
177 pub service_version: Option<String>,
180}
181
182impl Default for LoggerOptions {
183 fn default() -> Self {
184 Self {
185 level: Level::INFO,
186 format: LogFormat::Auto,
187 add_source: true,
188 enable_masking: true,
189 sensitive_fields: default_sensitive_fields(),
190 span_events: false,
191 throttle: ThrottleConfig::default(),
192 service_name: None,
193 service_version: None,
194 }
195 }
196}
197
198pub fn setup(opts: LoggerOptions) -> Result<(), LoggerError> {
204 if LOGGER_INIT.get().is_some() {
205 return Err(LoggerError::AlreadyInitialised);
206 }
207
208 let format = opts.format.resolve();
209
210 let filter = EnvFilter::try_from_default_env()
212 .unwrap_or_else(|_| EnvFilter::new(opts.level.to_string()));
213
214 let timer = UtcTime::rfc_3339();
216
217 let span_events = if opts.span_events {
218 FmtSpan::NEW | FmtSpan::CLOSE
219 } else {
220 FmtSpan::NONE
221 };
222
223 let sensitive: std::collections::HashSet<String> = if opts.enable_masking {
225 opts.sensitive_fields
226 .iter()
227 .map(|s| s.to_lowercase())
228 .collect()
229 } else {
230 std::collections::HashSet::new()
231 };
232
233 let throttle_filter = build_throttle_filter(&opts.throttle);
235
236 match format {
237 LogFormat::Json => {
238 let writer = masking::make_masking_writer(
239 sensitive,
240 true,
241 opts.service_name.clone(),
242 opts.service_version.clone(),
243 );
244 let layer = tracing_subscriber::fmt::layer()
245 .json()
246 .with_timer(timer)
247 .with_file(opts.add_source)
248 .with_line_number(opts.add_source)
249 .with_target(true)
250 .with_span_events(span_events)
251 .with_writer(writer);
252
253 if let Some(throttle) = throttle_filter {
254 tracing_subscriber::registry()
255 .with(filter)
256 .with(layer.with_filter(throttle))
257 .try_init()
258 .map_err(|e| LoggerError::SetGlobalError(e.to_string()))?;
259 } else {
260 tracing_subscriber::registry()
261 .with(filter)
262 .with(layer)
263 .try_init()
264 .map_err(|e| LoggerError::SetGlobalError(e.to_string()))?;
265 }
266 }
267 LogFormat::Text => {
268 let writer = masking::make_masking_writer(sensitive, false, None, None);
269 let ansi = !is_no_color();
270 let formatter = format::ColouredFormatter::new(ansi)
271 .with_file(opts.add_source)
272 .with_line_number(opts.add_source);
273 let layer = tracing_subscriber::fmt::layer()
274 .with_ansi(ansi)
275 .with_span_events(span_events)
276 .event_format(formatter)
277 .with_writer(writer);
278
279 if let Some(throttle) = throttle_filter {
280 tracing_subscriber::registry()
281 .with(filter)
282 .with(layer.with_filter(throttle))
283 .try_init()
284 .map_err(|e| LoggerError::SetGlobalError(e.to_string()))?;
285 } else {
286 tracing_subscriber::registry()
287 .with(filter)
288 .with(layer)
289 .try_init()
290 .map_err(|e| LoggerError::SetGlobalError(e.to_string()))?;
291 }
292 }
293 LogFormat::Auto => unreachable!("Auto should be resolved"),
294 }
295
296 let _ = LOGGER_INIT.set(());
297 Ok(())
298}
299
300pub fn setup_default() -> Result<(), LoggerError> {
314 let level = std::env::var("LOG_LEVEL")
315 .or_else(|_| std::env::var("RUST_LOG"))
316 .ok()
317 .and_then(|s| s.parse().ok())
318 .unwrap_or(Level::INFO);
319
320 let format = std::env::var("LOG_FORMAT")
321 .ok()
322 .and_then(|s| s.parse().ok())
323 .unwrap_or(LogFormat::Auto);
324
325 let throttle_enabled = std::env::var("LOG_THROTTLE_ENABLED")
326 .ok()
327 .is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("true"));
328
329 let throttle_burst = std::env::var("LOG_THROTTLE_BURST")
330 .ok()
331 .and_then(|v| v.parse().ok())
332 .unwrap_or(50.0);
333
334 let throttle_rate = std::env::var("LOG_THROTTLE_RATE")
335 .ok()
336 .and_then(|v| v.parse().ok())
337 .unwrap_or(1.0);
338
339 let service_name = std::env::var("SERVICE_NAME").ok();
340 let service_version = std::env::var("SERVICE_VERSION").ok();
341
342 setup(LoggerOptions {
343 level,
344 format,
345 throttle: ThrottleConfig {
346 enabled: throttle_enabled,
347 burst: throttle_burst,
348 rate: throttle_rate,
349 ..Default::default()
350 },
351 service_name,
352 service_version,
353 ..Default::default()
354 })
355}
356
357fn build_throttle_filter(config: &ThrottleConfig) -> Option<TracingRateLimitLayer> {
359 if !config.enabled {
360 return None;
361 }
362
363 let policy = Policy::token_bucket(config.burst, config.rate)
364 .unwrap_or_else(|_| Policy::token_bucket(50.0, 1.0).expect("default policy is valid"));
365
366 let mut builder = TracingRateLimitLayer::builder()
367 .with_policy(policy)
368 .with_max_signatures(config.max_signatures);
369
370 if !config.excluded_fields.is_empty() {
371 builder = builder.with_excluded_fields(config.excluded_fields.clone());
372 }
373
374 match builder.build() {
375 Ok(layer) => Some(layer),
376 Err(e) => {
377 eprintln!("Failed to build log throttle layer: {e}");
378 None
379 }
380 }
381}
382
383fn is_terminal() -> bool {
385 use std::io::IsTerminal;
386 io::stderr().is_terminal()
387}
388
389fn is_no_color() -> bool {
391 std::env::var("NO_COLOR").is_ok()
392}
393
394#[cfg(test)]
395mod tests {
396 use super::*;
397
398 #[test]
399 fn test_log_format_from_str() {
400 assert_eq!("json".parse::<LogFormat>().unwrap(), LogFormat::Json);
401 assert_eq!("text".parse::<LogFormat>().unwrap(), LogFormat::Text);
402 assert_eq!("pretty".parse::<LogFormat>().unwrap(), LogFormat::Text);
403 assert_eq!("auto".parse::<LogFormat>().unwrap(), LogFormat::Auto);
404 assert!(matches!(
406 "yaml".parse::<LogFormat>(),
407 Err(LoggerError::InvalidFormat(_))
408 ));
409 }
410
411 #[test]
412 fn test_log_format_resolve() {
413 assert_eq!(LogFormat::Json.resolve(), LogFormat::Json);
415 assert_eq!(LogFormat::Text.resolve(), LogFormat::Text);
416
417 let resolved = LogFormat::Auto.resolve();
419 assert!(matches!(resolved, LogFormat::Json | LogFormat::Text));
420 }
421
422 #[test]
423 fn test_logger_options_default() {
424 let opts = LoggerOptions::default();
425 assert_eq!(opts.level, Level::INFO);
426 assert_eq!(opts.format, LogFormat::Auto);
427 assert!(opts.add_source);
428 assert!(opts.enable_masking);
429 assert!(!opts.sensitive_fields.is_empty());
430 }
431
432 #[test]
433 fn test_is_no_color() {
434 temp_env::with_var("NO_COLOR", None::<&str>, || assert!(!is_no_color()));
435 temp_env::with_var("NO_COLOR", Some("1"), || assert!(is_no_color()));
436 }
437}