saluki_app/logging/
config.rs1use std::fmt;
2
3use bytesize::ByteSize;
4use saluki_common::logging::parse_filter_directives;
5use saluki_error::{generic_error, ErrorContext as _, GenericError};
6use serde::Deserialize;
7use tracing::Level;
8use tracing_subscriber::filter::{LevelFilter, Targets};
9
10const DEFAULT_LOG_FILE_MAX_SIZE: ByteSize = ByteSize::mib(10);
11const DEFAULT_LOG_FILE_MAX_ROLLS: usize = 1;
12
13pub struct LoggingConfiguration {
21 pub log_level: LogLevel,
23
24 pub log_format_json: bool,
26
27 pub log_format_rfc3339: bool,
33
34 pub log_to_console: bool,
36
37 pub log_to_syslog: bool,
44
45 pub syslog_uri: String,
52
53 pub syslog_rfc: bool,
58
59 pub log_file: String,
61
62 pub log_file_max_size: ByteSize,
64
65 pub log_file_max_rolls: usize,
67}
68
69impl LoggingConfiguration {
70 pub fn simple() -> Self {
74 Self {
75 log_level: LevelFilter::INFO.into(),
76 log_format_json: false,
77 log_format_rfc3339: false,
78 log_to_console: true,
79 log_to_syslog: false,
80 syslog_uri: String::new(),
81 syslog_rfc: false,
82 log_file: String::new(),
83 log_file_max_size: DEFAULT_LOG_FILE_MAX_SIZE,
84 log_file_max_rolls: DEFAULT_LOG_FILE_MAX_ROLLS,
85 }
86 }
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn simple_defaults_to_console_only_logging_with_syslog_disabled() {
95 let config = LoggingConfiguration::simple();
96
97 assert_eq!(config.log_level.as_targets().to_string(), "info");
98 assert!(!config.log_format_json);
99 assert!(!config.log_format_rfc3339);
100 assert!(config.log_to_console);
101 assert!(!config.log_to_syslog);
102 assert!(config.syslog_uri.is_empty());
103 assert!(!config.syslog_rfc);
104 assert!(config.log_file.is_empty());
105 assert_eq!(config.log_file_max_size, DEFAULT_LOG_FILE_MAX_SIZE);
106 assert_eq!(config.log_file_max_rolls, DEFAULT_LOG_FILE_MAX_ROLLS);
107 }
108
109 #[test]
110 fn log_level_try_from_rejects_empty_string() {
111 let error = match LogLevel::try_from(String::new()) {
112 Ok(_) => panic!("an empty log level must be rejected"),
113 Err(error) => error,
114 };
115 assert!(
116 error.to_string().contains("cannot be empty"),
117 "unexpected error message: {error}"
118 );
119 }
120
121 #[test]
122 fn log_level_try_from_parses_valid_directive() {
123 let log_level = LogLevel::try_from("saluki=debug".to_string()).expect("a valid directive should parse");
124 assert_eq!(log_level.as_targets().to_string(), "saluki=debug");
125 }
126}
127
128#[derive(Deserialize)]
133#[serde(try_from = "String")]
134pub struct LogLevel(Targets);
135
136impl LogLevel {
137 pub fn as_targets(&self) -> Targets {
139 self.0.clone()
140 }
141
142 pub fn info() -> Self {
146 Self(Targets::new().with_default(Level::INFO))
147 }
148}
149
150impl From<LevelFilter> for LogLevel {
151 fn from(level: LevelFilter) -> Self {
152 Self(Targets::new().with_default(level))
153 }
154}
155
156impl TryFrom<String> for LogLevel {
157 type Error = GenericError;
158
159 fn try_from(value: String) -> Result<Self, Self::Error> {
160 if value.is_empty() {
161 return Err(generic_error!("Log level cannot be empty."));
162 }
163
164 parse_filter_directives(&value)
165 .map(Self)
166 .error_context("Failed to parse valid log level.")
167 }
168}
169
170impl fmt::Display for LogLevel {
171 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172 self.0.fmt(f)
173 }
174}