use crate::config::{Format, Level, Log, LogTarget};
use std::ffi::{CStr, CString};
use std::fmt;
use std::fs::{File, OpenOptions};
use std::io::{self, Write};
use std::sync::{Mutex, OnceLock};
static LOGGER: OnceLock<Logger> = OnceLock::new();
struct Logger {
level: Level,
format: Format,
also_stderr: bool,
sink: Mutex<Sink>,
}
enum Sink {
Syslog,
File(File),
Stderr,
}
pub fn init(cfg: &Log) -> io::Result<()> {
let sink = match &cfg.target {
LogTarget::Syslog => {
unsafe { libc::openlog(c"rash".as_ptr(), libc::LOG_PID, libc::LOG_USER) };
Sink::Syslog
}
LogTarget::File(p) => Sink::File(OpenOptions::new().create(true).append(true).open(p)?),
LogTarget::Stderr => Sink::Stderr,
};
let _ = LOGGER.set(Logger {
level: cfg.level,
format: cfg.format,
also_stderr: cfg.also_stderr,
sink: Mutex::new(sink),
});
Ok(())
}
pub fn emit(level: Level, args: fmt::Arguments<'_>) {
let Some(logger) = LOGGER.get() else {
let _ = writeln!(io::stderr(), "rash: {args}");
return;
};
if level > logger.level {
return;
}
let msg = args.to_string();
let mut sink = logger.sink.lock().unwrap_or_else(|e| e.into_inner());
let rendered = line(logger.format, level, &msg);
match &mut *sink {
Sink::Syslog => {
if let Ok(c) = CString::new(msg.as_bytes()) {
unsafe { libc::syslog(level as i32, c"%s".as_ptr(), c.as_ptr()) };
}
}
Sink::File(f) => {
let _ = writeln!(f, "{rendered}");
let _ = f.flush();
}
Sink::Stderr => {
let _ = writeln!(io::stderr(), "{rendered}");
}
}
if logger.also_stderr && !matches!(&*sink, Sink::Stderr) {
let _ = writeln!(io::stderr(), "{rendered}");
}
}
fn line(format: Format, level: Level, msg: &str) -> String {
match format {
Format::Text => format!(
"{} rash[{}]: {msg}",
timestamp(c"%Y/%m/%d %H:%M:%S"),
std::process::id()
),
Format::Json => serde_json::json!({
"ts": timestamp(c"%Y-%m-%dT%H:%M:%S%z"),
"level": level.to_string(),
"pid": std::process::id(),
"msg": msg,
})
.to_string(),
}
}
fn timestamp(fmt: &CStr) -> String {
let mut buf = [0u8; 64];
let n = unsafe {
let now = libc::time(std::ptr::null_mut());
let mut tm: libc::tm = std::mem::zeroed();
if libc::localtime_r(&now, &mut tm).is_null() {
return String::new();
}
libc::strftime(buf.as_mut_ptr().cast(), buf.len(), fmt.as_ptr(), &tm)
};
String::from_utf8_lossy(&buf[..n]).into_owned()
}
#[macro_export]
macro_rules! logmsg {
($level:expr, $($arg:tt)*) => {
$crate::log::emit($level, ::core::format_args!($($arg)*))
};
}
#[macro_export]
macro_rules! log_err {
($($arg:tt)*) => { $crate::logmsg!($crate::config::Level::Err, $($arg)*) };
}
#[macro_export]
macro_rules! log_info {
($($arg:tt)*) => { $crate::logmsg!($crate::config::Level::Info, $($arg)*) };
}
#[macro_export]
macro_rules! log_debug {
($($arg:tt)*) => { $crate::logmsg!($crate::config::Level::Debug, $($arg)*) };
}