saluki_app/logging/
config.rs1use 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
11pub struct LoggingConfiguration {
19 pub log_level: LogLevel,
21
22 pub log_format_json: bool,
24
25 pub log_format_rfc3339: bool,
31
32 pub log_to_console: bool,
34
35 pub log_to_syslog: bool,
42
43 pub syslog_uri: String,
50
51 pub syslog_rfc: bool,
56
57 pub log_file: String,
59
60 pub log_file_max_size: ByteSize,
62
63 pub log_file_max_rolls: usize,
65}
66
67impl LoggingConfiguration {
68 pub fn simple() -> Self {
72 Self {
73 log_level: LevelFilter::INFO.into(),
74 log_format_json: false,
75 log_format_rfc3339: false,
76 log_to_console: true,
77 log_to_syslog: false,
78 syslog_uri: String::new(),
79 syslog_rfc: false,
80 log_file: String::new(),
81 log_file_max_size: DEFAULT_LOG_FILE_MAX_SIZE,
82 log_file_max_rolls: DEFAULT_LOG_FILE_MAX_ROLLS,
83 }
84 }
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn simple_defaults_to_console_only_logging_with_syslog_disabled() {
93 let config = LoggingConfiguration::simple();
94
95 assert_eq!(config.log_level.as_env_filter().to_string(), "info");
96 assert!(!config.log_format_json);
97 assert!(!config.log_format_rfc3339);
98 assert!(config.log_to_console);
99 assert!(!config.log_to_syslog);
100 assert!(config.syslog_uri.is_empty());
101 assert!(!config.syslog_rfc);
102 assert!(config.log_file.is_empty());
103 assert_eq!(config.log_file_max_size, DEFAULT_LOG_FILE_MAX_SIZE);
104 assert_eq!(config.log_file_max_rolls, DEFAULT_LOG_FILE_MAX_ROLLS);
105 }
106
107 #[test]
108 fn log_level_try_from_rejects_empty_string() {
109 let error = match LogLevel::try_from(String::new()) {
110 Ok(_) => panic!("an empty log level must be rejected"),
111 Err(error) => error,
112 };
113 assert!(
114 error.to_string().contains("cannot be empty"),
115 "unexpected error message: {error}"
116 );
117 }
118
119 #[test]
120 fn log_level_try_from_parses_valid_directive() {
121 let log_level = LogLevel::try_from("saluki=debug".to_string()).expect("a valid directive should parse");
122 assert_eq!(log_level.as_env_filter().to_string(), "saluki=debug");
123 }
124}
125
126#[derive(Deserialize)]
130#[serde(try_from = "String")]
131pub struct LogLevel(EnvFilter);
132
133impl LogLevel {
134 pub fn as_env_filter(&self) -> EnvFilter {
136 self.0.clone()
137 }
138}
139
140impl From<LevelFilter> for LogLevel {
141 fn from(level: LevelFilter) -> Self {
142 Self(EnvFilter::default().add_directive(level.into()))
143 }
144}
145
146impl TryFrom<String> for LogLevel {
147 type Error = GenericError;
148
149 fn try_from(value: String) -> Result<Self, Self::Error> {
150 if value.is_empty() {
151 return Err(generic_error!("Log level cannot be empty."));
152 }
153
154 EnvFilter::builder()
155 .parse(value)
156 .map(Self)
157 .error_context("Failed to parse valid log level.")
158 }
159}
160
161impl fmt::Display for LogLevel {
162 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
163 self.0.fmt(f)
164 }
165}