Skip to main content

log_io/
sink.rs

1//! Output sinks.
2//!
3//! A sink takes a [`Record`], serializes it with its [`Format`] and
4//! delivers the bytes somewhere. The built-in sinks are:
5//!
6//! | Sink | Destination | Notes |
7//! |---|---|---|
8//! | [`StdoutSink`], [`StderrSink`] | standard streams | written under the stream's own lock |
9//! | [`FileSink`] | a file | buffered; see [`FileSink`] for durability |
10//! | [`WriterSink`] | any `io::Write` | e.g. a [`RotatingFile`] ([`RotatingFileSink`]) |
11//! | [`NonBlocking`] | any `io::Write`, on a background thread | bounded queue, explicit overflow policy |
12//! | [`NullSink`] | nowhere | benchmarks and tests |
13//!
14//! # Threading
15//!
16//! Every sink is `Send + Sync`. The synchronous sinks serialize writers
17//! with a mutex and perform the write on the calling thread, so in an
18//! async runtime they block the executor thread for the duration of
19//! the write. Use [`NonBlocking`] to move I/O off the calling thread.
20//!
21//! # Allocation
22//!
23//! Records are formatted into a reusable thread-local buffer, so the
24//! synchronous sinks do not allocate in steady state. A buffer that
25//! grew past 64 KiB for an unusually large record is shrunk back to
26//! 64 KiB afterwards, so one huge record does not pin memory on every
27//! thread for the life of the process.
28//!
29//! # Reliability
30//!
31//! The built-in sinks recover from mutex poisoning: if a thread panics
32//! while holding a sink's lock (for example inside a custom writer),
33//! later records are still written. Logging stays available during
34//! incident response.
35
36use core::cell::RefCell;
37use std::io::{self, Write};
38use std::sync::{Mutex, MutexGuard};
39
40use crate::error::{Error, Result};
41use crate::format::Format;
42use crate::record::Record;
43
44/// Output stage of the pipeline.
45///
46/// Implementors must be safe to call from many threads concurrently.
47/// A sink should never panic on bad input; return an error instead so
48/// the logger can report it and keep feeding the other sinks.
49///
50/// # Examples
51///
52#[cfg_attr(all(feature = "json"), doc = "```")]
53#[cfg_attr(not(all(feature = "json")), doc = "```ignore")]
54/// use std::sync::Mutex;
55/// use log_io::format::{Format, JsonFormat};
56/// use log_io::{Level, Logger, Record, Result, Sink};
57///
58/// /// Collects formatted records in memory.
59/// #[derive(Default)]
60/// struct Memory(Mutex<Vec<String>>);
61///
62/// impl Sink for Memory {
63///     fn write_record(&self, record: &Record<'_>) -> Result<()> {
64///         let mut line = String::new();
65///         JsonFormat::new().write_record(record, &mut line)?;
66///         self.0.lock().unwrap_or_else(|p| p.into_inner()).push(line);
67///         Ok(())
68///     }
69///     fn flush(&self) -> Result<()> {
70///         Ok(())
71///     }
72/// }
73///
74/// let sink = std::sync::Arc::new(Memory::default());
75/// let logger = Logger::builder().no_timestamps().with_sink(sink.clone()).build();
76/// logger.log(Level::Info, "hello", &[]);
77/// assert_eq!(sink.0.lock().unwrap().len(), 1);
78/// ```
79pub trait Sink: Send + Sync {
80    /// Serialize and deliver one record.
81    ///
82    /// # Errors
83    ///
84    /// Returns the destination's I/O error, a format error, or a
85    /// sink-specific error.
86    fn write_record(&self, record: &Record<'_>) -> Result<()>;
87
88    /// Push any buffered bytes to the destination (for files, to the
89    /// operating system; see the sink's own docs for `fsync`).
90    ///
91    /// # Errors
92    ///
93    /// Returns the destination's I/O error, or an error deferred from
94    /// an earlier background operation.
95    fn flush(&self) -> Result<()>;
96
97    /// Flush triggered by a record at or above the logger's flush level
98    /// (see [`crate::LoggerBuilder::flush_on`]).
99    ///
100    /// The default implementation calls [`Sink::flush`], so the record
101    /// has reached the destination when the logging call returns. A
102    /// background sink may override it to hand the request to its
103    /// worker without waiting, when waiting would break its own latency
104    /// contract; [`NonBlocking`] does this under
105    /// [`Overflow::DropNewest`]. [`crate::Logger::flush`] and
106    /// [`crate::Logger::shutdown`] call [`Sink::flush`] and
107    /// [`Sink::shutdown`], never this method.
108    ///
109    /// # Errors
110    ///
111    /// Same as [`Sink::flush`]. A sink that does not wait can only
112    /// report errors it already knows about.
113    fn request_flush(&self) -> Result<()> {
114        self.flush()
115    }
116
117    /// Flush and release background resources. Called by
118    /// [`crate::Logger::shutdown`], by a [`crate::FlushGuard`] on drop,
119    /// and when the last clone of a [`crate::Logger`] is dropped.
120    ///
121    /// A sink must keep accepting records after `shutdown` (writing
122    /// them synchronously if its background machinery is gone): late
123    /// records from other threads must not be lost. The default
124    /// implementation calls [`Sink::flush`].
125    ///
126    /// # Errors
127    ///
128    /// Same as [`Sink::flush`].
129    fn shutdown(&self) -> Result<()> {
130        self.flush()
131    }
132}
133
134impl<S: Sink + ?Sized> Sink for std::sync::Arc<S> {
135    fn write_record(&self, record: &Record<'_>) -> Result<()> {
136        (**self).write_record(record)
137    }
138    fn flush(&self) -> Result<()> {
139        (**self).flush()
140    }
141    fn request_flush(&self) -> Result<()> {
142        (**self).request_flush()
143    }
144    fn shutdown(&self) -> Result<()> {
145        (**self).shutdown()
146    }
147}
148
149impl<S: Sink + ?Sized> Sink for Box<S> {
150    fn write_record(&self, record: &Record<'_>) -> Result<()> {
151        (**self).write_record(record)
152    }
153    fn flush(&self) -> Result<()> {
154        (**self).flush()
155    }
156    fn request_flush(&self) -> Result<()> {
157        (**self).request_flush()
158    }
159    fn shutdown(&self) -> Result<()> {
160        (**self).shutdown()
161    }
162}
163
164mod file;
165mod non_blocking;
166mod recent;
167mod rotating;
168mod stdio;
169mod writer;
170
171pub use self::file::{FileSink, FileSinkBuilder};
172pub(crate) use self::non_blocking::on_background_worker;
173pub use self::non_blocking::{NonBlocking, NonBlockingBuilder, Overflow};
174pub use self::recent::{DEFAULT_LINE_LIMIT, RecentLines};
175pub use self::rotating::{RotatingFile, RotatingFileBuilder, Rotation};
176pub use self::stdio::{StderrSink, StdoutSink};
177pub use self::writer::{NullSink, WriterSink};
178
179/// A [`WriterSink`] over a [`RotatingFile`].
180///
181/// # Examples
182///
183#[cfg_attr(all(feature = "json"), doc = "```no_run")]
184#[cfg_attr(not(all(feature = "json")), doc = "```ignore")]
185/// use log_io::format::JsonFormat;
186/// use log_io::sink::{RotatingFile, RotatingFileSink, Rotation};
187///
188/// let file = RotatingFile::builder("logs/app.jsonl")
189///     .max_bytes(10 * 1024 * 1024)
190///     .rotation(Rotation::Daily)
191///     .max_files(7)
192///     .open()?;
193/// let sink: RotatingFileSink<JsonFormat> = RotatingFileSink::new(file, JsonFormat::new());
194/// # Ok::<(), std::io::Error>(())
195/// ```
196pub type RotatingFileSink<F> = WriterSink<RotatingFile, F>;
197
198/// Keeps a sink's lock on its own cache lines.
199///
200/// Every logging thread reads the logger's sink table on each record,
201/// and every thread writes the sink's mutex. If a small sink allocation
202/// shares a cache line with other hot data, each lock and unlock
203/// invalidates that line for all threads. Measured on the reference
204/// machine, padding restored the 4-thread throughput of 1.0.
205#[repr(align(128))]
206pub(crate) struct CachePadded<T>(pub(crate) T);
207
208impl<T> core::ops::Deref for CachePadded<T> {
209    type Target = T;
210    fn deref(&self) -> &T {
211        &self.0
212    }
213}
214
215/// Capacity a thread-local format buffer may keep between records.
216pub(crate) const MAX_RETAINED_CAPACITY: usize = 64 * 1024;
217
218thread_local! {
219    /// Reusable formatting buffer, so steady-state formatting does not
220    /// touch the allocator.
221    static FORMAT_BUF: RefCell<String> = const { RefCell::new(String::new()) };
222}
223
224/// Shrink `buf` back to [`MAX_RETAINED_CAPACITY`] after an oversized
225/// record. Cheap when no shrinking is needed: one comparison.
226#[inline]
227pub(crate) fn trim_buffer(buf: &mut String) {
228    if buf.capacity() > MAX_RETAINED_CAPACITY {
229        buf.clear();
230        buf.shrink_to(MAX_RETAINED_CAPACITY);
231    }
232}
233
234/// Format `record` into the thread-local buffer and pass the text to
235/// `write`.
236///
237/// If the buffer is already in use on this thread (a sink that logs
238/// from inside its own write) or thread-local storage is being torn
239/// down, a temporary buffer is used instead.
240pub(crate) fn with_formatted<F, W>(format: &F, record: &Record<'_>, mut write: W) -> Result<()>
241where
242    F: Format,
243    W: FnMut(&str) -> io::Result<()>,
244{
245    let pooled = FORMAT_BUF.try_with(|cell| {
246        let mut buf = cell.try_borrow_mut().ok()?;
247        Some(format_into(&mut buf, format, record, &mut write))
248    });
249    match pooled {
250        Ok(Some(result)) => result,
251        _ => format_into(&mut String::with_capacity(256), format, record, &mut write),
252    }
253}
254
255fn format_into<F, W>(buf: &mut String, format: &F, record: &Record<'_>, write: &mut W) -> Result<()>
256where
257    F: Format,
258    W: FnMut(&str) -> io::Result<()>,
259{
260    buf.clear();
261    let result = format
262        .write_record_generic(record, buf)
263        .map_err(Error::Format)
264        .and_then(|()| write(buf).map_err(Error::Io));
265    trim_buffer(buf);
266    result
267}
268
269/// Applies a Unix permission mode to files `options` creates. On other
270/// platforms the file inherits its directory's access control list and
271/// `mode` has no effect.
272pub(crate) fn apply_mode(options: &mut std::fs::OpenOptions, mode: Option<u32>) {
273    #[cfg(unix)]
274    if let Some(mode) = mode {
275        use std::os::unix::fs::OpenOptionsExt;
276        let _options = options.mode(mode);
277    }
278    #[cfg(not(unix))]
279    let _unused = (options, mode);
280}
281
282/// Acquire a mutex guard even if the mutex is poisoned.
283///
284/// Poisoning only means a previous holder unwound; the writer inside is
285/// still usable, and logging must keep working after a panic.
286pub(crate) fn lock_recover<T: ?Sized>(m: &Mutex<T>) -> MutexGuard<'_, T> {
287    m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
288}
289
290/// Format `record` and write it to the writer behind `target`.
291pub(crate) fn write_locked<W: Write + ?Sized, F: Format>(
292    target: &Mutex<W>,
293    format: &F,
294    record: &Record<'_>,
295) -> Result<()> {
296    with_formatted(format, record, |text| {
297        lock_recover(target).write_all(text.as_bytes())
298    })
299}
300
301/// Flush the writer behind `target`.
302pub(crate) fn flush_locked<W: Write + ?Sized>(target: &Mutex<W>) -> Result<()> {
303    lock_recover(target).flush().map_err(Error::from)
304}
305
306#[cfg(all(test, feature = "json"))]
307mod tests {
308    use super::*;
309    use crate::format::JsonFormat;
310    use crate::{Level, Metadata};
311
312    #[test]
313    fn test_trim_buffer_caps_retained_capacity() {
314        let mut buf = String::with_capacity(4 * MAX_RETAINED_CAPACITY);
315        buf.push('x');
316        trim_buffer(&mut buf);
317        assert!(buf.capacity() <= MAX_RETAINED_CAPACITY);
318        let mut small = String::with_capacity(128);
319        small.push('y');
320        trim_buffer(&mut small);
321        assert_eq!(small, "y", "small buffers are left alone");
322    }
323
324    #[test]
325    fn test_thread_local_buffer_shrinks_after_oversize_record() {
326        // The audit's LOG-007 reproduction, scaled down: one 1 MiB
327        // record must not leave a 1 MiB+ buffer behind.
328        std::thread::spawn(|| {
329            let big = "x".repeat(1024 * 1024);
330            let record = Record::new(Metadata::new(Level::Info, "t"), &big, &[]);
331            let mut written = 0;
332            with_formatted(&JsonFormat::new(), &record, |s| {
333                written = s.len();
334                Ok(())
335            })
336            .unwrap();
337            assert!(written > big.len());
338            let cap = FORMAT_BUF.with(|b| b.borrow().capacity());
339            assert!(cap <= MAX_RETAINED_CAPACITY, "retained {cap} bytes");
340        })
341        .join()
342        .unwrap();
343    }
344
345    #[test]
346    fn test_with_formatted_reentrant_call_uses_temporary_buffer() {
347        let record = Record::new(Metadata::new(Level::Info, "t"), "outer", &[]);
348        let mut inner_text = String::new();
349        with_formatted(&JsonFormat::new(), &record, |outer| {
350            let inner = Record::new(Metadata::new(Level::Info, "t"), "inner", &[]);
351            with_formatted(&JsonFormat::new(), &inner, |s| {
352                inner_text = s.to_owned();
353                Ok(())
354            })
355            .map_err(|e| io::Error::other(e.to_string()))?;
356            assert!(outer.contains("outer"));
357            Ok(())
358        })
359        .unwrap();
360        assert!(inner_text.contains("inner"));
361    }
362
363    #[test]
364    fn test_with_formatted_surfaces_write_and_format_errors() {
365        let record = Record::new(Metadata::new(Level::Info, "t"), "m", &[]);
366        let err = with_formatted(&JsonFormat::new(), &record, |_| Err(io::Error::other("x")))
367            .unwrap_err();
368        assert!(matches!(err, Error::Io(_)));
369
370        struct Broken;
371        impl core::fmt::Display for Broken {
372            fn fmt(&self, _: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
373                Err(core::fmt::Error)
374            }
375        }
376        let fields = [crate::Field::new("b", crate::Value::Display(&Broken))];
377        let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
378        let err = with_formatted(&JsonFormat::new(), &record, |_| Ok(())).unwrap_err();
379        assert!(matches!(err, Error::Format(_)));
380    }
381}