Reorganize
This commit is contained in:
448
crates/lib/toolbox/src/logging.rs
Normal file
448
crates/lib/toolbox/src/logging.rs
Normal file
@@ -0,0 +1,448 @@
|
||||
use anyhow::Result;
|
||||
use clap::{Parser, ValueEnum};
|
||||
use serde::Deserialize;
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
use tracing_subscriber::{EnvFilter, Layer, layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
//
|
||||
// MARK: loglevel
|
||||
//
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, ValueEnum)]
|
||||
pub enum LogLevel {
|
||||
Trace,
|
||||
Debug,
|
||||
Info,
|
||||
Warn,
|
||||
Error,
|
||||
}
|
||||
|
||||
impl Default for LogLevel {
|
||||
fn default() -> Self {
|
||||
Self::Info
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for LogLevel {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Self::Trace => write!(f, "trace"),
|
||||
Self::Debug => write!(f, "debug"),
|
||||
Self::Info => write!(f, "info"),
|
||||
Self::Warn => write!(f, "warn"),
|
||||
Self::Error => write!(f, "error"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// MARK: logconfig
|
||||
//
|
||||
|
||||
/// Configures log levels for known sources
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
|
||||
pub struct LoggingConfig {
|
||||
pub other: LogLevel,
|
||||
pub silence: LogLevel,
|
||||
|
||||
// Libs
|
||||
pub libservice: LogLevel,
|
||||
pub toolbox: LogLevel,
|
||||
|
||||
// Bins
|
||||
pub service: LogLevel,
|
||||
pub webpage: LogLevel,
|
||||
}
|
||||
|
||||
impl From<LoggingConfig> for EnvFilter {
|
||||
fn from(conf: LoggingConfig) -> Self {
|
||||
// Should never fail
|
||||
#[expect(clippy::unwrap_used)]
|
||||
EnvFilter::from_str(
|
||||
&[
|
||||
//
|
||||
// Silence
|
||||
//
|
||||
// http
|
||||
format!("hyper_util={}", conf.silence),
|
||||
format!("h2={}", conf.silence),
|
||||
format!("rustls={}", conf.silence),
|
||||
format!("tower={}", conf.silence),
|
||||
//
|
||||
// Libs
|
||||
//
|
||||
format!("toolbox={}", conf.toolbox),
|
||||
format!("libservice={}", conf.libservice),
|
||||
//
|
||||
// Bins
|
||||
//
|
||||
format!("service_webpage={}", conf.service),
|
||||
format!("webpage={}", conf.webpage),
|
||||
conf.other.to_string(),
|
||||
]
|
||||
.join(","),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// MARK: LogCliVQ
|
||||
//
|
||||
|
||||
/// Provides global -v and -q cli arguments.
|
||||
/// Use with `#[arg(flatten)]`.
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
pub struct LogCliVQ {
|
||||
/// Increase verbosity (can be repeated)
|
||||
#[arg(default_value = "0")]
|
||||
#[arg(short, action = clap::ArgAction::Count,global = true)]
|
||||
v: u8,
|
||||
|
||||
/// Decrease verbosity (can be repeated)
|
||||
#[arg(default_value = "0")]
|
||||
#[arg(short, action = clap::ArgAction::Count, global = true)]
|
||||
q: u8,
|
||||
}
|
||||
|
||||
impl LogCliVQ {
|
||||
pub fn into_preset(&self) -> LogFilterPreset {
|
||||
let level_i: i16 = self.v as i16 - self.q as i16;
|
||||
|
||||
let preset;
|
||||
if level_i <= -2 {
|
||||
preset = LogFilterPreset::Error
|
||||
} else if level_i == -1 {
|
||||
preset = LogFilterPreset::Warn
|
||||
} else if level_i == 0 {
|
||||
preset = LogFilterPreset::Info
|
||||
} else if level_i == 1 {
|
||||
preset = LogFilterPreset::Debug
|
||||
} else if level_i >= 2 {
|
||||
preset = LogFilterPreset::Trace
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
return preset;
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// MARK: logpreset
|
||||
//
|
||||
|
||||
/// Provides preset configurations of [LoggingConfig]
|
||||
#[derive(Debug, Deserialize, Clone, Copy)]
|
||||
pub enum LogFilterPreset {
|
||||
/// Standard "error" log level
|
||||
Error,
|
||||
|
||||
/// Standard "warn" log level
|
||||
Warn,
|
||||
|
||||
/// Standard "info" log level.
|
||||
/// This is the default.
|
||||
Info,
|
||||
|
||||
/// Standard "debug" log level
|
||||
Debug,
|
||||
|
||||
/// Standard "trace" log level
|
||||
Trace,
|
||||
|
||||
/// Filter for loki subscriber.
|
||||
///
|
||||
/// This is similar to `Trace`,
|
||||
/// but excludes particulary spammy sources.
|
||||
Loki,
|
||||
}
|
||||
|
||||
impl Default for LogFilterPreset {
|
||||
fn default() -> Self {
|
||||
return Self::Info;
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LogFilterPreset> for LoggingConfig {
|
||||
fn from(val: LogFilterPreset) -> Self {
|
||||
val.get_config()
|
||||
}
|
||||
}
|
||||
|
||||
impl LogFilterPreset {
|
||||
pub fn get_config(&self) -> LoggingConfig {
|
||||
match self {
|
||||
Self::Error => LoggingConfig {
|
||||
other: LogLevel::Error,
|
||||
silence: LogLevel::Error,
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Error,
|
||||
toolbox: LogLevel::Error,
|
||||
|
||||
// Bins
|
||||
webpage: LogLevel::Error,
|
||||
service: LogLevel::Error,
|
||||
},
|
||||
|
||||
Self::Warn => LoggingConfig {
|
||||
other: LogLevel::Warn,
|
||||
silence: LogLevel::Warn,
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Warn,
|
||||
toolbox: LogLevel::Warn,
|
||||
|
||||
// Bins
|
||||
webpage: LogLevel::Warn,
|
||||
service: LogLevel::Warn,
|
||||
},
|
||||
|
||||
Self::Info => LoggingConfig {
|
||||
other: LogLevel::Warn,
|
||||
silence: LogLevel::Warn,
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Info,
|
||||
toolbox: LogLevel::Info,
|
||||
|
||||
// Bins
|
||||
webpage: LogLevel::Info,
|
||||
service: LogLevel::Info,
|
||||
},
|
||||
|
||||
Self::Debug => LoggingConfig {
|
||||
other: LogLevel::Warn,
|
||||
silence: LogLevel::Warn,
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Debug,
|
||||
toolbox: LogLevel::Debug,
|
||||
|
||||
// Bins
|
||||
webpage: LogLevel::Debug,
|
||||
service: LogLevel::Debug,
|
||||
},
|
||||
|
||||
Self::Trace => LoggingConfig {
|
||||
other: LogLevel::Trace,
|
||||
silence: LogLevel::Warn,
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Trace,
|
||||
toolbox: LogLevel::Trace,
|
||||
|
||||
// Bins
|
||||
webpage: LogLevel::Trace,
|
||||
service: LogLevel::Trace,
|
||||
},
|
||||
|
||||
Self::Loki => LoggingConfig {
|
||||
other: LogLevel::Trace,
|
||||
silence: LogLevel::Warn,
|
||||
|
||||
// Libs
|
||||
libservice: LogLevel::Trace,
|
||||
toolbox: LogLevel::Trace,
|
||||
|
||||
// Bins
|
||||
webpage: LogLevel::Trace,
|
||||
service: LogLevel::Trace,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// MARK: initializer
|
||||
//
|
||||
|
||||
#[cfg(feature = "loki")]
|
||||
#[derive(Deserialize, Clone)]
|
||||
pub struct LokiConfig {
|
||||
pub loki_host: url::Url,
|
||||
pub loki_user: String,
|
||||
pub loki_pass: String,
|
||||
pub loki_node_name: String,
|
||||
}
|
||||
|
||||
/// Where to print logs
|
||||
pub enum LoggingTarget {
|
||||
/// Send logs to stdout
|
||||
Stdout { format: LoggingFormat },
|
||||
|
||||
/// Send logs to stderr
|
||||
Stderr { format: LoggingFormat },
|
||||
/*
|
||||
/// Send logs to an IndicatifWriter.
|
||||
///
|
||||
/// This is the same as Stderr { format: Ansi {color:true} },
|
||||
/// but uses an indicatifwriter with the given multiprogress.
|
||||
Indicatif(MultiProgress),
|
||||
*/
|
||||
}
|
||||
|
||||
/// How to print logs
|
||||
#[derive(Debug, Clone, Copy, Deserialize)]
|
||||
pub enum LoggingFormat {
|
||||
Ansi,
|
||||
AnsiNoColor,
|
||||
Json,
|
||||
}
|
||||
|
||||
impl Default for LoggingFormat {
|
||||
fn default() -> Self {
|
||||
Self::Ansi
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LoggingInitializer {
|
||||
pub app_name: &'static str,
|
||||
|
||||
/// If `Some`, send logs to the given loki server
|
||||
#[cfg(feature = "loki")]
|
||||
pub loki: Option<LokiConfig>,
|
||||
|
||||
/// Log filter for printed logs
|
||||
pub preset: LogFilterPreset,
|
||||
|
||||
/// Where to print logs
|
||||
pub target: LoggingTarget,
|
||||
}
|
||||
|
||||
impl LoggingInitializer {
|
||||
pub fn initialize(self) -> Result<()> {
|
||||
let mut stderr_ansi_layer = None;
|
||||
let mut stderr_json_layer = None;
|
||||
let mut stdout_ansi_layer = None;
|
||||
let mut stdout_json_layer = None;
|
||||
match self.target {
|
||||
LoggingTarget::Stderr {
|
||||
format: LoggingFormat::Ansi,
|
||||
} => {
|
||||
stderr_ansi_layer = Some(
|
||||
tracing_subscriber::fmt::Layer::default()
|
||||
.without_time()
|
||||
.with_ansi(true)
|
||||
.with_writer(std::io::stderr)
|
||||
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||
)
|
||||
}
|
||||
|
||||
LoggingTarget::Stderr {
|
||||
format: LoggingFormat::AnsiNoColor,
|
||||
} => {
|
||||
stderr_ansi_layer = Some(
|
||||
tracing_subscriber::fmt::Layer::default()
|
||||
.without_time()
|
||||
.with_ansi(false)
|
||||
.with_writer(std::io::stderr)
|
||||
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||
)
|
||||
}
|
||||
|
||||
LoggingTarget::Stderr {
|
||||
format: LoggingFormat::Json,
|
||||
} => {
|
||||
stderr_json_layer = Some(
|
||||
tracing_subscriber::fmt::Layer::default()
|
||||
.without_time()
|
||||
.with_ansi(false)
|
||||
.json()
|
||||
.with_writer(std::io::stderr)
|
||||
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||
)
|
||||
}
|
||||
|
||||
LoggingTarget::Stdout {
|
||||
format: LoggingFormat::Ansi,
|
||||
} => {
|
||||
stdout_ansi_layer = Some(
|
||||
tracing_subscriber::fmt::Layer::default()
|
||||
.without_time()
|
||||
.with_ansi(true)
|
||||
.with_writer(std::io::stdout)
|
||||
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||
)
|
||||
}
|
||||
|
||||
LoggingTarget::Stdout {
|
||||
format: LoggingFormat::AnsiNoColor,
|
||||
} => {
|
||||
stdout_ansi_layer = Some(
|
||||
tracing_subscriber::fmt::Layer::default()
|
||||
.without_time()
|
||||
.with_ansi(false)
|
||||
.with_writer(std::io::stdout)
|
||||
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||
)
|
||||
}
|
||||
|
||||
LoggingTarget::Stdout {
|
||||
format: LoggingFormat::Json,
|
||||
} => {
|
||||
stdout_json_layer = Some(
|
||||
tracing_subscriber::fmt::Layer::default()
|
||||
.without_time()
|
||||
.with_ansi(false)
|
||||
.json()
|
||||
.with_writer(std::io::stdout)
|
||||
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||
)
|
||||
} /*
|
||||
LoggingTarget::Indicatif(mp) => {
|
||||
let writer: IndicatifWriter<tracing_indicatif::writer::Stderr> =
|
||||
IndicatifWriter::new(mp);
|
||||
|
||||
indicatif_layer = Some(
|
||||
tracing_subscriber::fmt::Layer::default()
|
||||
.without_time()
|
||||
.with_ansi(true)
|
||||
.with_writer(writer.make_writer())
|
||||
.with_filter::<EnvFilter>(self.preset.get_config().into()),
|
||||
)
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
||||
let loki_layer = {
|
||||
#[cfg(feature = "loki")]
|
||||
if let Some(cfg) = self.loki {
|
||||
use anyhow::Context;
|
||||
use base64::{Engine, prelude::BASE64_STANDARD};
|
||||
|
||||
let basic_auth = format!("{}:{}", cfg.loki_user, cfg.loki_pass);
|
||||
let encoded_basic_auth = BASE64_STANDARD.encode(basic_auth.as_bytes());
|
||||
|
||||
let (layer, task) = tracing_loki::builder()
|
||||
.label("node_name", cfg.loki_node_name)
|
||||
.context("while building loki node_name label")?
|
||||
.label("app", self.app_name)
|
||||
.context("while building loki app label")?
|
||||
.http_header("Authorization", format!("Basic {encoded_basic_auth}"))
|
||||
.context("while building loki header")?
|
||||
.build_url(cfg.loki_host)
|
||||
.context("while building loki layer")?;
|
||||
|
||||
tokio::spawn(task);
|
||||
Some(layer.with_filter::<EnvFilter>(LogFilterPreset::Loki.get_config().into()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "loki"))]
|
||||
None::<Box<dyn Layer<_> + Send + Sync>>
|
||||
};
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(loki_layer)
|
||||
.with(stdout_ansi_layer)
|
||||
.with(stdout_json_layer)
|
||||
.with(stderr_ansi_layer)
|
||||
.with(stderr_json_layer)
|
||||
.init();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user