libdd_log/
logger.rs

1// Copyright 2025-Present Datadog, Inc. https://www.datadoghq.com/
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::writers::{FileWriter, StdWriter};
5use std::sync::{LazyLock, Mutex};
6use tracing::subscriber::DefaultGuard;
7use tracing_subscriber::filter::LevelFilter;
8use tracing_subscriber::layer::{Layered, SubscriberExt};
9use tracing_subscriber::reload::Handle;
10use tracing_subscriber::{fmt, reload, EnvFilter, Layer, Registry};
11
12pub type Error = String;
13
14/// Log level for filtering log events.
15#[repr(C)]
16#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
17pub enum LogEventLevel {
18    /// The "trace" level.
19    ///
20    /// Designates very low priority, often extremely verbose, information.
21    Trace = 0,
22    /// The "debug" level.
23    ///
24    /// Designates lower priority information.
25    Debug = 1,
26    /// The "info" level.
27    ///
28    /// Designates useful information.
29    Info = 2,
30    /// The "warn" level.
31    ///
32    /// Designates hazardous situations.
33    Warn = 3,
34    /// The "error" level.
35    ///
36    /// Designates very serious errors.
37    Error = 4,
38}
39
40/// Configuration for file-based logging.
41pub struct FileConfig {
42    /// Path where log files will be written.
43    pub path: String,
44    /// Maximum size in bytes for each log file.
45    /// Set to 0 to disable size-based rotation.
46    pub max_size_bytes: u64,
47    /// Maximum total number of files (current + rotated) to keep on disk.
48    /// When this limit is exceeded, the oldest rotated files are deleted.
49    /// Set to 0 to disable file cleanup.
50    pub max_files: u64,
51}
52
53/// Target for standard stream output.
54#[repr(C)]
55#[derive(Debug, Clone, Copy)]
56pub enum StdTarget {
57    /// Write to standard output (stdout).
58    Out,
59    /// Write to standard error (stderr).
60    Err,
61}
62
63/// Configuration for standard stream logging.
64pub struct StdConfig {
65    /// Target stream (stdout or stderr).
66    pub target: StdTarget,
67}
68
69/// Logger with layer-based architecture.
70struct Logger {
71    /// Handle for modifying the log layers at runtime.
72    /// Complex type definition causes issues with cbindgen, so we suppress clippy's type
73    /// complexity warning.
74    #[allow(clippy::type_complexity)]
75    layer_handle: Handle<
76        Vec<Box<dyn Layer<Layered<reload::Layer<EnvFilter, Registry>, Registry>> + Send + Sync>>,
77        Layered<reload::Layer<EnvFilter, Registry>, Registry>,
78    >,
79    /// Handle for modifying the log filter at runtime.
80    filter_handle: Handle<EnvFilter, Registry>,
81    /// Guard is for local subscriber which is not used in the global logger.
82    #[allow(dead_code)]
83    _guard: Option<DefaultGuard>,
84    /// File configuration.
85    file_config: Option<FileConfig>,
86    /// Standard stream configuration.
87    std_config: Option<StdConfig>,
88}
89
90impl Logger {
91    #[cfg(test)]
92    fn setup() -> Result<Self, Error> {
93        Self::setup_with_global(false)
94    }
95
96    fn setup_global() -> Result<Self, Error> {
97        Self::setup_with_global(true)
98    }
99
100    fn setup_with_global(global: bool) -> Result<Self, Error> {
101        let layers = vec![];
102        let env_filter = env_filter();
103        let (filter_layer, filter_handle) = reload::Layer::new(env_filter);
104        let (layers_layer, layer_handle) = reload::Layer::new(layers);
105
106        let subscriber = tracing_subscriber::registry()
107            .with(filter_layer)
108            .with(layers_layer);
109
110        if global {
111            match tracing::subscriber::set_global_default(subscriber) {
112                Ok(_) => Ok(Self {
113                    layer_handle,
114                    filter_handle,
115                    _guard: None,
116                    file_config: None,
117                    std_config: None,
118                }),
119
120                Err(_e) => Err(Error::from("Failed to set global default subscriber")),
121            }
122        } else {
123            Ok(Self {
124                layer_handle,
125                filter_handle,
126                _guard: Some(tracing::subscriber::set_default(subscriber)),
127                file_config: None,
128                std_config: None,
129            })
130        }
131    }
132
133    fn configure(&self) -> Result<(), Error> {
134        self.layer_handle
135            .modify(|layers| {
136                // Clear existing layers first
137                // since we can't selectively replace them because of the dynamic nature of the
138                // layers. This is necessary to avoid accumulating layers on each
139                // configuration call.
140                layers.clear();
141
142                // Add file layer if configured
143                if let Some(file_config) = &self.file_config {
144                    if let Ok(file_layer) = file_layer(file_config) {
145                        layers.push(file_layer);
146                    }
147                }
148
149                if let Some(std_config) = &self.std_config {
150                    if let Ok(std_layer) = std_layer(std_config) {
151                        layers.push(std_layer);
152                    }
153                }
154            })
155            .map_err(|e| Error::from(format!("Failed to update logger configuration: {e}")))?;
156
157        Ok(())
158    }
159
160    fn disable_file(&mut self) -> Result<(), Error> {
161        self.file_config = None;
162        self.configure()
163    }
164
165    fn configure_file(&mut self, file_config: FileConfig) -> Result<(), Error> {
166        self.file_config = Some(file_config);
167        self.configure()
168    }
169
170    fn disable_std(&mut self) -> Result<(), Error> {
171        self.std_config = None;
172        self.configure()
173    }
174
175    fn configure_std(&mut self, std_config: StdConfig) -> Result<(), Error> {
176        self.std_config = Some(std_config);
177        self.configure()
178    }
179
180    /// Set the log level for the logger.
181    fn set_log_level(&self, log_level: LogEventLevel) -> Result<(), Error> {
182        let level_filter = LevelFilter::from(log_level);
183        let new_filter = EnvFilter::try_from_default_env()
184            .unwrap_or_else(|_| EnvFilter::new(level_filter.to_string().to_lowercase()));
185
186        self.filter_handle
187            .modify(|filter| {
188                *filter = new_filter;
189            })
190            .map_err(|e| Error::from(format!("Failed to update log level: {e}")))?;
191
192        Ok(())
193    }
194}
195
196/// Create environment filter with default to INFO level.
197fn env_filter() -> EnvFilter {
198    EnvFilter::try_from_default_env()
199        .unwrap_or_else(|_| EnvFilter::new(LevelFilter::INFO.to_string().to_lowercase()))
200}
201
202/// Create standard output layer.
203#[allow(clippy::type_complexity)]
204fn std_layer(
205    config: &StdConfig,
206) -> Result<
207    Box<dyn Layer<Layered<reload::Layer<EnvFilter, Registry>, Registry>> + Send + Sync + 'static>,
208    Error,
209> {
210    let writer = StdWriter::new(config.target);
211
212    Ok(fmt::layer()
213        .with_writer(writer)
214        .with_thread_ids(true)
215        .with_thread_names(true)
216        .with_target(true)
217        .with_file(true)
218        .with_line_number(true)
219        .with_ansi(false)
220        .boxed())
221}
222
223#[allow(clippy::type_complexity)]
224fn file_layer(
225    config: &FileConfig,
226) -> Result<
227    Box<dyn Layer<Layered<reload::Layer<EnvFilter, Registry>, Registry>> + Send + Sync + 'static>,
228    Error,
229> {
230    let writer = FileWriter::new(config)
231        .map_err(|e| Error::from(format!("Failed to create file writer: {e}")))?;
232
233    Ok(fmt::layer()
234        .with_writer(writer)
235        .with_thread_ids(true)
236        .with_thread_names(true)
237        .with_target(true)
238        .with_file(true)
239        .with_line_number(true)
240        .with_ansi(false)
241        .json()
242        .boxed())
243}
244
245impl From<LogEventLevel> for LevelFilter {
246    fn from(level: LogEventLevel) -> Self {
247        match level {
248            LogEventLevel::Trace => LevelFilter::TRACE,
249            LogEventLevel::Debug => LevelFilter::DEBUG,
250            LogEventLevel::Info => LevelFilter::INFO,
251            LogEventLevel::Warn => LevelFilter::WARN,
252            LogEventLevel::Error => LevelFilter::ERROR,
253        }
254    }
255}
256
257static LOGGER: LazyLock<Mutex<Option<Logger>>> = LazyLock::new(|| Mutex::new(None));
258
259/// Configures the global logger to write to a file in JSON format.
260///
261/// # Arguments
262/// * `file_config` - Configuration specifying the file path
263pub fn logger_configure_file(file_config: FileConfig) -> Result<(), Error> {
264    let logger_mutex = &LOGGER;
265    let mut logger_guard = logger_mutex
266        .lock()
267        .map_err(|e| Error::from(format!("Failed to acquire logger lock: {e}")))?;
268
269    if let Some(logger) = logger_guard.as_mut() {
270        logger.configure_file(file_config)
271    } else {
272        let mut logger = Logger::setup_global()?;
273        logger.configure_file(file_config)?;
274        *logger_guard = Some(logger);
275        Ok(())
276    }
277}
278
279/// Disables file logging for the global logger.
280///
281/// Removes file logging configuration while keeping other outputs (like std streams) active.
282pub fn logger_disable_file() -> Result<(), Error> {
283    let logger_mutex = &LOGGER;
284    let mut logger_guard = logger_mutex
285        .lock()
286        .map_err(|e| Error::from(format!("Failed to acquire logger lock: {e}")))?;
287
288    if let Some(logger) = logger_guard.as_mut() {
289        logger.disable_file()
290    } else {
291        Err(Error::from("Logger not initialized"))
292    }
293}
294
295/// Configures the global logger to write to stdout or stderr in compact format.
296///
297/// # Arguments
298/// * `std_config` - Configuration specifying stdout or stderr
299pub fn logger_configure_std(std_config: StdConfig) -> Result<(), Error> {
300    let logger_mutex = &LOGGER;
301    let mut logger_guard = logger_mutex
302        .lock()
303        .map_err(|e| Error::from(format!("Failed to acquire logger lock: {e}")))?;
304
305    if let Some(logger) = logger_guard.as_mut() {
306        logger.configure_std(std_config)
307    } else {
308        let mut logger = Logger::setup_global()?;
309        logger.configure_std(std_config)?;
310        *logger_guard = Some(logger);
311        Ok(())
312    }
313}
314
315/// Disables standard stream logging for the global logger.
316///
317/// Removes std stream logging configuration while keeping other outputs (like file) active.
318pub fn logger_disable_std() -> Result<(), Error> {
319    let logger_mutex = &LOGGER;
320    let mut logger_guard = logger_mutex
321        .lock()
322        .map_err(|e| Error::from(format!("Failed to acquire logger lock: {e}")))?;
323
324    if let Some(logger) = logger_guard.as_mut() {
325        logger.disable_std()
326    } else {
327        Err(Error::from("Logger not initialized"))
328    }
329}
330
331/// Sets the minimum log level for the global logger.
332///
333/// # Arguments
334/// * `log_level` - Minimum level (Trace, Debug, Info, Warn, Error)
335pub fn logger_set_log_level(log_level: LogEventLevel) -> Result<(), Error> {
336    let logger_mutex = &LOGGER;
337    let logger_guard = logger_mutex
338        .lock()
339        .map_err(|e| Error::from(format!("Failed to acquire logger lock: {e}")))?;
340
341    if let Some(logger) = logger_guard.as_ref() {
342        logger.set_log_level(log_level)
343    } else {
344        Err(Error::from("Logger not initialized"))
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use std::sync::{Arc, Mutex};
351    use tempfile::TempDir;
352    use tracing::field::{Field, Visit};
353    use tracing::subscriber::Interest;
354    use tracing::{debug, error, info, trace, warn, Event, Metadata, Subscriber};
355    use tracing_subscriber::layer::{Context, Layer};
356
357    use super::*;
358
359    #[derive(Default)]
360    struct MessageVisitor {
361        message: Option<String>,
362        all_fields: std::collections::HashMap<String, String>,
363    }
364
365    impl Visit for MessageVisitor {
366        fn record_i64(&mut self, field: &Field, value: i64) {
367            let field_name = field.name();
368            let field_value = value.to_string();
369            self.all_fields
370                .insert(field_name.to_string(), field_value.clone());
371
372            if field_name == "message" {
373                self.message = Some(field_value);
374            }
375        }
376
377        fn record_u64(&mut self, field: &Field, value: u64) {
378            let field_name = field.name();
379            let field_value = value.to_string();
380            self.all_fields
381                .insert(field_name.to_string(), field_value.clone());
382
383            if field_name == "message" {
384                self.message = Some(field_value);
385            }
386        }
387
388        fn record_bool(&mut self, field: &Field, value: bool) {
389            let field_name = field.name();
390            let field_value = value.to_string();
391            self.all_fields
392                .insert(field_name.to_string(), field_value.clone());
393
394            if field_name == "message" {
395                self.message = Some(field_value);
396            }
397        }
398
399        fn record_str(&mut self, field: &Field, value: &str) {
400            let field_name = field.name();
401            self.all_fields
402                .insert(field_name.to_string(), value.to_string());
403
404            if field_name == "message" {
405                self.message = Some(value.to_string());
406            }
407        }
408
409        fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
410            let field_name = field.name();
411            let field_value = format!("{value:?}");
412            self.all_fields
413                .insert(field_name.to_string(), field_value.clone());
414
415            if field_name == "message" {
416                self.message = Some(field_value);
417            }
418        }
419    }
420
421    #[derive(Default)]
422    struct RecordingLayer<S> {
423        events: Arc<Mutex<Vec<String>>>,
424        _subscriber: std::marker::PhantomData<S>,
425    }
426
427    impl<S> RecordingLayer<S> {
428        fn new(events: Arc<Mutex<Vec<String>>>) -> Self {
429            RecordingLayer {
430                events,
431                _subscriber: std::marker::PhantomData,
432            }
433        }
434    }
435
436    impl<S> Layer<S> for RecordingLayer<S>
437    where
438        S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
439    {
440        fn register_callsite(&self, _metadata: &'static Metadata<'static>) -> Interest {
441            Interest::always()
442        }
443
444        fn enabled(&self, _metadata: &Metadata<'_>, _ctx: Context<'_, S>) -> bool {
445            true
446        }
447
448        fn on_event(&self, event: &Event<'_>, _ctx: Context<'_, S>) {
449            let mut visitor = MessageVisitor::default();
450            event.record(&mut visitor);
451
452            let mut events = self.events.lock().unwrap();
453            let message = visitor.message.unwrap_or_else(|| {
454                // If no explicit message field, try to reconstruct from all fields
455                if !visitor.all_fields.is_empty() {
456                    format!("Fields: {:?}", visitor.all_fields)
457                } else {
458                    format!(
459                        "Event: {} - {}",
460                        event.metadata().target(),
461                        event.metadata().name()
462                    )
463                }
464            });
465            events.push(message);
466        }
467    }
468    #[test]
469    #[cfg_attr(miri, ignore)]
470    fn test_logger_setup() {
471        let logger = Logger::setup();
472        assert!(logger.is_ok(), "Logger setup should succeed");
473    }
474
475    #[test]
476    #[cfg_attr(miri, ignore)]
477    fn test_logger_with_std() {
478        let events: Arc<Mutex<Vec<String>>> = Default::default();
479        let mut logger = Logger::setup().expect("Should setup logger successfully");
480
481        let std_config = StdConfig {
482            target: StdTarget::Out,
483        };
484
485        logger
486            .configure_std(std_config)
487            .expect("Should configure std output");
488
489        // Add recording layer after configuration
490        logger
491            .layer_handle
492            .modify(|layers| {
493                layers.push(Box::new(RecordingLayer::new(Arc::clone(&events))));
494            })
495            .expect("Should be able to add recording layer");
496
497        logger
498            .set_log_level(LogEventLevel::Info)
499            .expect("Should set log level to Info");
500
501        info!(message = "Std output test message");
502
503        let captured_events = events.lock().unwrap();
504        assert_eq!(
505            captured_events.len(),
506            1,
507            "Should capture message with std output"
508        );
509        assert_eq!(captured_events[0], "Std output test message");
510
511        drop(logger);
512    }
513
514    #[test]
515    #[cfg_attr(miri, ignore)]
516    fn test_logger_with_file() {
517        let events: Arc<Mutex<Vec<String>>> = Default::default();
518        let mut logger = Logger::setup().expect("Should setup logger successfully");
519
520        let temp_dir = TempDir::new().expect("Should create temp directory");
521        let log_path = temp_dir.path().join("test.log");
522
523        let file_config = FileConfig {
524            path: log_path.to_string_lossy().to_string(),
525            max_files: 0,
526            max_size_bytes: 0,
527        };
528
529        logger
530            .configure_file(file_config)
531            .expect("Should configure file output");
532
533        // Add recording layer after configuration
534        logger
535            .layer_handle
536            .modify(|layers| {
537                layers.push(Box::new(RecordingLayer::new(Arc::clone(&events))));
538            })
539            .expect("Should be able to add recording layer");
540
541        logger
542            .set_log_level(LogEventLevel::Info)
543            .expect("Should set log level to Info");
544
545        info!(message = "File output test message");
546
547        let captured_events = events.lock().unwrap();
548        assert_eq!(
549            captured_events.len(),
550            1,
551            "Should capture message with file output"
552        );
553        assert_eq!(captured_events[0], "File output test message");
554        drop(captured_events);
555
556        assert!(
557            log_path.exists(),
558            "Log file should be created at {log_path:?}"
559        );
560
561        // add delay to ensure file is written
562        std::thread::sleep(std::time::Duration::from_millis(100));
563
564        if let Ok(content) = std::fs::read_to_string(&log_path) {
565            assert!(
566                !content.is_empty(),
567                "Log file should contain some log output"
568            );
569        }
570
571        drop(logger);
572    }
573
574    #[test]
575    #[cfg_attr(miri, ignore)]
576    fn test_logger_with_std_and_file() {
577        let events: Arc<Mutex<Vec<String>>> = Default::default();
578        let mut logger = Logger::setup().expect("Should setup logger successfully");
579
580        // Configure std output
581        let std_config = StdConfig {
582            target: StdTarget::Err,
583        };
584        logger
585            .configure_std(std_config)
586            .expect("Should configure std output");
587
588        let temp_dir = TempDir::new().expect("Should create temp directory");
589        let log_path = temp_dir.path().join("test.log");
590        let file_config = FileConfig {
591            path: log_path.to_string_lossy().to_string(),
592            max_size_bytes: 0,
593            max_files: 0,
594        };
595        logger
596            .configure_file(file_config)
597            .expect("Should configure file output");
598
599        // Add recording layer after configuration
600        logger
601            .layer_handle
602            .modify(|layers| {
603                layers.push(Box::new(RecordingLayer::new(Arc::clone(&events))));
604            })
605            .expect("Should be able to add recording layer");
606
607        logger
608            .set_log_level(LogEventLevel::Info)
609            .expect("Should set log level to Info");
610
611        warn!(message = "Std and file output test message");
612
613        let captured_events = events.lock().unwrap();
614        assert_eq!(
615            captured_events.len(),
616            1,
617            "Should capture message with std and file output"
618        );
619        assert_eq!(captured_events[0], "Std and file output test message");
620        drop(captured_events);
621
622        // Verify that the log file was created
623        assert!(
624            log_path.exists(),
625            "Log file should be created at {log_path:?}"
626        );
627
628        drop(logger);
629    }
630
631    #[test]
632    #[cfg_attr(miri, ignore)]
633    fn test_logger_level_change() {
634        let events: Arc<Mutex<Vec<String>>> = Default::default();
635        let logger = Logger::setup().expect("Should setup logger successfully");
636
637        // Add recording layer
638        logger
639            .layer_handle
640            .modify(|layers| {
641                layers.push(Box::new(RecordingLayer::new(Arc::clone(&events))));
642            })
643            .expect("Should be able to add recording layer");
644
645        // Test TRACE level (captures everything)
646        logger
647            .set_log_level(LogEventLevel::Trace)
648            .expect("Should set log level to Trace");
649
650        trace!(message = "Trace message");
651        debug!(message = "Debug message");
652        info!(message = "Info message");
653        warn!(message = "Warn message");
654        error!(message = "Error message");
655
656        {
657            let captured_events = events.lock().unwrap();
658            assert_eq!(
659                captured_events.len(),
660                5,
661                "Should capture all 5 messages at TRACE level"
662            );
663        }
664
665        // Clear and test WARN level (only WARN and ERROR)
666        events.lock().unwrap().clear();
667        logger
668            .set_log_level(LogEventLevel::Warn)
669            .expect("Should set log level to Warn");
670
671        trace!(message = "Trace filtered");
672        debug!(message = "Debug filtered");
673        info!(message = "Info filtered");
674        warn!(message = "Warn message");
675        error!(message = "Error message");
676
677        {
678            let captured_events = events.lock().unwrap();
679            assert_eq!(
680                captured_events.len(),
681                2,
682                "Should capture only WARN and ERROR messages"
683            );
684            assert_eq!(captured_events[0], "Warn message");
685            assert_eq!(captured_events[1], "Error message");
686        }
687
688        // Clear and test ERROR level (only ERROR)
689        events.lock().unwrap().clear();
690        logger
691            .set_log_level(LogEventLevel::Error)
692            .expect("Should set log level to Error");
693
694        trace!(message = "Trace filtered");
695        debug!(message = "Debug filtered");
696        info!(message = "Info filtered");
697        warn!(message = "Warn filtered");
698        error!(message = "Error message");
699
700        {
701            let captured_events = events.lock().unwrap();
702            assert_eq!(
703                captured_events.len(),
704                1,
705                "Should capture only ERROR message"
706            );
707            assert_eq!(captured_events[0], "Error message");
708        }
709
710        drop(logger);
711    }
712}