use std::fs::{File, OpenOptions};
use std::io::Write;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, OnceLock};
use std::time::Duration;
pub const RWS_CONFIG_ACCESS_LOG: &str = "RWS_CONFIG_ACCESS_LOG";
pub const RWS_CONFIG_ACCESS_LOG_MAX_SIZE: &str = "RWS_CONFIG_ACCESS_LOG_MAX_SIZE";
pub const RWS_CONFIG_ACCESS_LOG_KEEP: &str = "RWS_CONFIG_ACCESS_LOG_KEEP";
static REOPEN: AtomicBool = AtomicBool::new(false);
pub fn reopen() {
REOPEN.store(true, Ordering::Relaxed);
}
pub fn install_reopen_signal() {
if std::env::var(RWS_CONFIG_ACCESS_LOG).map_or(true, |p| p.trim().is_empty()) {
return;
}
#[cfg(all(unix, any(feature = "http1", feature = "http2")))]
{
extern "C" fn on_sigusr1(_: libc::c_int) {
REOPEN.store(true, Ordering::Relaxed);
}
unsafe {
libc::signal(libc::SIGUSR1, on_sigusr1 as *const () as libc::sighandler_t);
}
}
}
pub(crate) struct LogFile {
path: PathBuf,
file: File,
size: u64,
max_size: u64,
keep: u32,
}
impl LogFile {
pub(crate) fn open(path: &Path, max_size: u64, keep: u32) -> std::io::Result<LogFile> {
let file = OpenOptions::new().create(true).append(true).open(path)?;
let size = file.metadata()?.len();
Ok(LogFile { path: path.to_path_buf(), file, size, max_size, keep })
}
pub(crate) fn write(&mut self, bytes: &[u8], reopen: bool) -> std::io::Result<()> {
if reopen {
*self = LogFile::open(&self.path, self.max_size, self.keep)?;
}
if self.max_size > 0 && self.size > 0 && self.size + bytes.len() as u64 > self.max_size {
self.rotate()?;
}
self.file.write_all(bytes)?;
self.size += bytes.len() as u64;
Ok(())
}
fn rotate(&mut self) -> std::io::Result<()> {
let numbered = |n: u32| {
let mut name = self.path.clone().into_os_string();
name.push(format!(".{n}"));
PathBuf::from(name)
};
if self.keep == 0 {
self.file = OpenOptions::new().create(true).write(true).truncate(true).open(&self.path)?;
self.file = OpenOptions::new().append(true).open(&self.path)?;
self.size = 0;
return Ok(());
}
let _ = std::fs::remove_file(numbered(self.keep));
for n in (1..self.keep).rev() {
let _ = std::fs::rename(numbered(n), numbered(n + 1));
}
std::fs::rename(&self.path, numbered(1))?;
*self = LogFile::open(&self.path, self.max_size, self.keep)?;
Ok(())
}
}
enum Destination {
Stdout,
File(Mutex<LogFile>),
}
fn destination() -> &'static Destination {
static DESTINATION: OnceLock<Destination> = OnceLock::new();
DESTINATION.get_or_init(|| {
let path = std::env::var(RWS_CONFIG_ACCESS_LOG).unwrap_or_default();
if path.trim().is_empty() {
return Destination::Stdout;
}
let number = |key: &str, default: u64| std::env::var(key).ok().and_then(|v| parse_size(&v)).unwrap_or(default);
let max_size = number(RWS_CONFIG_ACCESS_LOG_MAX_SIZE, 0);
let keep = number(RWS_CONFIG_ACCESS_LOG_KEEP, 5).min(1000) as u32;
match LogFile::open(Path::new(path.trim()), max_size, keep) {
Ok(file) => Destination::File(Mutex::new(file)),
Err(e) => {
eprintln!("access log: can't open {}: {e}; writing to stdout", path.trim());
Destination::Stdout
}
}
})
}
pub(crate) fn parse_size(value: &str) -> Option<u64> {
let value = value.trim();
let (digits, multiplier) = match value.as_bytes().last()? {
b'k' | b'K' => (&value[..value.len() - 1], 1 << 10),
b'm' | b'M' => (&value[..value.len() - 1], 1 << 20),
b'g' | b'G' => (&value[..value.len() - 1], 1 << 30),
_ => (value, 1),
};
digits.trim().parse::<u64>().ok()?.checked_mul(multiplier)
}
pub(crate) const FLUSH_INTERVAL: Duration = Duration::from_millis(100);
pub(crate) const SHARD_LIMIT: usize = 32 * 1024;
struct Shard(Mutex<Vec<u8>>);
fn registry() -> &'static Mutex<Vec<Arc<Shard>>> {
static REGISTRY: OnceLock<Mutex<Vec<Arc<Shard>>>> = OnceLock::new();
REGISTRY.get_or_init(|| {
let spawned = std::thread::Builder::new()
.name("rws-access-log".to_string())
.spawn(|| loop {
std::thread::sleep(FLUSH_INTERVAL);
flush();
});
if spawned.is_err() {
eprintln!("access log: no flusher thread; lines are written when a buffer fills or its thread exits");
}
#[cfg(all(unix, any(feature = "http1", feature = "http2")))]
{
extern "C" fn flush_at_exit() {
flush();
}
unsafe {
libc::atexit(flush_at_exit);
}
}
Mutex::new(Vec::new())
})
}
struct ShardHandle(Arc<Shard>);
impl Drop for ShardHandle {
fn drop(&mut self) {
let pending = std::mem::take(&mut *self.0 .0.lock().unwrap_or_else(|e| e.into_inner()));
write_out(&pending);
registry().lock().unwrap_or_else(|e| e.into_inner()).retain(|s| !Arc::ptr_eq(s, &self.0));
}
}
thread_local! {
static SHARD: ShardHandle = {
let shard = Arc::new(Shard(Mutex::new(Vec::with_capacity(4096))));
registry().lock().unwrap_or_else(|e| e.into_inner()).push(Arc::clone(&shard));
ShardHandle(shard)
};
}
pub(crate) fn write_line(write: impl FnOnce(&mut Vec<u8>)) {
let mut write = Some(write);
let full = SHARD.try_with(|handle| {
let mut buf = handle.0 .0.lock().unwrap_or_else(|e| e.into_inner());
if let Some(write) = write.take() {
write(&mut buf);
buf.push(b'\n');
}
(buf.len() >= SHARD_LIMIT).then(|| std::mem::take(&mut *buf))
});
match full {
Ok(Some(full)) => write_out(&full),
Ok(None) => {}
Err(_) => {
if let Some(write) = write.take() {
let mut line = Vec::new();
write(&mut line);
line.push(b'\n');
write_out(&line);
}
}
}
}
pub fn flush() {
let shards: Vec<Arc<Shard>> = registry().lock().unwrap_or_else(|e| e.into_inner()).clone();
for shard in shards {
let pending = std::mem::take(&mut *shard.0.lock().unwrap_or_else(|e| e.into_inner()));
write_out(&pending);
}
}
fn write_out(bytes: &[u8]) {
if bytes.is_empty() {
return;
}
match destination() {
Destination::Stdout => write_stdout(bytes),
Destination::File(file) => {
let mut file = file.lock().unwrap_or_else(|e| e.into_inner());
if let Err(e) = file.write(bytes, REOPEN.swap(false, Ordering::Relaxed)) {
eprintln!("access log: {e}");
}
}
}
}
fn write_stdout(bytes: &[u8]) {
let mut out = std::io::stdout().lock();
let _ = out.write_all(bytes);
let _ = out.flush();
}