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}