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