1use crate::config::{Format, Level, Log, LogTarget};
12use std::ffi::{CStr, CString};
13use std::fmt;
14use std::fs::{File, OpenOptions};
15use std::io::{self, Write};
16use std::sync::{Mutex, OnceLock};
17
18static LOGGER: OnceLock<Logger> = OnceLock::new();
19
20struct Logger {
21 level: Level,
22 format: Format,
23 also_stderr: bool,
24 sink: Mutex<Sink>,
25}
26
27enum Sink {
28 Syslog,
29 File(File),
30 Stderr,
31}
32
33pub fn init(cfg: &Log) -> io::Result<()> {
35 let sink = match &cfg.target {
36 LogTarget::Syslog => {
37 unsafe { libc::openlog(c"rash".as_ptr(), libc::LOG_PID, libc::LOG_USER) };
41 Sink::Syslog
42 }
43 LogTarget::File(p) => Sink::File(OpenOptions::new().create(true).append(true).open(p)?),
44 LogTarget::Stderr => Sink::Stderr,
45 };
46
47 let _ = LOGGER.set(Logger {
48 level: cfg.level,
49 format: cfg.format,
50 also_stderr: cfg.also_stderr,
51 sink: Mutex::new(sink),
52 });
53 Ok(())
54}
55
56pub fn emit(level: Level, args: fmt::Arguments<'_>) {
60 let Some(logger) = LOGGER.get() else {
61 let _ = writeln!(io::stderr(), "rash: {args}");
64 return;
65 };
66
67 if level > logger.level {
70 return;
71 }
72
73 let msg = args.to_string();
74 let mut sink = logger.sink.lock().unwrap_or_else(|e| e.into_inner());
77
78 let rendered = line(logger.format, level, &msg);
82
83 match &mut *sink {
84 Sink::Syslog => {
85 if let Ok(c) = CString::new(msg.as_bytes()) {
86 unsafe { libc::syslog(level as i32, c"%s".as_ptr(), c.as_ptr()) };
90 }
91 }
92 Sink::File(f) => {
93 let _ = writeln!(f, "{rendered}");
94 let _ = f.flush();
95 }
96 Sink::Stderr => {
97 let _ = writeln!(io::stderr(), "{rendered}");
98 }
99 }
100
101 if logger.also_stderr && !matches!(&*sink, Sink::Stderr) {
103 let _ = writeln!(io::stderr(), "{rendered}");
104 }
105}
106
107fn line(format: Format, level: Level, msg: &str) -> String {
112 match format {
113 Format::Text => format!(
114 "{} rash[{}]: {msg}",
115 timestamp(c"%Y/%m/%d %H:%M:%S"),
116 std::process::id()
117 ),
118 Format::Json => serde_json::json!({
119 "ts": timestamp(c"%Y-%m-%dT%H:%M:%S%z"),
120 "level": level.to_string(),
121 "pid": std::process::id(),
122 "msg": msg,
123 })
124 .to_string(),
125 }
126}
127
128fn timestamp(fmt: &CStr) -> String {
131 let mut buf = [0u8; 64];
132
133 let n = unsafe {
137 let now = libc::time(std::ptr::null_mut());
138 let mut tm: libc::tm = std::mem::zeroed();
139 if libc::localtime_r(&now, &mut tm).is_null() {
140 return String::new();
141 }
142 libc::strftime(buf.as_mut_ptr().cast(), buf.len(), fmt.as_ptr(), &tm)
143 };
144
145 String::from_utf8_lossy(&buf[..n]).into_owned()
146}
147
148#[macro_export]
149macro_rules! logmsg {
150 ($level:expr, $($arg:tt)*) => {
151 $crate::log::emit($level, ::core::format_args!($($arg)*))
152 };
153}
154
155#[macro_export]
156macro_rules! log_err {
157 ($($arg:tt)*) => { $crate::logmsg!($crate::config::Level::Err, $($arg)*) };
158}
159
160#[macro_export]
161macro_rules! log_info {
162 ($($arg:tt)*) => { $crate::logmsg!($crate::config::Level::Info, $($arg)*) };
163}
164
165#[macro_export]
166macro_rules! log_debug {
167 ($($arg:tt)*) => { $crate::logmsg!($crate::config::Level::Debug, $($arg)*) };
168}