saluki_app/logging/
mod.rs

1//! Logging.
2
3// TODO: `AgentLikeFieldVisitor` currently allocates a `String` to hold the message field when it finds it. This is
4// suboptimal because it means we allocate a string literally every time we log a message. Logging is rare, but it's
5// just a recipe for small, unnecessary allocations over time... and makes it that much more inefficient to enable
6// debug/trace-level logging in production.
7//
8// We might consider _something_ like a string pool in the future, but we can defer that until we have a better idea of
9// what the potential impact is in practice.
10
11use std::io::Write;
12
13use saluki_error::{generic_error, GenericError};
14use tracing_appender::non_blocking::{NonBlocking, NonBlockingBuilder, WorkerGuard};
15use tracing_rolling_file::RollingFileAppenderBase;
16use tracing_subscriber::{
17    layer::SubscriberExt as _, reload::Layer as ReloadLayer, util::SubscriberInitExt as _, Layer,
18};
19
20mod api;
21use self::api::set_logging_api_handler;
22pub use self::api::{acquire_logging_api_handler, LoggingAPIHandler};
23
24mod config;
25pub(crate) use self::config::LoggingConfiguration;
26
27mod layer;
28use self::layer::build_formatting_layer;
29
30// Number of buffered lines in each non-blocking log writer.
31//
32// This directly influences the idle memory usage since each logging backend (console, file, etc) will have a bounded
33// channel that can hold this many elements, and each element is roughly 32 bytes, so 1,000 elements/lines consumes a
34// minimum of ~32KB, etc.
35const NB_LOG_WRITER_BUFFER_SIZE: usize = 4096;
36
37#[derive(Default)]
38pub(crate) struct LoggingGuard {
39    worker_guards: Vec<WorkerGuard>,
40}
41
42impl LoggingGuard {
43    fn add_worker_guard(&mut self, guard: WorkerGuard) {
44        self.worker_guards.push(guard);
45    }
46}
47
48/// Initializes the logging subsystem for `tracing` with the ability to dynamically update the log filtering directives
49/// at runtime.
50///
51/// This function reads the `DD_LOG_LEVEL` environment variable to determine the log level to use. If the environment
52/// variable is not set, the default log level is `INFO`. Additionally, it reads the `DD_LOG_FORMAT_JSON` environment
53/// variable to determine which output format to use. If it is set to `json` (case insensitive), the logs will be
54/// formatted as JSON. If it is set to any other value, or not set at all, the logs will default to a rich, colored,
55/// human-readable format.
56///
57/// An API handler can be acquired (via [`acquire_logging_api_handler`]) to install the API routes which allow for
58/// dynamically controlling the logging level filtering. See [`LoggingAPIHandler`] for more information.
59///
60/// Returns a [`LoggingGuard`] which must be held until the application is about to shutdown, ensuring that any
61/// configured logging backends are able to completely flush any pending logs before the application exits.
62///
63/// # Errors
64///
65/// If the logging subsystem was already initialized, an error will be returned.
66pub(crate) async fn initialize_logging(config: &LoggingConfiguration) -> Result<LoggingGuard, GenericError> {
67    // TODO: Support for logging to syslog.
68
69    // Set up our log level filtering and dynamic filter layer.
70    let level_filter = config.log_level.as_env_filter();
71    let (filter_layer, reload_handle) = ReloadLayer::new(level_filter.clone());
72    set_logging_api_handler(LoggingAPIHandler::new(level_filter, reload_handle));
73
74    // Build all configured layers: one per output mechanism (console, file, etc).
75    let mut configured_layers = Vec::new();
76    let mut logging_guard = LoggingGuard::default();
77
78    if config.log_to_console {
79        let (nb_stdout, guard) = writer_to_nonblocking("console", std::io::stdout());
80        logging_guard.add_worker_guard(guard);
81
82        configured_layers.push(build_formatting_layer(config, nb_stdout));
83    }
84
85    if !config.log_file.is_empty() {
86        let appender_builder = RollingFileAppenderBase::builder();
87        let appender = appender_builder
88            .filename(config.log_file.clone())
89            .max_filecount(config.log_file_max_rolls)
90            .condition_max_file_size(config.log_file_max_size.as_u64())
91            .build()
92            .map_err(|e| generic_error!("Failed to build log file appender: {}", e))?;
93
94        let (nb_appender, guard) = writer_to_nonblocking("file", appender);
95        logging_guard.add_worker_guard(guard);
96
97        configured_layers.push(build_formatting_layer(config, nb_appender));
98    }
99
100    // `tracing` accepts a `Vec<L>` where `L` implements `Layer<S>`, which acts as a fanout.. and then we're applying
101    // our filter layer on top of that, so that we filter out events once rather than per output layer.
102    tracing_subscriber::registry()
103        .with(configured_layers.with_filter(filter_layer))
104        .try_init()?;
105
106    Ok(logging_guard)
107}
108
109fn writer_to_nonblocking<W>(writer_name: &'static str, writer: W) -> (NonBlocking, WorkerGuard)
110where
111    W: Write + Send + 'static,
112{
113    let thread_name = format!("log-writer-{}", writer_name);
114    NonBlockingBuilder::default()
115        .thread_name(&thread_name)
116        .buffered_lines_limit(NB_LOG_WRITER_BUFFER_SIZE)
117        .lossy(true)
118        .finish(writer)
119}