#![warn(missing_docs)]
pub mod context;
pub mod error;
pub mod fields;
pub mod masking;
pub mod module_levels;
#[cfg(feature = "otlp")]
pub mod otlp;
pub mod sampling;
use std::collections::HashMap;
use std::fs::OpenOptions;
use std::sync::Arc;
use rskit_config::{LogFormat, LogOutput, LoggingConfig};
use tracing::dispatcher::DefaultGuard;
use tracing_subscriber::{EnvFilter, fmt, fmt::writer::BoxMakeWriter, layer::SubscriberExt};
pub use error::LoggingResult;
pub use masking::{DefaultMasker, Masker, MaskingConfig, MaskingMakeWriter};
pub use module_levels::{ModuleLevelsConfig, build_env_filter};
#[cfg(feature = "otlp")]
pub use otlp::{OtlpConfig, OtlpProvider};
pub use sampling::SamplingConfig;
#[cfg(feature = "otlp")]
pub struct LoggingSetup<'a> {
pub config: &'a LoggingConfig,
pub sampling: Option<&'a SamplingConfig>,
pub module_levels: Option<&'a HashMap<String, String>>,
pub masking: Option<&'a MaskingConfig>,
pub otlp: Option<&'a otlp::OtlpConfig>,
pub service_name: &'a str,
pub environment: &'a str,
pub version: &'a str,
}
#[cfg(feature = "otlp")]
impl<'a> LoggingSetup<'a> {
#[must_use]
pub const fn new(
config: &'a LoggingConfig,
service_name: &'a str,
environment: &'a str,
version: &'a str,
) -> Self {
Self {
config,
sampling: None,
module_levels: None,
masking: None,
otlp: None,
service_name,
environment,
version,
}
}
#[must_use]
pub const fn with_sampling(mut self, sampling: &'a SamplingConfig) -> Self {
self.sampling = Some(sampling);
self
}
#[must_use]
pub const fn with_module_levels(mut self, module_levels: &'a HashMap<String, String>) -> Self {
self.module_levels = Some(module_levels);
self
}
#[must_use]
pub const fn with_masking(mut self, masking: &'a MaskingConfig) -> Self {
self.masking = Some(masking);
self
}
#[must_use]
pub const fn with_otlp(mut self, otlp: &'a otlp::OtlpConfig) -> Self {
self.otlp = Some(otlp);
self
}
}
pub struct LoggingGuard {
#[allow(dead_code)]
guard: DefaultGuard,
#[cfg(feature = "otlp")]
otlp_provider: Option<otlp::OtlpProvider>,
}
impl LoggingGuard {
fn new(guard: DefaultGuard) -> Self {
Self {
guard,
#[cfg(feature = "otlp")]
otlp_provider: None,
}
}
#[cfg(feature = "otlp")]
fn with_otlp_provider(guard: DefaultGuard, otlp_provider: Option<otlp::OtlpProvider>) -> Self {
Self {
guard,
otlp_provider,
}
}
}
#[cfg(feature = "otlp")]
impl Drop for LoggingGuard {
fn drop(&mut self) {
if let Some(provider) = self.otlp_provider.take()
&& let Err(error) = provider.shutdown()
{
tracing::warn!(%error, "failed to shut down OTLP logging provider");
}
}
}
pub fn init_logging(cfg: &LoggingConfig) -> LoggingResult<LoggingGuard> {
init_logging_with_default_masking(cfg)
}
pub fn init_logging_env() -> LoggingGuard {
let filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
let layer = fmt::layer().pretty();
let dispatcher = tracing_subscriber::registry()
.with(filter)
.with(layer)
.into();
LoggingGuard::new(tracing::dispatcher::set_default(&dispatcher))
}
fn init_logging_with_default_masking(cfg: &LoggingConfig) -> LoggingResult<LoggingGuard> {
let filter = build_filter(&cfg.level, None);
let masker: Arc<dyn masking::Masker> = Arc::new(masking::DefaultMasker::default());
let writer = masking::MaskingMakeWriter::new(build_output_writer(&cfg.output)?, masker);
let guard = match cfg.format {
LogFormat::Json => {
let layer = fmt::layer()
.json()
.with_current_span(true)
.with_span_list(true)
.with_writer(writer);
let dispatcher = tracing_subscriber::registry()
.with(filter)
.with(layer)
.into();
tracing::dispatcher::set_default(&dispatcher)
}
_ => {
let layer = fmt::layer().pretty().with_writer(writer);
let dispatcher = tracing_subscriber::registry()
.with(filter)
.with(layer)
.into();
tracing::dispatcher::set_default(&dispatcher)
}
};
Ok(LoggingGuard::new(guard))
}
pub fn init_logging_with_masking(
cfg: &LoggingConfig,
masking_cfg: &masking::MaskingConfig,
) -> LoggingResult<LoggingGuard> {
let m = if masking_cfg.enabled {
Some(masking_cfg)
} else {
None
};
init_logging_with_options(cfg, None, None, m)
}
pub fn init_logging_with_options(
cfg: &LoggingConfig,
sampling_cfg: Option<&SamplingConfig>,
module_levels: Option<&HashMap<String, String>>,
masking_cfg: Option<&MaskingConfig>,
) -> LoggingResult<LoggingGuard> {
let filter = build_filter(&cfg.level, module_levels);
let sampling_layer = sampling_cfg
.filter(|s| s.enabled)
.map(sampling::SamplingLayer::new);
let writer = build_output_writer(&cfg.output)?;
if let Some(m) = masking_cfg.filter(|m| m.enabled) {
let masker: Arc<dyn masking::Masker> = Arc::new(masking::DefaultMasker::new(m)?);
let writer = masking::MaskingMakeWriter::new(writer, masker);
let guard = match cfg.format {
LogFormat::Json => {
let layer = fmt::layer()
.json()
.with_current_span(true)
.with_span_list(true)
.with_writer(writer);
let dispatcher = tracing_subscriber::registry()
.with(filter)
.with(sampling_layer)
.with(layer)
.into();
tracing::dispatcher::set_default(&dispatcher)
}
_ => {
let layer = fmt::layer().pretty().with_writer(writer);
let dispatcher = tracing_subscriber::registry()
.with(filter)
.with(sampling_layer)
.with(layer)
.into();
tracing::dispatcher::set_default(&dispatcher)
}
};
return Ok(LoggingGuard::new(guard));
}
let guard = match cfg.format {
LogFormat::Json => {
let layer = fmt::layer()
.json()
.with_current_span(true)
.with_span_list(true)
.with_writer(writer);
let dispatcher = tracing_subscriber::registry()
.with(filter)
.with(sampling_layer)
.with(layer)
.into();
tracing::dispatcher::set_default(&dispatcher)
}
_ => {
let layer = fmt::layer().pretty().with_writer(writer);
let dispatcher = tracing_subscriber::registry()
.with(filter)
.with(sampling_layer)
.with(layer)
.into();
tracing::dispatcher::set_default(&dispatcher)
}
};
Ok(LoggingGuard::new(guard))
}
fn build_filter(level: &str, module_levels: Option<&HashMap<String, String>>) -> EnvFilter {
match module_levels {
Some(levels) if !levels.is_empty() => module_levels::build_env_filter(level, levels),
_ => EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(level)),
}
}
fn build_output_writer(output: &LogOutput) -> LoggingResult<BoxMakeWriter> {
let writer = match output {
LogOutput::Stdout => BoxMakeWriter::new(std::io::stdout),
LogOutput::Stderr => BoxMakeWriter::new(std::io::stderr),
LogOutput::File { path } => {
let file = OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|err| error::log_file_open(path.clone(), err))?;
BoxMakeWriter::new(Arc::new(file))
}
_ => return Err(error::unsupported_output()),
};
Ok(writer)
}
#[cfg(feature = "otlp")]
pub fn init_logging_full(setup: LoggingSetup<'_>) -> LoggingResult<LoggingGuard> {
let filter = build_filter(&setup.config.level, setup.module_levels);
let sampling_layer = setup
.sampling
.filter(|s| s.enabled)
.map(sampling::SamplingLayer::new);
let writer = build_output_writer(&setup.config.output)?;
let otlp_provider = match setup.otlp {
Some(oc) => {
otlp::OtlpProvider::new(oc, setup.service_name, setup.environment, setup.version)?
}
None => None,
};
let otlp_layer = otlp_provider
.as_ref()
.map(|p| p.layer::<tracing_subscriber::Registry>());
if let Some(m) = setup.masking.filter(|m| m.enabled) {
let masker: Arc<dyn masking::Masker> = Arc::new(masking::DefaultMasker::new(m)?);
let writer = masking::MaskingMakeWriter::new(writer, masker);
let guard = match setup.config.format {
LogFormat::Json => {
let layer = fmt::layer()
.json()
.with_current_span(true)
.with_span_list(true)
.with_writer(writer);
let dispatcher = tracing_subscriber::registry()
.with(filter)
.with(sampling_layer)
.with(layer)
.with(otlp_layer)
.into();
tracing::dispatcher::set_default(&dispatcher)
}
_ => {
let layer = fmt::layer().pretty().with_writer(writer);
let dispatcher = tracing_subscriber::registry()
.with(filter)
.with(sampling_layer)
.with(layer)
.with(otlp_layer)
.into();
tracing::dispatcher::set_default(&dispatcher)
}
};
return Ok(LoggingGuard::with_otlp_provider(guard, otlp_provider));
}
let guard = match setup.config.format {
LogFormat::Json => {
let layer = fmt::layer()
.json()
.with_current_span(true)
.with_span_list(true)
.with_writer(writer);
let dispatcher = tracing_subscriber::registry()
.with(filter)
.with(sampling_layer)
.with(layer)
.with(otlp_layer)
.into();
tracing::dispatcher::set_default(&dispatcher)
}
_ => {
let layer = fmt::layer().pretty().with_writer(writer);
let dispatcher = tracing_subscriber::registry()
.with(filter)
.with(sampling_layer)
.with(layer)
.with(otlp_layer)
.into();
tracing::dispatcher::set_default(&dispatcher)
}
};
Ok(LoggingGuard::with_otlp_provider(guard, otlp_provider))
}
pub use tracing::{debug, error, info, instrument, trace, warn};
#[cfg(test)]
mod tests {
use super::*;
use rskit_config::LoggingConfig;
#[test]
fn init_console_does_not_panic() {
let cfg = LoggingConfig::default();
let _guard = init_logging(&cfg).unwrap();
tracing::info!("test log");
}
#[test]
fn init_json_does_not_panic() {
let cfg = LoggingConfig {
format: rskit_config::LogFormat::Json,
..Default::default()
};
let _guard = init_logging(&cfg).unwrap();
tracing::info!("test json log");
}
}