use ntime::Timestamp;
use std::error::Error;
use std::sync::{Arc, Mutex};
use crate::attributes;
use crate::attributes::value::{ToValue, Value};
use crate::format;
use crate::level::Level;
use crate::queue;
use crate::sink;
use crate::sink::Sink;
static GLOBAL_LOGGER_NEXT_UUID: Mutex<u32> = Mutex::new(0);
pub struct Logger {
id: u32,
depth: sink::LogDepth,
level: Level,
async_writes: bool,
attributes: attributes::Map,
sinks: Vec<Arc<Mutex<Box<dyn Sink + Send>>>>,
parent_sinks: Vec<Arc<Mutex<Box<dyn Sink + Send>>>>,
has_levelless_sinks: bool,
}
impl Logger {
fn next_uuid() -> u32 {
let mut next_id = GLOBAL_LOGGER_NEXT_UUID.lock().unwrap();
let id = *next_id;
*next_id += 1;
id
}
pub fn new() -> Self {
Self {
id: Self::next_uuid(),
depth: 0,
level: Level::Warning,
async_writes: false,
attributes: attributes::Map::new(),
sinks: Vec::new(),
parent_sinks: Vec::new(),
has_levelless_sinks: false,
}
}
pub fn is_root(&self) -> bool {
return self.depth == 0;
}
fn has_sinks(&self) -> bool {
!self.parent_sinks.is_empty() || !self.sinks.is_empty()
}
pub fn level(&self) -> &Level {
return &self.level;
}
pub fn set_level(&mut self, level: Level) -> &mut Self {
self.level = level;
if self.has_sinks() {
self.trace_with("log level updated", [("name", Value::from(level.to_string())), ("new_level", Value::from(level.value()))]);
}
self
}
pub fn set_async(&mut self, async_writes: bool) -> &mut Self {
if async_writes == self.async_writes {
return self;
}
self.async_writes = async_writes;
match self.async_writes {
true => queue::inc_refcount(),
false => queue::dec_refcount(),
};
if self.has_sinks() {
self.trace_with(
if async_writes { "enabled async log updates" } else { "disabled async log updates" },
[("total_async_loggers", queue::refcount().to_value())],
);
}
self
}
pub fn add_sink<T: sink::Sink + Send + 'static>(&mut self, sink: T) -> &mut Self {
let name: String = sink.name().into();
let receives_all_levels = sink.receives_all_levels();
self.sinks.push(Arc::new(Mutex::new(Box::new(sink))));
self.has_levelless_sinks |= receives_all_levels;
self.trace_with(
"added new log sink",
[
("name", Value::from(name)),
("async", Value::from(self.async_writes)),
("logs_all_levels", Value::from(receives_all_levels)),
],
);
self
}
pub fn set<T: ToValue>(&mut self, key: &str, v: T) -> &mut Self {
self.attributes.insert(key, v.to_value());
self
}
pub fn set_value(&mut self, key: &str, val: Value) -> &mut Self {
self.attributes.insert(key, val);
self
}
fn log_with_two<const X: usize, const Y: usize>(&mut self, level: Level, msg: &str, attrs_1: [(&str, Value); X], attrs_2: [(&str, Value); Y]) -> &mut Self {
if !self.has_sinks() {
panic!("tried to log without sinks configured for logger {id}", id = self.id);
}
if !self.has_levelless_sinks && !self.level.covers(&level) {
return self;
}
self.attributes.clear_ephemeral();
for (k, v) in attrs_1 {
self.attributes.insert_ephemeral(k, v);
}
for (k, v) in attrs_2 {
self.attributes.insert_ephemeral(k, v);
}
let update = sink::LogUpdate::new(Timestamp::now(), level, msg.into());
let attrs = &self.attributes;
let panic_msg: Option<String> = if level == Level::Panic { Some(format::as_panic_string(&update, attrs)) } else { None };
for asink in self.parent_sinks.iter().chain(self.sinks.iter()) {
if !self.level.covers(&level) {
if !asink.lock().unwrap().receives_all_levels() {
continue;
}
}
let res = match self.async_writes {
true => {
queue::log(&asink, &update, &attrs);
Ok(())
}
false => asink.lock().unwrap().log(&update, &attrs),
};
if let Err(e) = res {
panic!("failed to log update {update:?} on sink {name} for logger {id}: {e}", name = asink.lock().unwrap().name(), id = self.id);
}
}
if panic_msg.is_some() {
self.flush();
panic!("{}", panic_msg.unwrap());
}
self
}
pub fn log(&mut self, level: Level, msg: &str) -> &mut Self {
self.log_with_two(level, msg, [], [])
}
pub fn log_with<const L: usize>(&mut self, level: Level, msg: &str, attrs: [(&str, Value); L]) -> &mut Self {
self.log_with_two(level, msg, attrs, [])
}
pub fn trace(&mut self, msg: &str) -> &mut Self {
self.trace_with(msg, [])
}
pub fn trace_with<const L: usize>(&mut self, msg: &str, attrs: [(&str, Value); L]) -> &mut Self {
self.log_with_two(Level::Trace, msg, attrs, [(attributes::KEY_LOGGER_ID, Value::from(self.id))])
}
pub fn debug(&mut self, msg: &str) -> &mut Self {
self.log(Level::Debug, msg)
}
pub fn debug_with<const L: usize>(&mut self, msg: &str, attrs: [(&str, Value); L]) -> &mut Self {
self.log_with(Level::Debug, msg, attrs)
}
pub fn info(&mut self, msg: &str) -> &mut Self {
self.log(Level::Info, msg)
}
pub fn info_with<const L: usize>(&mut self, msg: &str, attrs: [(&str, Value); L]) -> &mut Self {
self.log_with(Level::Info, msg, attrs)
}
pub fn warn(&mut self, msg: &str) -> &mut Self {
self.log(Level::Warning, msg)
}
pub fn warn_with<const L: usize>(&mut self, msg: &str, attrs: [(&str, Value); L]) -> &mut Self {
self.log_with(Level::Warning, msg, attrs)
}
pub fn err(&mut self, msg: &str) -> &mut Self {
self.log(Level::Error, msg)
}
pub fn err_with<const L: usize>(&mut self, msg: &str, attrs: [(&str, Value); L]) -> &mut Self {
self.log_with(Level::Error, msg, attrs)
}
pub fn error<T: Error>(&mut self, error: T, msg: &str) -> &mut Self {
self.log_with(Level::Error, msg, [(attributes::KEY_ERROR, error.to_string().to_value())])
}
pub fn error_with<T: Error, const L: usize>(&mut self, error: T, msg: &str, attrs: [(&str, Value); L]) -> &mut Self {
self.log_with_two(Level::Error, msg, attrs, [(attributes::KEY_ERROR, error.to_string().to_value())])
}
pub fn fatal(&mut self, msg: &str) -> &mut Self {
self.log(Level::Fatal, msg)
}
pub fn fatal_with<const L: usize>(&mut self, msg: &str, attrs: [(&str, Value); L]) -> &mut Self {
self.log_with(Level::Fatal, msg, attrs)
}
pub fn panic(&mut self, msg: &str) -> &mut Self {
self.log(Level::Panic, msg)
}
pub fn panic_with<const L: usize>(&mut self, msg: &str, attrs: [(&str, Value); L]) -> &mut Self {
self.log_with(Level::Panic, msg, attrs)
}
pub fn flush(&mut self) -> &Self {
for asink in self.parent_sinks.iter().chain(self.sinks.iter()) {
let res = match self.async_writes {
true => {
queue::flush_sink(&asink);
Ok(())
}
false => asink.lock().unwrap().flush(),
};
if let Err(e) = res {
panic!("failed to flush sink {name} for logger {id}: {e}", name = asink.lock().unwrap().name(), id = self.id);
}
}
self
}
}
impl Clone for Logger {
fn clone(&self) -> Self {
if self.depth >= sink::MAX_LOGDEPTH {
panic!("cannot clone logger {id} with maximum log depth of {max_depth}", max_depth = sink::MAX_LOGDEPTH, id = self.id);
}
let mut parent_sinks: Vec<Arc<Mutex<Box<dyn Sink + Send>>>> = Vec::new();
for s in &self.sinks {
parent_sinks.push(s.clone());
}
for s in &self.parent_sinks {
parent_sinks.push(s.clone());
}
let mut clone = Self {
id: Self::next_uuid(),
depth: self.depth + 1,
level: self.level,
async_writes: false,
attributes: self.attributes.clone(),
sinks: Vec::new(),
parent_sinks: parent_sinks,
has_levelless_sinks: self.has_levelless_sinks,
};
clone.set_async(self.async_writes);
clone
}
}
impl Drop for Logger {
fn drop(&mut self) {
self.flush();
if self.depth == 0 {
queue::flush();
}
self.set_async(false);
}
}
#[cfg(test)]
mod basic {
use crate::sink::MAX_LOGDEPTH;
use super::*;
#[test]
#[should_panic]
fn panic_no_sinks() {
let mut log = Logger::new();
log.set_level(Level::Info).info("this should explode");
}
#[test]
#[should_panic]
fn panic_log_panics() {
let mut log = Logger::new();
log.add_sink(sink::stdout::default()).set_level(Level::Info);
log.info("this should log fine");
log.panic("and this should explode");
}
#[test]
fn set_async_before_sinks() {
let mut log = Logger::new();
log.set_async(true).add_sink(sink::stdout::default());
log.info("this should log fine");
}
#[test]
#[should_panic]
fn max_depth_exceeded() {
let mut log = Logger::new();
for _ in 0..MAX_LOGDEPTH + 1 {
log = log.clone();
}
}
}
#[cfg(test)]
mod formatting {
use super::*;
use ntime;
use std::io::{Error, ErrorKind};
#[test]
fn sync_formatted_output() {
struct TestCase<'t> {
name: &'t str,
out_format: format::OutputFormat,
time_format: ntime::Format,
want: &'t str,
}
let test_cases: [TestCase; _] = [
TestCase {
name: "default stdout",
out_format: format::OutputFormat::Compact,
time_format: ntime::Format::UtcMillisDateTime,
want: "2026-03-04 15:10:15.000 [INF] root test info
2026-03-04 15:10:16.234 [WRN] root test warn
2026-03-04 15:10:17.468 [INF] first test info number=1
2026-03-04 15:10:18.702 [WRN] first test warn number=1
2026-03-04 15:10:19.936 [DBG] first test debug number=1
2026-03-04 15:10:21.170 [ERR] something failed error=\"oh no\" number=1",
},
TestCase {
name: "stdout with timestamps",
out_format: format::OutputFormat::Compact,
time_format: ntime::Format::TimestampNanoseconds,
want: "1772637015000000000 [INF] root test info
1772637016234000000 [WRN] root test warn
1772637017468000000 [INF] first test info number=1
1772637018702000000 [WRN] first test warn number=1
1772637019936000000 [DBG] first test debug number=1
1772637021170000000 [ERR] something failed error=\"oh no\" number=1",
},
TestCase {
name: "JSON stdout",
out_format: format::OutputFormat::Json,
time_format: ntime::Format::UtcDateTime,
want: "{\"time\":\"2026-03-04 15:10:15\",\"level\":\"info\",\"message\":\"root test info\"}
{\"time\":\"2026-03-04 15:10:16\",\"level\":\"warning\",\"message\":\"root test warn\"}
{\"time\":\"2026-03-04 15:10:17\",\"level\":\"info\",\"message\":\"first test info\",\"number\":1}
{\"time\":\"2026-03-04 15:10:18\",\"level\":\"warning\",\"message\":\"first test warn\",\"number\":1}
{\"time\":\"2026-03-04 15:10:19\",\"level\":\"debug\",\"message\":\"first test debug\",\"number\":1}
{\"time\":\"2026-03-04 15:10:21\",\"level\":\"error\",\"message\":\"something failed\",\"error\":\"oh no\",\"number\":1}",
},
TestCase {
name: "JSON stdout with timestamps",
out_format: format::OutputFormat::Json,
time_format: ntime::Format::TimestampMilliseconds,
want: "{\"timestamp\":1772637015000,\"level\":\"info\",\"message\":\"root test info\"}
{\"timestamp\":1772637016234,\"level\":\"warning\",\"message\":\"root test warn\"}
{\"timestamp\":1772637017468,\"level\":\"info\",\"message\":\"first test info\",\"number\":1}
{\"timestamp\":1772637018702,\"level\":\"warning\",\"message\":\"first test warn\",\"number\":1}
{\"timestamp\":1772637019936,\"level\":\"debug\",\"message\":\"first test debug\",\"number\":1}
{\"timestamp\":1772637021170,\"level\":\"error\",\"message\":\"something failed\",\"error\":\"oh no\",\"number\":1}",
},
];
for tc in test_cases {
let got: String;
{
let string_sink = sink::string::String::new(sink::string::StringConfig {
mock_time: true,
formatter_cfg: format::FormatterConfig {
format: tc.out_format,
time_format: tc.time_format,
},
..sink::string::StringConfig::default()
});
let string_sink_output = string_sink.output();
let mut log = Logger::new();
log.add_sink(string_sink).set_level(Level::Info);
log.info("root test info").warn("root test warn").debug("root test debug");
let mut nlog = log.clone();
nlog.set_level(Level::Debug).set("number", 1);
nlog.info("first test info")
.warn("first test warn")
.debug("first test debug")
.trace("trace log to be ignored")
.error(Error::new(ErrorKind::NotFound, "oh no"), "something failed");
got = string_sink_output.lock().unwrap().clone();
}
assert_eq!(got, tc.want, "{}", tc.name);
}
}
}