timber_rust/service/write/io.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Dante Doménech Martinez dante19031999@gmail.com
3
4use crate::service::ServiceError;
5use crate::service::fallback::Fallback;
6use crate::service::write::{MessageFormatter, StandardMessageFormatter};
7use crate::{LoggerStatus, Message, Service};
8use std::any::Any;
9use std::io::BufWriter;
10use std::sync::Mutex;
11
12/// A private synchronization container for [`IoWrite`].
13///
14/// This struct groups the writer and formatter into a single unit. This ensures
15/// **atomicity**: the formatter state and writer output are synchronized.
16/// By placing both in a single [`Mutex`], we guarantee that log interleaving
17/// is impossible even if the formatter holds internal state.
18struct IoData<W, F>
19where
20 W: std::io::Write + Send + Sync,
21 F: MessageFormatter,
22{
23 /// The byte-oriented output destination.
24 writer: W,
25 /// The logic used to transform a [`Message`] into bytes.
26 formatter: F,
27}
28
29/// A thread-safe [`Service`] for byte-stream logging destinations.
30///
31/// [`IoWrite`] is the primary workhorse for file-based, socket-based, or
32/// console-based logging. It implements the [`Service`] trait by wrapping its
33/// internal data in a [`Mutex`].
34///
35/// ### Performance Note
36/// This service does not explicitly call `flush()` after every write. If low-latency
37/// is required with guaranteed persistence, wrap your writer in [`std::io::BufWriter`].
38pub struct IoWrite<W, F>
39where
40 W: std::io::Write + Send + Sync,
41 F: MessageFormatter,
42{
43 /// The mutex-protected destination and formatting logic.
44 writer: Mutex<IoData<W, F>>,
45}
46
47impl<W, F> IoWrite<W, F>
48where
49 W: std::io::Write + Send + Sync,
50 F: MessageFormatter,
51{
52 /// Creates a new [`IoWrite`] on the heap.
53 ///
54 /// # Parameters
55 /// - `writer`: A type implementing [`std::io::Write`].
56 /// - `formatter`: The [`MessageFormatter`] implementation.
57 pub fn new(writer: W) -> Box<Self> {
58 Box::new(Self {
59 writer: Mutex::new(IoData {
60 writer,
61 formatter: Default::default(),
62 }),
63 })
64 }
65
66 /// Creates a new [`IoWrite`] on the heap with a custom [formatter][`MessageFormatter`].
67 ///
68 /// # Parameters
69 /// - `writer`: A type implementing [`std::io::Write`].
70 /// - `formatter`: The [`MessageFormatter`] implementation.
71 pub fn with_formatter(writer: W, formatter: F) -> Box<Self> {
72 Box::new(Self {
73 writer: Mutex::new(IoData { writer, formatter }),
74 })
75 }
76}
77
78impl<W, F> Service for IoWrite<W, F>
79where
80 W: std::io::Write + Send + Sync + 'static,
81 F: MessageFormatter + 'static,
82{
83 fn status(&self) -> LoggerStatus {
84 LoggerStatus::Running
85 }
86
87 /// Acquires the lock and streams the formatted message to the writer.
88 ///
89 /// # Errors
90 /// - [`ServiceError::LockPoisoned`]: If the internal [`Mutex`] is poisoned.
91 /// - [`ServiceError`]: If the formatter fails or the writer encounters an I/O error.
92 fn work(&self, msg: &Message) -> Result<(), ServiceError> {
93 let mut guard = self.writer.lock()?;
94
95 // Destructuring allows simultaneous mutable access to both fields.
96 let IoData {
97 formatter, writer, ..
98 } = &mut *guard;
99
100 formatter.format_io(msg, writer)?;
101 Ok(())
102 }
103
104 fn as_any(&self) -> &dyn Any {
105 self
106 }
107}
108
109impl<W, F> Fallback for IoWrite<W, F>
110where
111 W: std::io::Write + Send + Sync + 'static,
112 F: MessageFormatter + 'static,
113{
114 /// Best-effort fallback. Skips writing if the mutex is locked or poisoned
115 /// to prevent cascading failures in the logging pipeline.
116 fn fallback(&self, error: &ServiceError, msg: &Message) {
117 if let Ok(mut guard) = self.writer.lock() {
118 let mut out = std::io::stdout();
119 let _ = guard.formatter.format_io(msg, &mut out);
120 let _ = eprintln!("IoWriteService Fallback [Error: {}]", error);
121 }
122 }
123}
124
125/// A type alias for an [`IoWrite`] service using a dynamic trait object.
126///
127/// This is particularly useful when you need to change the logging destination
128/// at runtime (e.g., switching from a File to a Network stream).
129///
130/// **Bound Requirements:** The inner writer must be [`Send`] + [`Sync`] + `'static`.
131#[allow(type_alias_bounds)]
132pub type BoxedIo<F: MessageFormatter> = IoWrite<Box<dyn std::io::Write + Send + Sync>, F>;
133
134/// A type alias for an [`IoWrite`] service writing specifically to a [`std::fs::File`].
135#[allow(type_alias_bounds)]
136pub type File<F: MessageFormatter> = IoWrite<std::fs::File, F>;
137
138/// A type alias for an [`IoWrite`] service writing specifically to a [`std::io::BufWriter<std::fs::File>`][`BufWriter`].
139#[allow(type_alias_bounds)]
140pub type BufferedFile<F: MessageFormatter> = IoWrite<BufWriter<std::fs::File>, F>;
141
142/// A pre-configured [`BoxedIo`] service using the crate's [`StandardMessageFormatter`].
143pub type StandardBoxedIo = BoxedIo<StandardMessageFormatter>;
144
145/// A pre-configured [`File`] using the crate's [`StandardMessageFormatter`].
146pub type StandardFile = File<StandardMessageFormatter>;
147
148/// A pre-configured [`BufferedFile`] using the crate's [`StandardMessageFormatter`].
149pub type StandardBufferedFile = BufferedFile<StandardMessageFormatter>;