Skip to main content

saluki_app/logging/
config.rs

1use std::fmt;
2
3use bytesize::ByteSize;
4use saluki_error::{generic_error, ErrorContext as _, GenericError};
5use serde::Deserialize;
6use tracing_subscriber::{filter::LevelFilter, EnvFilter};
7
8const DEFAULT_LOG_FILE_MAX_SIZE: ByteSize = ByteSize::mib(10);
9const DEFAULT_LOG_FILE_MAX_ROLLS: usize = 1;
10
11/// Logging configuration.
12///
13/// This is a plain value type. Callers construct instances directly, either via [`simple`] for a basic
14/// console-only configuration or via a translator that maps from the application's wider configuration into this
15/// type, applying any application-specific rules.
16///
17/// [`simple`]: LoggingConfiguration::simple
18pub struct LoggingConfiguration {
19    /// Verbosity directives (e.g., `info`, `debug`, `saluki=trace`).
20    pub log_level: LogLevel,
21
22    /// Whether to emit log records as JSON instead of the default human-readable format.
23    pub log_format_json: bool,
24
25    /// Whether to write log records to standard output.
26    pub log_to_console: bool,
27
28    /// Whether to write log records to syslog.
29    ///
30    /// Defaults to `false`. When this is `true` and [`syslog_uri`] is empty, callers should resolve the destination to
31    /// the platform's default local syslog URI before constructing the logging output stack.
32    ///
33    /// [`syslog_uri`]: LoggingConfiguration::syslog_uri
34    pub log_to_syslog: bool,
35
36    /// URI of the syslog destination.
37    ///
38    /// Defaults to an empty string. An empty value means "use the platform default" only when [`log_to_syslog`] is
39    /// enabled; otherwise it has no effect. Supported URI schemes are handled by the syslog output implementation.
40    ///
41    /// [`log_to_syslog`]: LoggingConfiguration::log_to_syslog
42    pub syslog_uri: String,
43
44    /// Whether to use the Agent's RFC-style syslog header.
45    ///
46    /// Defaults to `false`, which preserves the Agent's legacy syslog header format. Set this to `true` when the
47    /// receiving syslog daemon expects the Agent's RFC-style header.
48    pub syslog_rfc: bool,
49
50    /// Path to the log file to write to, or empty to disable file logging.
51    pub log_file: String,
52
53    /// Maximum size of a log file before it is rolled over.
54    pub log_file_max_size: ByteSize,
55
56    /// Maximum number of rolled-over log files to retain.
57    pub log_file_max_rolls: usize,
58}
59
60impl LoggingConfiguration {
61    /// Returns a configuration that writes only to the console in human-readable format at INFO level.
62    ///
63    /// Used as a safe default when an application has not yet supplied an explicit configuration.
64    pub fn simple() -> Self {
65        Self {
66            log_level: LevelFilter::INFO.into(),
67            log_format_json: false,
68            log_to_console: true,
69            log_to_syslog: false,
70            syslog_uri: String::new(),
71            syslog_rfc: false,
72            log_file: String::new(),
73            log_file_max_size: DEFAULT_LOG_FILE_MAX_SIZE,
74            log_file_max_rolls: DEFAULT_LOG_FILE_MAX_ROLLS,
75        }
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use super::*;
82
83    #[test]
84    fn simple_defaults_to_console_only_logging_with_syslog_disabled() {
85        let config = LoggingConfiguration::simple();
86
87        assert_eq!(config.log_level.as_env_filter().to_string(), "info");
88        assert!(!config.log_format_json);
89        assert!(config.log_to_console);
90        assert!(!config.log_to_syslog);
91        assert!(config.syslog_uri.is_empty());
92        assert!(!config.syslog_rfc);
93        assert!(config.log_file.is_empty());
94        assert_eq!(config.log_file_max_size, DEFAULT_LOG_FILE_MAX_SIZE);
95        assert_eq!(config.log_file_max_rolls, DEFAULT_LOG_FILE_MAX_ROLLS);
96    }
97}
98
99/// A parsed `tracing` log level filter.
100///
101/// Wraps [`EnvFilter`] so it can be deserialized from a string (e.g., `"info"`, `"saluki=trace,info"`).
102#[derive(Deserialize)]
103#[serde(try_from = "String")]
104pub struct LogLevel(EnvFilter);
105
106impl LogLevel {
107    /// Returns the underlying `EnvFilter`.
108    pub fn as_env_filter(&self) -> EnvFilter {
109        self.0.clone()
110    }
111}
112
113impl From<LevelFilter> for LogLevel {
114    fn from(level: LevelFilter) -> Self {
115        Self(EnvFilter::default().add_directive(level.into()))
116    }
117}
118
119impl TryFrom<String> for LogLevel {
120    type Error = GenericError;
121
122    fn try_from(value: String) -> Result<Self, Self::Error> {
123        if value.is_empty() {
124            return Err(generic_error!("Log level cannot be empty."));
125        }
126
127        EnvFilter::builder()
128            .parse(value)
129            .map(Self)
130            .error_context("Failed to parse valid log level.")
131    }
132}
133
134impl fmt::Display for LogLevel {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        self.0.fmt(f)
137    }
138}