use core::cell::RefCell;
use std::io::{self, Write};
use std::sync::{Mutex, MutexGuard};
use crate::error::{Error, Result};
use crate::format::Format;
use crate::record::Record;
#[cfg_attr(all(feature = "json"), doc = "```")]
#[cfg_attr(not(all(feature = "json")), doc = "```ignore")]
pub trait Sink: Send + Sync {
fn write_record(&self, record: &Record<'_>) -> Result<()>;
fn flush(&self) -> Result<()>;
fn request_flush(&self) -> Result<()> {
self.flush()
}
fn shutdown(&self) -> Result<()> {
self.flush()
}
}
impl<S: Sink + ?Sized> Sink for std::sync::Arc<S> {
fn write_record(&self, record: &Record<'_>) -> Result<()> {
(**self).write_record(record)
}
fn flush(&self) -> Result<()> {
(**self).flush()
}
fn request_flush(&self) -> Result<()> {
(**self).request_flush()
}
fn shutdown(&self) -> Result<()> {
(**self).shutdown()
}
}
impl<S: Sink + ?Sized> Sink for Box<S> {
fn write_record(&self, record: &Record<'_>) -> Result<()> {
(**self).write_record(record)
}
fn flush(&self) -> Result<()> {
(**self).flush()
}
fn request_flush(&self) -> Result<()> {
(**self).request_flush()
}
fn shutdown(&self) -> Result<()> {
(**self).shutdown()
}
}
mod file;
mod non_blocking;
mod recent;
mod rotating;
mod stdio;
mod writer;
pub use self::file::{FileSink, FileSinkBuilder};
pub(crate) use self::non_blocking::on_background_worker;
pub use self::non_blocking::{NonBlocking, NonBlockingBuilder, Overflow};
pub use self::recent::{DEFAULT_LINE_LIMIT, RecentLines};
pub use self::rotating::{RotatingFile, RotatingFileBuilder, Rotation};
pub use self::stdio::{StderrSink, StdoutSink};
pub use self::writer::{NullSink, WriterSink};
#[cfg_attr(all(feature = "json"), doc = "```no_run")]
#[cfg_attr(not(all(feature = "json")), doc = "```ignore")]
pub type RotatingFileSink<F> = WriterSink<RotatingFile, F>;
#[repr(align(128))]
pub(crate) struct CachePadded<T>(pub(crate) T);
impl<T> core::ops::Deref for CachePadded<T> {
type Target = T;
fn deref(&self) -> &T {
&self.0
}
}
pub(crate) const MAX_RETAINED_CAPACITY: usize = 64 * 1024;
thread_local! {
static FORMAT_BUF: RefCell<String> = const { RefCell::new(String::new()) };
}
#[inline]
pub(crate) fn trim_buffer(buf: &mut String) {
if buf.capacity() > MAX_RETAINED_CAPACITY {
buf.clear();
buf.shrink_to(MAX_RETAINED_CAPACITY);
}
}
pub(crate) fn with_formatted<F, W>(format: &F, record: &Record<'_>, mut write: W) -> Result<()>
where
F: Format,
W: FnMut(&str) -> io::Result<()>,
{
let pooled = FORMAT_BUF.try_with(|cell| {
let mut buf = cell.try_borrow_mut().ok()?;
Some(format_into(&mut buf, format, record, &mut write))
});
match pooled {
Ok(Some(result)) => result,
_ => format_into(&mut String::with_capacity(256), format, record, &mut write),
}
}
fn format_into<F, W>(buf: &mut String, format: &F, record: &Record<'_>, write: &mut W) -> Result<()>
where
F: Format,
W: FnMut(&str) -> io::Result<()>,
{
buf.clear();
let result = format
.write_record_generic(record, buf)
.map_err(Error::Format)
.and_then(|()| write(buf).map_err(Error::Io));
trim_buffer(buf);
result
}
pub(crate) const DEFAULT_FILE_MODE: u32 = 0o600;
pub(crate) fn apply_mode(options: &mut std::fs::OpenOptions, mode: u32) {
#[cfg(unix)]
{
use std::os::unix::fs::OpenOptionsExt;
let _options = options.mode(mode);
}
#[cfg(not(unix))]
let _unused = (options, mode);
}
#[cfg(any(target_os = "linux", target_os = "android"))]
const O_NOFOLLOW: Option<i32> = Some(
if cfg!(any(
target_arch = "arm",
target_arch = "aarch64",
target_arch = "powerpc",
target_arch = "powerpc64",
target_arch = "m68k"
)) {
0x8000
} else {
0x2_0000
},
);
#[cfg(any(
target_vendor = "apple",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly"
))]
const O_NOFOLLOW: Option<i32> = Some(0x100);
#[cfg(any(target_os = "solaris", target_os = "illumos"))]
const O_NOFOLLOW: Option<i32> = Some(0x2_0000);
#[cfg(all(
unix,
not(any(
target_os = "linux",
target_os = "android",
target_vendor = "apple",
target_os = "freebsd",
target_os = "netbsd",
target_os = "openbsd",
target_os = "dragonfly",
target_os = "solaris",
target_os = "illumos"
))
))]
const O_NOFOLLOW: Option<i32> = None;
fn refuse(message: &'static str) -> io::Error {
io::Error::new(io::ErrorKind::InvalidInput, message)
}
pub(crate) fn open_log_file(
path: &std::path::Path,
append: bool,
mode: Option<u32>,
follow_symlinks: bool,
) -> io::Result<std::fs::File> {
let mut options = std::fs::OpenOptions::new();
let _options = options.create(true).write(true).append(append);
apply_mode(&mut options, mode.unwrap_or(DEFAULT_FILE_MODE));
if !follow_symlinks {
if std::fs::symlink_metadata(path).is_ok_and(|m| m.file_type().is_symlink()) {
return Err(refuse(
"log file path is a symbolic link; set follow_symlinks(true) to allow it",
));
}
#[cfg(unix)]
if let Some(flag) = O_NOFOLLOW {
use std::os::unix::fs::OpenOptionsExt;
let _options = options.custom_flags(flag);
}
#[cfg(windows)]
{
use std::os::windows::fs::OpenOptionsExt;
const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
let _options = options.custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
}
}
let file = options.open(path)?;
if !follow_symlinks {
let opened = file.metadata()?;
if !opened.file_type().is_file() {
return Err(refuse("log file path is not a regular file"));
}
let named = std::fs::symlink_metadata(path)?;
if named.file_type().is_symlink() {
return Err(refuse(
"log file path is a symbolic link; set follow_symlinks(true) to allow it",
));
}
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
if (named.dev(), named.ino()) != (opened.dev(), opened.ino()) {
return Err(refuse(
"log file path was replaced while it was being opened",
));
}
}
}
if !append {
file.set_len(0)?;
}
Ok(file)
}
pub(crate) fn lock_recover<T: ?Sized>(m: &Mutex<T>) -> MutexGuard<'_, T> {
m.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
}
pub(crate) fn write_locked<W: Write + ?Sized, F: Format>(
target: &Mutex<W>,
format: &F,
record: &Record<'_>,
) -> Result<()> {
with_formatted(format, record, |text| {
lock_recover(target).write_all(text.as_bytes())
})
}
pub(crate) fn flush_locked<W: Write + ?Sized>(target: &Mutex<W>) -> Result<()> {
lock_recover(target).flush().map_err(Error::from)
}
#[cfg(all(test, feature = "json"))]
mod tests {
use super::*;
use crate::format::JsonFormat;
use crate::{Level, Metadata};
#[test]
fn test_trim_buffer_caps_retained_capacity() {
let mut buf = String::with_capacity(4 * MAX_RETAINED_CAPACITY);
buf.push('x');
trim_buffer(&mut buf);
assert!(buf.capacity() <= MAX_RETAINED_CAPACITY);
let mut small = String::with_capacity(128);
small.push('y');
trim_buffer(&mut small);
assert_eq!(small, "y", "small buffers are left alone");
}
#[test]
fn test_thread_local_buffer_shrinks_after_oversize_record() {
std::thread::spawn(|| {
let big = "x".repeat(1024 * 1024);
let record = Record::new(Metadata::new(Level::Info, "t"), &big, &[]);
let mut written = 0;
with_formatted(&JsonFormat::new(), &record, |s| {
written = s.len();
Ok(())
})
.unwrap();
assert!(written > big.len());
let cap = FORMAT_BUF.with(|b| b.borrow().capacity());
assert!(cap <= MAX_RETAINED_CAPACITY, "retained {cap} bytes");
})
.join()
.unwrap();
}
#[test]
fn test_with_formatted_reentrant_call_uses_temporary_buffer() {
let record = Record::new(Metadata::new(Level::Info, "t"), "outer", &[]);
let mut inner_text = String::new();
with_formatted(&JsonFormat::new(), &record, |outer| {
let inner = Record::new(Metadata::new(Level::Info, "t"), "inner", &[]);
with_formatted(&JsonFormat::new(), &inner, |s| {
inner_text = s.to_owned();
Ok(())
})
.map_err(|e| io::Error::other(e.to_string()))?;
assert!(outer.contains("outer"));
Ok(())
})
.unwrap();
assert!(inner_text.contains("inner"));
}
#[test]
fn test_with_formatted_surfaces_write_and_format_errors() {
let record = Record::new(Metadata::new(Level::Info, "t"), "m", &[]);
let err = with_formatted(&JsonFormat::new(), &record, |_| Err(io::Error::other("x")))
.unwrap_err();
assert!(matches!(err, Error::Io(_)));
struct Broken;
impl core::fmt::Display for Broken {
fn fmt(&self, _: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
Err(core::fmt::Error)
}
}
let fields = [crate::Field::new("b", crate::Value::Display(&Broken))];
let record = Record::new(Metadata::new(Level::Info, "t"), "m", &fields);
let err = with_formatted(&JsonFormat::new(), &record, |_| Ok(())).unwrap_err();
assert!(matches!(err, Error::Format(_)));
}
}