1use parking_lot::Mutex;
2use std::sync::Arc;
3
4use tracing_subscriber::{Registry, filter::LevelFilter, prelude::*, util::SubscriberInitExt};
5
6use crate::{
7 LogLevel,
8 error::{BuildError, InitError},
9 layer::{BackendId, BackendLayer, SinkRegistry},
10 sink::FormatterConfig,
11 sinks::console::ConsoleSink,
12};
13
14#[cfg(feature = "file")]
15use crate::sinks::file::{FileSink, validate_file_config};
16
17#[cfg(feature = "store")]
18use crate::{sinks::store::StoreSink, store::LogStore};
19
20#[derive(Debug, Clone)]
23enum BackendKind {
24 Console,
25 #[cfg(feature = "file")]
26 File(FileConfig),
27 #[cfg(feature = "store")]
28 Store(LogStore),
29}
30
31#[derive(Debug, Clone)]
32struct BackendSpec {
33 id: BackendId,
34 kind: BackendKind,
35}
36
37#[allow(clippy::struct_excessive_bools)]
38#[derive(Debug, Clone)]
39struct LoggerState {
40 max_level: LogLevel,
41 ansi: bool,
42 target: bool,
43 timestamp: bool,
44 backend_specs: Vec<BackendSpec>,
45 next_backend_id: BackendId,
46 #[cfg(feature = "log")]
47 log_bridge: bool,
48}
49
50impl Default for LoggerState {
51 fn default() -> Self {
52 Self {
53 max_level: LogLevel::Info,
54 ansi: true,
55 target: true,
56 timestamp: true,
57 backend_specs: vec![BackendSpec {
58 id: 0,
59 kind: BackendKind::Console,
60 }],
61 next_backend_id: 1,
62 #[cfg(feature = "log")]
63 log_bridge: false,
64 }
65 }
66}
67
68#[derive(Clone, Default)]
70pub struct Logger {
71 state: Arc<Mutex<LoggerState>>,
72 sinks: Arc<SinkRegistry>,
73}
74
75impl std::fmt::Debug for Logger {
76 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
77 let state = self.lock_state();
78 f.debug_struct("Logger")
79 .field("max_level", &state.max_level)
80 .field("ansi", &state.ansi)
81 .field("target", &state.target)
82 .field("timestamp", &state.timestamp)
83 .field("backend_specs", &state.backend_specs)
84 .field("next_backend_id", &state.next_backend_id)
85 .finish_non_exhaustive()
86 }
87}
88
89impl Logger {
90 #[must_use]
102 pub fn new() -> Self {
103 Self::default()
104 }
105
106 #[must_use]
108 pub fn with_max_level(self, level: LogLevel) -> Self {
109 self.update_state(|state| state.max_level = level);
110 self
111 }
112
113 #[must_use]
117 pub fn with_console(self, enabled: bool) -> Self {
118 self.set_console_enabled(enabled);
119 self
120 }
121
122 #[must_use]
124 pub fn with_ansi(self, enabled: bool) -> Self {
125 self.update_state(|state| state.ansi = enabled);
126 self
127 }
128
129 #[must_use]
131 pub fn with_target(self, enabled: bool) -> Self {
132 self.update_state(|state| state.target = enabled);
133 self
134 }
135
136 #[must_use]
138 pub fn with_timestamp(self, enabled: bool) -> Self {
139 self.update_state(|state| state.timestamp = enabled);
140 self
141 }
142
143 #[cfg(feature = "file")]
146 #[cfg_attr(docsrs, doc(cfg(feature = "file")))]
147 #[must_use]
148 pub fn with_file(self, config: FileConfig) -> Self {
149 self.stage_backend(BackendKind::File(config));
150 self
151 }
152
153 #[cfg(feature = "file")]
163 #[cfg_attr(docsrs, doc(cfg(feature = "file")))]
164 pub fn add_file_backend(&self, config: FileConfig) -> Result<(), BuildError> {
165 let sink = Arc::new(Self::build_file_sink(&config)?);
166 let backend_id = self.stage_backend(BackendKind::File(config));
167 self.sinks.insert(backend_id, sink);
168 Ok(())
169 }
170
171 #[cfg(feature = "store")]
174 #[cfg_attr(docsrs, doc(cfg(feature = "store")))]
175 #[must_use]
176 pub fn with_store(self, store: LogStore) -> Self {
177 self.stage_backend(BackendKind::Store(store));
178 self
179 }
180
181 #[cfg(feature = "store")]
186 #[cfg_attr(docsrs, doc(cfg(feature = "store")))]
187 pub fn add_store_backend(&self, store: LogStore) {
188 let sink = Arc::new(StoreSink::new(store.clone()));
189 let backend_id = self.stage_backend(BackendKind::Store(store));
190 self.sinks.insert(backend_id, sink);
191 }
192
193 #[cfg(feature = "log")]
196 #[cfg_attr(docsrs, doc(cfg(feature = "log")))]
197 #[must_use]
198 pub fn with_log_bridge(self, enabled: bool) -> Self {
199 self.update_state(|state| state.log_bridge = enabled);
200 self
201 }
202
203 pub fn build(&self) -> Result<BackendLayer, BuildError> {
215 self.ensure_realized_backends()?;
216
217 Ok(BackendLayer::new(
218 self.formatter_config(),
219 Arc::clone(&self.sinks),
220 ))
221 }
222
223 pub fn init(&self) -> Result<(), InitError> {
233 let max_level = self.lock_state().max_level;
234 #[cfg(feature = "log")]
235 let log_bridge = self.lock_state().log_bridge;
236
237 let layer = self.build()?;
238
239 #[cfg(feature = "log")]
240 if log_bridge {
241 tracing_log::LogTracer::init().map_err(|_| InitError::LogBridgeAlreadyInitialized)?;
242 }
243
244 Registry::default()
245 .with(LevelFilter::from(max_level))
246 .with(layer)
247 .try_init()
248 .map_err(|_| InitError::AlreadyInitialized)
249 }
250
251 fn formatter_config(&self) -> FormatterConfig {
252 let state = self.lock_state();
253 FormatterConfig {
254 ansi: state.ansi,
255 target: state.target,
256 timestamp: state.timestamp,
257 }
258 }
259
260 fn ensure_realized_backends(&self) -> Result<(), BuildError> {
261 let specs = self.lock_state().backend_specs.clone();
262
263 for spec in specs {
264 if self.sinks.contains(spec.id) {
265 continue;
266 }
267
268 let sink = Self::build_sink(&spec.kind)?;
269 self.sinks.insert(spec.id, sink);
270 }
271
272 Ok(())
273 }
274
275 #[allow(clippy::unnecessary_wraps)]
276 fn build_sink(kind: &BackendKind) -> Result<crate::sink::SharedSink, BuildError> {
277 Ok(match kind {
278 BackendKind::Console => Arc::new(ConsoleSink),
279 #[cfg(feature = "file")]
280 BackendKind::File(config) => Arc::new(Self::build_file_sink(config)?),
281 #[cfg(feature = "store")]
282 BackendKind::Store(store) => Arc::new(StoreSink::new(store.clone())),
283 })
284 }
285
286 #[cfg(feature = "file")]
287 fn build_file_sink(config: &FileConfig) -> Result<FileSink, BuildError> {
288 validate_file_config(config)?;
289 FileSink::new(config)
290 }
291
292 fn set_console_enabled(&self, enabled: bool) {
293 let backend_id = self.update_state(|state| {
294 let console_index = state
295 .backend_specs
296 .iter()
297 .position(|spec| matches!(spec.kind, BackendKind::Console));
298
299 match (enabled, console_index) {
300 (true, Some(index)) => Some(state.backend_specs[index].id),
301 (true, None) => {
302 let id = state.next_backend_id;
303 state.next_backend_id += 1;
304 state.backend_specs.insert(
305 0,
306 BackendSpec {
307 id,
308 kind: BackendKind::Console,
309 },
310 );
311 Some(id)
312 }
313 (false, Some(index)) => Some(state.backend_specs.remove(index).id),
314 (false, None) => None,
315 }
316 });
317
318 match (enabled, backend_id) {
319 (true, Some(id)) if !self.sinks.contains(id) => {
320 self.sinks.insert(id, Arc::new(ConsoleSink));
321 }
322 (false, Some(id)) => self.sinks.remove(id),
323 _ => {}
324 }
325 }
326
327 #[allow(unused)]
329 fn stage_backend(&self, kind: BackendKind) -> BackendId {
330 self.update_state(|state| {
331 let id = state.next_backend_id;
332 state.next_backend_id += 1;
333 state.backend_specs.push(BackendSpec { id, kind });
334 id
335 })
336 }
337
338 fn update_state<T>(&self, update: impl FnOnce(&mut LoggerState) -> T) -> T {
339 let mut state = self.state.lock();
340 update(&mut state)
341 }
342
343 fn lock_state(&self) -> parking_lot::MutexGuard<'_, LoggerState> {
344 self.state.lock()
345 }
346}
347
348#[cfg(feature = "file")]
350#[cfg_attr(docsrs, doc(cfg(feature = "file")))]
351#[derive(Debug, Clone, PartialEq, Eq)]
352pub struct FileConfig {
353 pub(crate) directory: std::path::PathBuf,
354 pub(crate) latest_file_name: String,
355 pub(crate) session_file_prefix: Option<String>,
356}
357
358#[cfg(feature = "file")]
359impl FileConfig {
360 #[must_use]
362 pub fn new(directory: impl Into<std::path::PathBuf>) -> Self {
363 Self {
364 directory: directory.into(),
365 latest_file_name: String::from("latest.log"),
366 session_file_prefix: None,
367 }
368 }
369
370 #[must_use]
372 pub fn with_latest_file_name(mut self, file_name: impl Into<String>) -> Self {
373 self.latest_file_name = file_name.into();
374 self
375 }
376
377 #[must_use]
381 pub fn with_session_file_prefix(mut self, prefix: impl Into<String>) -> Self {
382 self.session_file_prefix = Some(prefix.into());
383 self
384 }
385
386 #[must_use]
388 pub fn directory(&self) -> &std::path::Path {
389 &self.directory
390 }
391
392 #[must_use]
394 pub fn latest_file_name(&self) -> &str {
395 &self.latest_file_name
396 }
397
398 #[must_use]
400 pub fn session_file_prefix(&self) -> Option<&str> {
401 self.session_file_prefix.as_deref()
402 }
403}
404
405#[cfg(test)]
406mod tests {
407 use super::{BackendKind, Logger};
408 use crate::LogLevel;
409
410 #[test]
411 fn defaults_match_public_contract() {
412 let logger = Logger::new();
413 let state = logger.lock_state();
414
415 assert_eq!(state.max_level, LogLevel::Info);
416 assert!(state.ansi);
417 assert!(state.target);
418 assert!(state.timestamp);
419 assert_eq!(state.backend_specs.len(), 1);
420 assert!(matches!(state.backend_specs[0].kind, BackendKind::Console));
421
422 #[cfg(feature = "log")]
423 assert!(!state.log_bridge);
424 }
425
426 #[test]
427 fn max_level_is_stored() {
428 let logger = Logger::new().with_max_level(LogLevel::Debug);
429 assert_eq!(logger.lock_state().max_level, LogLevel::Debug);
430 }
431
432 #[test]
433 fn console_sink_can_be_disabled() {
434 let logger = Logger::new().with_console(false);
435 assert!(logger.lock_state().backend_specs.is_empty());
436 }
437
438 #[test]
439 fn console_sink_can_be_reenabled() {
440 let logger = Logger::new().with_console(false).with_console(true);
441 let state = logger.lock_state();
442
443 assert_eq!(state.backend_specs.len(), 1);
444 assert!(matches!(state.backend_specs[0].kind, BackendKind::Console));
445 }
446}