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 rotating;
167mod stdio;
168mod writer;
169
170pub use self::file::{FileSink, FileSinkBuilder};
171pub(crate) use self::non_blocking::on_background_worker;
172pub use self::non_blocking::{NonBlocking, NonBlockingBuilder, Overflow};
173pub use self::rotating::{RotatingFile, RotatingFileBuilder, Rotation};
174pub use self::stdio::{StderrSink, StdoutSink};
175pub use self::writer::{NullSink, WriterSink};
176
177/// A [`WriterSink`] over a [`RotatingFile`].
178///
179/// # Examples
180///
181#[cfg_attr(all(feature = "json"), doc = "```no_run")]
182#[cfg_attr(not(all(feature = "json")), doc = "```ignore")]
183/// use log_io::format::JsonFormat;
184/// use log_io::sink::{RotatingFile, RotatingFileSink, Rotation};
185///
186/// let file = RotatingFile::builder("logs/app.jsonl")
187/// .max_bytes(10 * 1024 * 1024)
188/// .rotation(Rotation::Daily)
189/// .max_files(7)
190/// .open()?;
191/// let sink: RotatingFileSink<JsonFormat> = RotatingFileSink::new(file, JsonFormat::new());
192/// # Ok::<(), std::io::Error>(())
193/// ```
194pub type RotatingFileSink<F> = WriterSink<RotatingFile, F>;
195
196/// Keeps a sink's lock on its own cache lines.
197///
198/// Every logging thread reads the logger's sink table on each record,
199/// and every thread writes the sink's mutex. If a small sink allocation
200/// shares a cache line with other hot data, each lock and unlock
201/// invalidates that line for all threads. Measured on the reference
202/// machine, padding restored the 4-thread throughput of 1.0.
203#[repr(align(128))]
204pub(crate) struct CachePadded<T>(pub(crate) T);
205
206impl<T> core::ops::Deref for CachePadded<T> {
207 type Target = T;
208 fn deref(&self) -> &T {
209 &self.0
210 }
211}
212
213/// Capacity a thread-local format buffer may keep between records.
214pub(crate) const MAX_RETAINED_CAPACITY: usize = 64 * 1024;
215
216thread_local! {
217 /// Reusable formatting buffer, so steady-state formatting does not
218 /// touch the allocator.
219 static FORMAT_BUF: RefCell<String> = const { RefCell::new(String::new()) };
220}
221
222/// Shrink `buf` back to [`MAX_RETAINED_CAPACITY`] after an oversized
223/// record. Cheap when no shrinking is needed: one comparison.
224#[inline]
225pub(crate) fn trim_buffer(buf: &mut String) {
226 if buf.capacity() > MAX_RETAINED_CAPACITY {
227 buf.clear();
228 buf.shrink_to(MAX_RETAINED_CAPACITY);
229 }
230}
231
232/// Format `record` into the thread-local buffer and pass the text to
233/// `write`.
234///
235/// If the buffer is already in use on this thread (a sink that logs
236/// from inside its own write) or thread-local storage is being torn
237/// down, a temporary buffer is used instead.
238pub(crate) fn with_formatted<F, W>(format: &F, record: &Record<'_>, mut write: W) -> Result<()>
239where
240 F: Format,
241 W: FnMut(&str) -> io::Result<()>,
242{
243 let pooled = FORMAT_BUF.try_with(|cell| {
244 let mut buf = cell.try_borrow_mut().ok()?;
245 Some(format_into(&mut buf, format, record, &mut write))
246 });
247 match pooled {
248 Ok(Some(result)) => result,
249 _ => format_into(&mut String::with_capacity(256), format, record, &mut write),
250 }
251}
252
253fn format_into<F, W>(buf: &mut String, format: &F, record: &Record<'_>, write: &mut W) -> Result<()>
254where
255 F: Format,
256 W: FnMut(&str) -> io::Result<()>,
257{
258 buf.clear();
259 let result = format
260 .write_record_generic(record, buf)
261 .map_err(Error::Format)
262 .and_then(|()| write(buf).map_err(Error::Io));
263 trim_buffer(buf);
264 result
265}
266
267/// Acquire a mutex guard even if the mutex is poisoned.
268///
269/// Poisoning only means a previous holder unwound; the writer inside is
270/// still usable, and logging must keep working after a panic.
271pub(crate) fn lock_recover<T: ?Sized>(m: &Mutex<T>) -> MutexGuard<'_, T> {
272 m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
273}
274
275/// Format `record` and write it to the writer behind `target`.
276pub(crate) fn write_locked<W: Write + ?Sized, F: Format>(
277 target: &Mutex<W>,
278 format: &F,
279 record: &Record<'_>,
280) -> Result<()> {
281 with_formatted(format, record, |text| {
282 lock_recover(target).write_all(text.as_bytes())
283 })
284}
285
286/// Flush the writer behind `target`.
287pub(crate) fn flush_locked<W: Write + ?Sized>(target: &Mutex<W>) -> Result<()> {
288 lock_recover(target).flush().map_err(Error::from)
289}
290
291#[cfg(all(test, feature = "json"))]
292mod tests {
293 use super::*;
294 use crate::format::JsonFormat;
295 use crate::{Level, Metadata};
296
297 #[test]
298 fn test_trim_buffer_caps_retained_capacity() {
299 let mut buf = String::with_capacity(4 * MAX_RETAINED_CAPACITY);
300 buf.push('x');
301 trim_buffer(&mut buf);
302 assert!(buf.capacity() <= MAX_RETAINED_CAPACITY);
303 let mut small = String::with_capacity(128);
304 small.push('y');
305 trim_buffer(&mut small);
306 assert_eq!(small, "y", "small buffers are left alone");
307 }
308
309 #[test]
310 fn test_thread_local_buffer_shrinks_after_oversize_record() {
311 // The audit's LOG-007 reproduction, scaled down: one 1 MiB
312 // record must not leave a 1 MiB+ buffer behind.
313 std::thread::spawn(|| {
314 let big = "x".repeat(1024 * 1024);
315 let record = Record::new(Metadata::new(Level::Info, "t"), &big, &[]);
316 let mut written = 0;
317 with_formatted(&JsonFormat::new(), &record, |s| {
318 written = s.len();
319 Ok(())
320 })
321 .unwrap();
322 assert!(written > big.len());
323 let cap = FORMAT_BUF.with(|b| b.borrow().capacity());
324 assert!(cap <= MAX_RETAINED_CAPACITY, "retained {cap} bytes");
325 })
326 .join()
327 .unwrap();
328 }
329
330 #[test]
331 fn test_with_formatted_reentrant_call_uses_temporary_buffer() {
332 let record = Record::new(Metadata::new(Level::Info, "t"), "outer", &[]);
333 let mut inner_text = String::new();
334 with_formatted(&JsonFormat::new(), &record, |outer| {
335 let inner = Record::new(Metadata::new(Level::Info, "t"), "inner", &[]);
336 with_formatted(&JsonFormat::new(), &inner, |s| {
337 inner_text = s.to_owned();
338 Ok(())
339 })
340 .map_err(|e| io::Error::other(e.to_string()))?;
341 assert!(outer.contains("outer"));
342 Ok(())
343 })
344 .unwrap();
345 assert!(inner_text.contains("inner"));
346 }
347
348 #[test]
349 fn test_with_formatted_surfaces_write_and_format_errors() {
350 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &[]);
351 let err = with_formatted(&JsonFormat::new(), &record, |_| Err(io::Error::other("x")))
352 .unwrap_err();
353 assert!(matches!(err, Error::Io(_)));
354
355 struct Broken;
356 impl core::fmt::Display for Broken {
357 fn fmt(&self, _: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
358 Err(core::fmt::Error)
359 }
360 }
361 let fields = [crate::Field::new("b", crate::Value::Display(&Broken))];
362 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
363 let err = with_formatted(&JsonFormat::new(), &record, |_| Ok(())).unwrap_err();
364 assert!(matches!(err, Error::Format(_)));
365 }
366}