1use 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#[cfg_attr(all(feature = "json"), doc = "```")]
53#[cfg_attr(not(all(feature = "json")), doc = "```ignore")]
54pub trait Sink: Send + Sync {
80 fn write_record(&self, record: &Record<'_>) -> Result<()>;
87
88 fn flush(&self) -> Result<()>;
96
97 fn request_flush(&self) -> Result<()> {
114 self.flush()
115 }
116
117 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#[cfg_attr(all(feature = "json"), doc = "```no_run")]
184#[cfg_attr(not(all(feature = "json")), doc = "```ignore")]
185pub type RotatingFileSink<F> = WriterSink<RotatingFile, F>;
197
198#[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
215pub(crate) const MAX_RETAINED_CAPACITY: usize = 64 * 1024;
217
218thread_local! {
219 static FORMAT_BUF: RefCell<String> = const { RefCell::new(String::new()) };
222}
223
224#[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
234pub(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
269pub(crate) const DEFAULT_FILE_MODE: u32 = 0o600;
273
274pub(crate) fn apply_mode(options: &mut std::fs::OpenOptions, mode: u32) {
278 #[cfg(unix)]
279 {
280 use std::os::unix::fs::OpenOptionsExt;
281 let _options = options.mode(mode);
282 }
283 #[cfg(not(unix))]
284 let _unused = (options, mode);
285}
286
287#[cfg(any(target_os = "linux", target_os = "android"))]
292const O_NOFOLLOW: Option<i32> = Some(
293 if cfg!(any(
294 target_arch = "arm",
295 target_arch = "aarch64",
296 target_arch = "powerpc",
297 target_arch = "powerpc64",
298 target_arch = "m68k"
299 )) {
300 0x8000
301 } else {
302 0x2_0000
303 },
304);
305#[cfg(any(
306 target_vendor = "apple",
307 target_os = "freebsd",
308 target_os = "netbsd",
309 target_os = "openbsd",
310 target_os = "dragonfly"
311))]
312const O_NOFOLLOW: Option<i32> = Some(0x100);
313#[cfg(any(target_os = "solaris", target_os = "illumos"))]
314const O_NOFOLLOW: Option<i32> = Some(0x2_0000);
315#[cfg(all(
316 unix,
317 not(any(
318 target_os = "linux",
319 target_os = "android",
320 target_vendor = "apple",
321 target_os = "freebsd",
322 target_os = "netbsd",
323 target_os = "openbsd",
324 target_os = "dragonfly",
325 target_os = "solaris",
326 target_os = "illumos"
327 ))
328))]
329const O_NOFOLLOW: Option<i32> = None;
330
331fn refuse(message: &'static str) -> io::Error {
332 io::Error::new(io::ErrorKind::InvalidInput, message)
333}
334
335pub(crate) fn open_log_file(
347 path: &std::path::Path,
348 append: bool,
349 mode: Option<u32>,
350 follow_symlinks: bool,
351) -> io::Result<std::fs::File> {
352 let mut options = std::fs::OpenOptions::new();
353 let _options = options.create(true).write(true).append(append);
354 apply_mode(&mut options, mode.unwrap_or(DEFAULT_FILE_MODE));
355 if !follow_symlinks {
356 if std::fs::symlink_metadata(path).is_ok_and(|m| m.file_type().is_symlink()) {
357 return Err(refuse(
358 "log file path is a symbolic link; set follow_symlinks(true) to allow it",
359 ));
360 }
361 #[cfg(unix)]
362 if let Some(flag) = O_NOFOLLOW {
363 use std::os::unix::fs::OpenOptionsExt;
364 let _options = options.custom_flags(flag);
365 }
366 #[cfg(windows)]
367 {
368 use std::os::windows::fs::OpenOptionsExt;
369 const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
371 let _options = options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
372 }
373 }
374 let file = options.open(path)?;
375 if !follow_symlinks {
376 let opened = file.metadata()?;
377 if !opened.file_type().is_file() {
378 return Err(refuse("log file path is not a regular file"));
379 }
380 let named = std::fs::symlink_metadata(path)?;
381 if named.file_type().is_symlink() {
382 return Err(refuse(
383 "log file path is a symbolic link; set follow_symlinks(true) to allow it",
384 ));
385 }
386 #[cfg(unix)]
387 {
388 use std::os::unix::fs::MetadataExt;
389 if (named.dev(), named.ino()) != (opened.dev(), opened.ino()) {
390 return Err(refuse(
391 "log file path was replaced while it was being opened",
392 ));
393 }
394 }
395 }
396 if !append {
397 file.set_len(0)?;
398 }
399 Ok(file)
400}
401
402pub(crate) fn lock_recover<T: ?Sized>(m: &Mutex<T>) -> MutexGuard<'_, T> {
407 m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
408}
409
410pub(crate) fn write_locked<W: Write + ?Sized, F: Format>(
412 target: &Mutex<W>,
413 format: &F,
414 record: &Record<'_>,
415) -> Result<()> {
416 with_formatted(format, record, |text| {
417 lock_recover(target).write_all(text.as_bytes())
418 })
419}
420
421pub(crate) fn flush_locked<W: Write + ?Sized>(target: &Mutex<W>) -> Result<()> {
423 lock_recover(target).flush().map_err(Error::from)
424}
425
426#[cfg(all(test, feature = "json"))]
427mod tests {
428 use super::*;
429 use crate::format::JsonFormat;
430 use crate::{Level, Metadata};
431
432 #[test]
433 fn test_trim_buffer_caps_retained_capacity() {
434 let mut buf = String::with_capacity(4 * MAX_RETAINED_CAPACITY);
435 buf.push('x');
436 trim_buffer(&mut buf);
437 assert!(buf.capacity() <= MAX_RETAINED_CAPACITY);
438 let mut small = String::with_capacity(128);
439 small.push('y');
440 trim_buffer(&mut small);
441 assert_eq!(small, "y", "small buffers are left alone");
442 }
443
444 #[test]
445 fn test_thread_local_buffer_shrinks_after_oversize_record() {
446 std::thread::spawn(|| {
449 let big = "x".repeat(1024 * 1024);
450 let record = Record::new(Metadata::new(Level::Info, "t"), &big, &[]);
451 let mut written = 0;
452 with_formatted(&JsonFormat::new(), &record, |s| {
453 written = s.len();
454 Ok(())
455 })
456 .unwrap();
457 assert!(written > big.len());
458 let cap = FORMAT_BUF.with(|b| b.borrow().capacity());
459 assert!(cap <= MAX_RETAINED_CAPACITY, "retained {cap} bytes");
460 })
461 .join()
462 .unwrap();
463 }
464
465 #[test]
466 fn test_with_formatted_reentrant_call_uses_temporary_buffer() {
467 let record = Record::new(Metadata::new(Level::Info, "t"), "outer", &[]);
468 let mut inner_text = String::new();
469 with_formatted(&JsonFormat::new(), &record, |outer| {
470 let inner = Record::new(Metadata::new(Level::Info, "t"), "inner", &[]);
471 with_formatted(&JsonFormat::new(), &inner, |s| {
472 inner_text = s.to_owned();
473 Ok(())
474 })
475 .map_err(|e| io::Error::other(e.to_string()))?;
476 assert!(outer.contains("outer"));
477 Ok(())
478 })
479 .unwrap();
480 assert!(inner_text.contains("inner"));
481 }
482
483 #[test]
484 fn test_with_formatted_surfaces_write_and_format_errors() {
485 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &[]);
486 let err = with_formatted(&JsonFormat::new(), &record, |_| Err(io::Error::other("x")))
487 .unwrap_err();
488 assert!(matches!(err, Error::Io(_)));
489
490 struct Broken;
491 impl core::fmt::Display for Broken {
492 fn fmt(&self, _: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
493 Err(core::fmt::Error)
494 }
495 }
496 let fields = [crate::Field::new("b", crate::Value::Display(&Broken))];
497 let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
498 let err = with_formatted(&JsonFormat::new(), &record, |_| Ok(())).unwrap_err();
499 assert!(matches!(err, Error::Format(_)));
500 }
501}