use alloc::string::ToString;
use alloc::vec::Vec;
use core::fmt::Display;
use hashbrown::HashMap;
use serde::Serialize;
use serde::de::DeserializeOwned;
#[cfg(std_io)]
use std::{
fs::{File, OpenOptions},
io::{BufWriter, Write},
path::PathBuf,
};
#[cfg(feature = "std")]
use std::{eprintln, println};
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
#[serde(bound = "")]
pub struct LoggerConfig<L: LogLevel> {
#[serde(default)]
#[cfg(std_io)]
pub file: Option<PathBuf>,
#[serde(default = "append_default")]
pub append: bool,
#[serde(default)]
pub stdout: bool,
#[serde(default)]
pub stderr: bool,
#[serde(default)]
pub log: Option<LogCrateLevel>,
#[serde(default)]
pub level: L,
}
impl<L: LogLevel> Default for LoggerConfig<L> {
fn default() -> Self {
Self {
#[cfg(std_io)]
file: None,
append: true,
stdout: false,
stderr: false,
log: None,
level: L::default(),
}
}
}
#[derive(
Clone, Copy, Debug, Default, serde::Serialize, serde::Deserialize, Hash, PartialEq, Eq,
)]
pub enum LogCrateLevel {
#[default]
#[serde(rename = "info")]
Info,
#[serde(rename = "debug")]
Debug,
#[serde(rename = "trace")]
Trace,
}
fn append_default() -> bool {
true
}
pub trait LogLevel:
DeserializeOwned + Serialize + Clone + Copy + core::fmt::Debug + Default
{
}
impl LogLevel for u32 {}
#[derive(Debug, Default)]
pub struct LoggerSinks {
loggers: Vec<LoggerKind>,
logger2index: HashMap<LoggerId, usize>,
}
impl LoggerSinks {
pub fn new() -> Self {
Self::default()
}
pub fn register<L: LogLevel>(&mut self, config: &LoggerConfig<L>) -> Vec<usize> {
let mut indices = Vec::new();
#[cfg(std_io)]
if let Some(file) = &config.file {
self.insert(&mut indices, LoggerId::File(file.clone()), || {
LoggerKind::File(FileLogger::new(file, config.append))
});
}
#[cfg(feature = "std")]
if config.stdout {
self.insert(&mut indices, LoggerId::Stdout, || LoggerKind::Stdout);
}
#[cfg(feature = "std")]
if config.stderr {
self.insert(&mut indices, LoggerId::Stderr, || LoggerKind::Stderr);
}
if let Some(level) = config.log {
self.insert(&mut indices, LoggerId::LogCrate(level), || {
LoggerKind::Log(level)
});
}
indices
}
pub fn log<S: Display>(&mut self, indices: &[usize], msg: &S) {
match indices.len() {
0 => {}
1 => self.loggers[indices[0]].log(msg),
_ => {
let msg = msg.to_string();
for &index in indices {
self.loggers[index].log(&msg);
}
}
}
}
fn insert<F: FnOnce() -> LoggerKind>(
&mut self,
indices: &mut Vec<usize>,
id: LoggerId,
make: F,
) {
if let Some(index) = self.logger2index.get(&id) {
indices.push(*index);
} else {
let index = self.loggers.len();
self.loggers.push(make());
self.logger2index.insert(id, index);
indices.push(index);
}
}
}
#[derive(Debug, Hash, PartialEq, Eq)]
enum LoggerId {
#[cfg(std_io)]
File(PathBuf),
#[cfg(feature = "std")]
Stdout,
#[cfg(feature = "std")]
Stderr,
LogCrate(LogCrateLevel),
}
#[derive(Debug)]
enum LoggerKind {
#[cfg(std_io)]
File(FileLogger),
#[cfg(feature = "std")]
Stdout,
#[cfg(feature = "std")]
Stderr,
Log(LogCrateLevel),
}
impl LoggerKind {
fn log<S: Display>(&mut self, msg: &S) {
match self {
#[cfg(std_io)]
LoggerKind::File(file_logger) => file_logger.log(msg),
#[cfg(feature = "std")]
LoggerKind::Stdout => println!("{msg}"),
#[cfg(feature = "std")]
LoggerKind::Stderr => eprintln!("{msg}"),
LoggerKind::Log(level) => match level {
LogCrateLevel::Info => log::info!("{msg}"),
LogCrateLevel::Debug => log::debug!("{msg}"),
LogCrateLevel::Trace => log::trace!("{msg}"),
},
}
}
}
#[cfg(std_io)]
#[derive(Debug)]
struct FileLogger {
writer: BufWriter<File>,
}
#[cfg(std_io)]
impl FileLogger {
fn new(path: &PathBuf, append: bool) -> Self {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).unwrap();
}
let file = OpenOptions::new()
.write(true)
.append(append)
.create(true)
.open(path)
.unwrap();
Self {
writer: BufWriter::new(file),
}
}
fn log<S: Display>(&mut self, msg: &S) {
writeln!(self.writer, "{msg}").expect("Should be able to log debug information.");
self.writer.flush().expect("Can complete write operation.");
}
}