1use tracing_subscriber::{Registry, filter::LevelFilter, prelude::*, util::SubscriberInitExt};
2
3use crate::{
4 LogLevel,
5 error::{BuildError, InitError},
6 layer::BackendLayer,
7 sink::FormatterConfig,
8 sinks::console::ConsoleSink,
9};
10
11#[cfg(feature = "file")]
12use crate::sinks::file::{FileSink, validate_file_config};
13
14#[allow(clippy::struct_excessive_bools)]
16#[derive(Debug, Clone)]
17pub struct Logger {
18 max_level: LogLevel,
19 ansi: bool,
20 target: bool,
21 timestamp: bool,
22 #[cfg(feature = "file")]
23 file: Option<FileConfig>,
24 #[cfg(feature = "log")]
25 log_bridge: bool,
26}
27
28impl Default for Logger {
29 fn default() -> Self {
30 Self {
31 max_level: LogLevel::Info,
32 ansi: true,
33 target: true,
34 timestamp: true,
35 #[cfg(feature = "file")]
36 file: None,
37 #[cfg(feature = "log")]
38 log_bridge: false,
39 }
40 }
41}
42
43impl Logger {
44 #[must_use]
54 pub fn new() -> Self {
55 Self::default()
56 }
57
58 #[must_use]
60 pub fn with_max_level(mut self, level: LogLevel) -> Self {
61 self.max_level = level;
62 self
63 }
64
65 #[must_use]
67 pub fn with_ansi(mut self, enabled: bool) -> Self {
68 self.ansi = enabled;
69 self
70 }
71
72 #[must_use]
74 pub fn with_target(mut self, enabled: bool) -> Self {
75 self.target = enabled;
76 self
77 }
78
79 #[must_use]
81 pub fn with_timestamp(mut self, enabled: bool) -> Self {
82 self.timestamp = enabled;
83 self
84 }
85
86 #[cfg(feature = "file")]
88 #[cfg_attr(docsrs, doc(cfg(feature = "file")))]
89 #[must_use]
90 pub fn with_file(mut self, config: FileConfig) -> Self {
91 self.file = Some(config);
92 self
93 }
94
95 #[cfg(feature = "log")]
98 #[cfg_attr(docsrs, doc(cfg(feature = "log")))]
99 #[must_use]
100 pub fn with_log_bridge(mut self, enabled: bool) -> Self {
101 self.log_bridge = enabled;
102 self
103 }
104
105 pub fn build(self) -> Result<BackendLayer, BuildError> {
117 let formatter = FormatterConfig {
118 ansi: self.ansi,
119 target: self.target,
120 timestamp: self.timestamp,
121 };
122
123 #[allow(unused_mut)]
124 let mut sinks = vec![Box::new(ConsoleSink) as _];
125
126 #[cfg(feature = "file")]
127 if let Some(file) = self.file {
128 validate_file_config(&file)?;
129 sinks.push(Box::new(FileSink::new(&file)?) as _);
130 }
131
132 Ok(BackendLayer::new(formatter, sinks))
133 }
134
135 pub fn init(self) -> Result<(), InitError> {
145 let max_level = self.max_level;
146 #[cfg(feature = "log")]
147 let log_bridge = self.log_bridge;
148
149 let layer = self.build()?;
150
151 #[cfg(feature = "log")]
152 if log_bridge {
153 tracing_log::LogTracer::init().map_err(|_| InitError::LogBridgeAlreadyInitialized)?;
154 }
155
156 Registry::default()
157 .with(LevelFilter::from(max_level))
158 .with(layer)
159 .try_init()
160 .map_err(|_| InitError::AlreadyInitialized)
161 }
162}
163
164#[cfg(feature = "file")]
166#[cfg_attr(docsrs, doc(cfg(feature = "file")))]
167#[derive(Debug, Clone, PartialEq, Eq)]
168pub struct FileConfig {
169 pub(crate) directory: std::path::PathBuf,
170 pub(crate) latest_file_name: String,
171 pub(crate) session_file_prefix: Option<String>,
172}
173
174#[cfg(feature = "file")]
175impl FileConfig {
176 #[must_use]
178 pub fn new(directory: impl Into<std::path::PathBuf>) -> Self {
179 Self {
180 directory: directory.into(),
181 latest_file_name: String::from("latest.log"),
182 session_file_prefix: None,
183 }
184 }
185
186 #[must_use]
188 pub fn with_latest_file_name(mut self, file_name: impl Into<String>) -> Self {
189 self.latest_file_name = file_name.into();
190 self
191 }
192
193 #[must_use]
197 pub fn with_session_file_prefix(mut self, prefix: impl Into<String>) -> Self {
198 self.session_file_prefix = Some(prefix.into());
199 self
200 }
201
202 #[must_use]
204 pub fn directory(&self) -> &std::path::Path {
205 &self.directory
206 }
207
208 #[must_use]
210 pub fn latest_file_name(&self) -> &str {
211 &self.latest_file_name
212 }
213
214 #[must_use]
216 pub fn session_file_prefix(&self) -> Option<&str> {
217 self.session_file_prefix.as_deref()
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use tracing_subscriber::filter::LevelFilter;
224
225 use super::Logger;
226
227 #[test]
228 fn defaults_match_public_contract() {
229 let logger = Logger::new();
230
231 assert_eq!(logger.max_level, LevelFilter::INFO);
232 assert!(logger.ansi);
233 assert!(logger.target);
234 assert!(logger.timestamp);
235
236 #[cfg(feature = "log")]
237 assert!(!logger.log_bridge);
238
239 #[cfg(feature = "file")]
240 assert!(logger.file.is_none());
241 }
242
243 #[test]
244 fn max_level_is_stored() {
245 let logger = Logger::new().with_max_level(LevelFilter::DEBUG);
246 assert_eq!(logger.max_level, LevelFilter::DEBUG);
247 }
248}