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>;