mirror of
https://github.com/Zenithsiz/zutil.git
synced 2026-02-03 10:31:34 +00:00
Added zutil-logger.
This commit is contained in:
parent
168a0032aa
commit
2913756373
@ -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
15
zutil-logger/Cargo.toml
Normal 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
111
zutil-logger/src/file.rs
Normal 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
194
zutil-logger/src/lib.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
79
zutil-logger/src/pre_init.rs
Normal file
79
zutil-logger/src/pre_init.rs
Normal 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
26
zutil-logger/src/term.rs
Normal 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),
|
||||
)
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user