Added zutil-logger.

This commit is contained in:
Filipe Rodrigues 2025-09-26 00:43:18 +01:00
parent 168a0032aa
commit 2913756373
Signed by: zenithsiz
SSH Key Fingerprint: SHA256:Mb5ppb3Sh7IarBO/sBTXLHbYEOz37hJAlslLQPPAPaU
6 changed files with 430 additions and 1 deletions

View File

@ -1,6 +1,6 @@
[workspace]
members = ["zutil-async-loadable", "zutil-cloned"]
members = ["zutil-async-loadable", "zutil-cloned", "zutil-logger"]
resolver = "2"
[workspace.dependencies]
@ -9,11 +9,13 @@ resolver = "2"
# Workspace members
zutil-async-loadable = { path = "zutil-async-loadable" }
zutil-cloned = { path = "zutil-cloned" }
zutil-logger = { path = "zutil-logger" }
app-error = { git = "https://github.com/Zenithsiz/app-error", rev = "30238f3778fe84809ba2113c1199852b7bc7c1e9" }
arrayref = "0.3.9"
ascii = "1.1.0"
derive_more = "2.0.1"
duplicate = "2.0.0"
eframe = "0.32.3"
either = "1.15.0"
futures = "0.3.31"
@ -32,6 +34,8 @@ stable_deref_trait = "1.2.0"
syn = "2.0.106"
thiserror = "2.0.16"
tokio = "1.47.1"
tracing = "0.1.41"
tracing-subscriber = "0.3.20"
yoke = "0.8.0"
[workspace.lints]

15
zutil-logger/Cargo.toml Normal file
View File

@ -0,0 +1,15 @@
[package]
name = "zutil-logger"
version = "0.1.0"
edition = "2024"
[dependencies]
duplicate = { workspace = true }
itertools = { workspace = true }
tracing = { features = ["log"], workspace = true }
tracing-subscriber = { features = ["env-filter"], workspace = true }
zutil-cloned = { workspace = true }
[lints]
workspace = true

111
zutil-logger/src/file.rs Normal file
View File

@ -0,0 +1,111 @@
//! File logging
// Imports
use {
std::{
fs,
io::{self, Write},
sync::{
Arc,
nonpoison::{Mutex, MutexGuard},
},
},
tracing::Subscriber,
tracing_subscriber::{EnvFilter, Layer, fmt::MakeWriter, registry::LookupSpan},
};
/// File layer writer.
#[derive(Clone, Debug)]
pub struct FileWriter {
kind: Arc<Mutex<FileWriterKind>>,
}
impl FileWriter {
/// Creates a new file writer, writing to memory
pub fn memory() -> Self {
Self {
kind: Arc::new(Mutex::new(FileWriterKind::Memory(vec![]))),
}
}
/// Sets this file writer to write into a file.
///
/// If this was writing into memory, writes all captured
/// data into the file
pub fn set_file(&self, mut file: fs::File) {
let mut kind = self.kind.lock();
if let FileWriterKind::Memory(bytes) = &*kind &&
let Err(err) = file.write_all(bytes)
{
tracing::warn!("Unable to write to log file: {err}")
}
*kind = FileWriterKind::File(file);
}
/// Sets this file writer to become empty
pub fn set_empty(&self) {
*self.kind.lock() = FileWriterKind::None;
}
}
impl<'a> MakeWriter<'a> for FileWriter {
type Writer = FileWriterKindGuard<'a>;
fn make_writer(&'a self) -> Self::Writer {
FileWriterKindGuard(self.kind.lock())
}
}
/// Backend for the file writer.
#[derive(Debug)]
enum FileWriterKind {
/// File
File(fs::File),
/// Memory
Memory(Vec<u8>),
/// None
None,
}
/// Guard that implements `io::Write` for `FileWriter` to return
#[derive(Debug)]
pub struct FileWriterKindGuard<'a>(MutexGuard<'a, FileWriterKind>);
impl io::Write for FileWriterKindGuard<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match &mut *self.0 {
FileWriterKind::File(file) => file.write(buf),
FileWriterKind::Memory(items) => items.write(buf),
FileWriterKind::None => Ok(0),
}
}
fn flush(&mut self) -> io::Result<()> {
match &mut *self.0 {
FileWriterKind::File(file) => file.flush(),
FileWriterKind::Memory(items) => items.flush(),
FileWriterKind::None => Ok(()),
}
}
}
/// Creates the file layer
pub fn layer<S>(
writer: FileWriter,
default_filters: impl IntoIterator<Item = (Option<&'_ str>, &'_ str)>,
) -> impl Layer<S>
where
S: Subscriber + for<'a> LookupSpan<'a>,
{
// Then create the layer
let env = super::get_env_filters("RUST_FILE_LOG", default_filters);
let layer = tracing_subscriber::fmt::layer()
.with_writer(writer)
.with_ansi(false)
.with_filter(EnvFilter::builder().parse_lossy(env));
Some(layer)
}

194
zutil-logger/src/lib.rs Normal file
View File

@ -0,0 +1,194 @@
//! Logger
// Features
#![feature(nonpoison_mutex, sync_nonpoison, anonymous_lifetime_in_impl_trait)]
// Modules
mod file;
mod pre_init;
mod term;
// Imports
use {
self::{file::FileWriter, pre_init::PreInitLogger},
itertools::Itertools,
std::{
self,
collections::{HashMap, hash_map},
env::{self, VarError},
fs,
io::Write,
path::Path,
},
tracing::Subscriber,
tracing_subscriber::{Layer, Registry, fmt::MakeWriter, layer::Layered, prelude::*, registry::LookupSpan},
};
/// Logger
pub struct Logger {
/// File writer
file_writer: FileWriter,
}
impl Logger {
/// Creates a new logger
///
/// Starts already logging to stderr.
pub fn new<W, L>(
stderr: W,
extra_layers: L,
default_stderr_filters: impl IntoIterator<Item = (Option<&'_ str>, &'_ str)>,
default_file_filters: impl IntoIterator<Item = (Option<&'_ str>, &'_ str)>,
) -> Self
where
W: for<'a> MakeWriter<'a> + Clone + Send + Sync + 'static,
L: ExtraLayers<LoggerSubscriber>,
L::Subscriber: Subscriber + for<'a> LookupSpan<'a> + Send + Sync + 'static,
{
// Create the pre-init logger to log everything until we have our loggers running.
let pre_init_logger = PreInitLogger::new();
// Then initialize our logging
let file_writer = FileWriter::memory();
// Note: Due to [this issue](https://github.com/tokio-rs/tracing/issues/1817),
// the order here matters, and the stderr ones must be last.
let subscriber = LoggerSubscriber::default();
let subscriber = extra_layers
.layer_on(subscriber)
.with(file::layer(file_writer.clone(), default_file_filters))
.with(term::layer(stderr.clone(), default_stderr_filters));
if let Err(err) = subscriber.try_init() {
eprintln!("Failed to set global logger: {err}");
}
// Finally write the pre-init output to our writes
pre_init_logger
.into_output()
.with_bytes(|bytes| {
stderr.make_writer().write_all(bytes)?;
file_writer.make_writer().write_all(bytes)
})
.expect("Unable to write pre-init output");
tracing::info!("Successfully initialized logger");
Self { file_writer }
}
/// Sets a file to log into.
///
/// Once the logger is finished, any logs produced until then
/// will be retro-actively written into this log file.
pub fn set_file(&self, path: Option<&Path>) {
match path {
Some(path) => match fs::File::create(path) {
Ok(file) => {
self.file_writer.set_file(file);
tracing::info!("Logging to file: {path:?}");
},
Err(err) => {
tracing::warn!("Unable to create log file {path:?}: {err}");
self.file_writer.set_empty()
},
},
None => self.file_writer.set_empty(),
}
}
}
/// Logger subscriber
// TODO: Hide this behind a trait impl type alias
pub type LoggerSubscriber = Registry;
/// Returns the env filters of a variable.
///
/// Adds default filters, if not specified
#[must_use]
fn get_env_filters(env: &str, default_filters: impl IntoIterator<Item = (Option<&'_ str>, &'_ str)>) -> String {
// Get the current filters
let env_var;
let mut cur_filters = match env::var(env) {
// Split filters by `,`, then src and level by `=`
Ok(var) => {
env_var = var;
env_var
.split(',')
.map(|s| match s.split_once('=') {
Some((src, level)) => (Some(src), level),
None => (None, s),
})
.collect::<HashMap<_, _>>()
},
// If there were none, don't use any
Err(err) => {
if let VarError::NotUnicode(var) = err {
tracing::warn!("Ignoring non-utf8 env variable {env:?}: {var:?}");
}
HashMap::new()
},
};
// Add all default filters, if not specified
for (src, level) in default_filters {
if let hash_map::Entry::Vacant(entry) = cur_filters.entry(src) {
let _ = entry.insert(level);
}
}
// Then re-create it
let var = cur_filters
.into_iter()
.map(|(src, level)| match src {
Some(src) => format!("{src}={level}"),
None => level.to_owned(),
})
.join(",");
tracing::trace!("Using {env}={var}");
var
}
/// Extra layers
pub trait ExtraLayers<S> {
/// Subscriber with all layers
type Subscriber;
/// Layers all layers onto a subscriber
fn layer_on(self, subscriber: S) -> Self::Subscriber;
}
impl<S> ExtraLayers<S> for () {
type Subscriber = S;
fn layer_on(self, subscriber: S) -> Self::Subscriber {
subscriber
}
}
impl<S, L> ExtraLayers<S> for (L,)
where
S: Subscriber,
L: Layer<S>,
{
type Subscriber = Layered<L, S>;
fn layer_on(self, subscriber: S) -> Self::Subscriber {
subscriber.with(self.0)
}
}
impl<S, L0, L1> ExtraLayers<S> for (L0, L1)
where
S: Subscriber,
L0: Layer<S>,
L1: Layer<Layered<L0, S>>,
{
type Subscriber = Layered<L1, Layered<L0, S>>;
fn layer_on(self, subscriber: S) -> Self::Subscriber {
subscriber.with(self.0).with(self.1)
}
}

View File

@ -0,0 +1,79 @@
//! Pre-initialization logger
// Imports
use {
std::{
io,
sync::{
Arc,
nonpoison::{Mutex, MutexGuard},
},
},
tracing::{dispatcher, subscriber::DefaultGuard},
tracing_subscriber::{EnvFilter, Layer, fmt::MakeWriter, prelude::__tracing_subscriber_SubscriberExt},
};
/// Pre-init logger
pub struct PreInitLogger {
/// Output
output: PreInitOutput,
/// Guard
_guard: DefaultGuard,
}
impl PreInitLogger {
/// Creates a new pre-init logger
pub fn new() -> Self {
let output = PreInitOutput::default();
let layer = tracing_subscriber::fmt::layer()
.with_target(false)
.with_writer(output.clone())
.with_ansi(false)
.with_filter(EnvFilter::from_default_env());
// Initialize a barebones logger first to catch all logs
// until our temporary subscriber is up and running.
let logger = tracing_subscriber::registry().with(layer);
let guard = dispatcher::set_default(&logger.into());
Self { output, _guard: guard }
}
/// Drops this logger and returns it's output
pub fn into_output(self) -> PreInitOutput {
self.output
}
}
/// Pre-init output
#[derive(Clone, Default, Debug)]
pub struct PreInitOutput(Arc<Mutex<Vec<u8>>>);
impl PreInitOutput {
/// Uses the bytes in this output
pub fn with_bytes<O>(&self, f: impl FnOnce(&[u8]) -> O) -> O {
f(&self.0.lock())
}
}
impl<'a> MakeWriter<'a> for PreInitOutput {
type Writer = PreOutputWrite<'a>;
fn make_writer(&'a self) -> Self::Writer {
PreOutputWrite(self.0.lock())
}
}
/// Pre-init output writer
pub struct PreOutputWrite<'a>(MutexGuard<'a, Vec<u8>>);
impl io::Write for PreOutputWrite<'_> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.write(buf)
}
fn flush(&mut self) -> io::Result<()> {
self.0.flush()
}
}

26
zutil-logger/src/term.rs Normal file
View File

@ -0,0 +1,26 @@
//! Terminal logging
// Imports
use {
tracing::{Subscriber, metadata::LevelFilter},
tracing_subscriber::{EnvFilter, Layer, fmt::MakeWriter, registry::LookupSpan},
};
/// Creates the terminal layer
pub fn layer<W, S>(stderr: W, default_filters: impl IntoIterator<Item = (Option<&'_ str>, &'_ str)>) -> impl Layer<S>
where
W: for<'a> MakeWriter<'a> + 'static,
S: Subscriber + for<'a> LookupSpan<'a>,
{
let env = super::get_env_filters("RUST_LOG", default_filters);
let layer = tracing_subscriber::fmt::layer().with_target(false).with_writer(stderr);
#[cfg(debug_assertions)]
let layer = layer.with_file(true).with_line_number(true).with_thread_names(true);
layer.with_filter(
EnvFilter::builder()
.with_default_directive(LevelFilter::INFO.into())
.parse_lossy(env),
)
}