Files
webpage/crates/lib/toolbox/src/logging.rs
rm-dr 4d8093c4a3
All checks were successful
CI / Check typos (push) Successful in 8s
CI / Check links (push) Successful in 6s
CI / Clippy (push) Successful in 51s
CI / Build and test (push) Successful in 1m7s
CI / Build container (push) Successful in 45s
CI / Deploy on waypoint (push) Successful in 42s
Env config
2025-11-06 20:45:44 -08:00

469 lines
9.9 KiB
Rust

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),
format!("reqwest={}", conf.silence),
format!("axum={}", 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 if level_i >= 3 {
preset = LogFilterPreset::HyperTrace
} 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,
/// Trace EVERYTHING
HyperTrace,
/// Filter for loki subscriber.
///
/// This is similar to `Trace`,
/// but excludes particularly 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::HyperTrace => LoggingConfig {
other: LogLevel::Trace,
silence: LogLevel::Trace,
// 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(())
}
}