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 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 particularly spammy sources. Loki, } impl Default for LogFilterPreset { fn default() -> Self { return Self::Info; } } impl From 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, /// 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::(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::(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::(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::(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::(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::(self.preset.get_config().into()), ) } /* LoggingTarget::Indicatif(mp) => { let writer: IndicatifWriter = IndicatifWriter::new(mp); indicatif_layer = Some( tracing_subscriber::fmt::Layer::default() .without_time() .with_ansi(true) .with_writer(writer.make_writer()) .with_filter::(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::(LogFilterPreset::Loki.get_config().into())) } else { None } #[cfg(not(feature = "loki"))] None:: + 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(()) } }