#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(clippy::pedantic)]
#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::todo,
clippy::unimplemented,
clippy::panic
)
)]
#![allow(
clippy::module_name_repetitions,
clippy::must_use_candidate,
clippy::missing_errors_doc
)]
mod error;
pub mod metrics;
mod redacting_writer;
use std::cell::RefCell;
use std::path::PathBuf;
use std::sync::{Mutex, OnceLock};
use tracing_subscriber::fmt::writer::BoxMakeWriter;
use tracing_subscriber::{
fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, Registry,
};
pub use error::Error;
pub use metrics::*;
pub use redacting_writer::RedactingWriter;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LogLevel {
Trace,
Debug,
Info,
Warn,
Error,
}
pub struct InitGuard;
static INIT: OnceLock<()> = OnceLock::new();
thread_local! {
static TOOL: RefCell<String> = const { RefCell::new(String::new()) };
static OP_STACK: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
}
pub fn init(tool: &str, level: LogLevel) -> InitGuard {
init_with(InitConfig::new(tool, level))
}
#[derive(Debug, Clone)]
pub struct InitConfig {
tool: String,
level: LogLevel,
deny_by_default: bool,
format: OutputFormat,
without_time: bool,
file_path: Option<PathBuf>,
default_filter: Option<String>,
env_var: Option<String>,
}
#[derive(Debug, Clone, Copy)]
enum OutputFormat {
Human,
Json,
Compact,
}
impl InitConfig {
#[must_use]
pub fn new(tool: impl Into<String>, level: LogLevel) -> Self {
Self {
tool: tool.into(),
level,
deny_by_default: false,
format: OutputFormat::Human,
without_time: false,
file_path: None,
default_filter: None,
env_var: None,
}
}
#[must_use]
pub fn deny_by_default(mut self) -> Self {
self.deny_by_default = true;
self
}
#[must_use]
pub fn json_output(mut self) -> Self {
self.format = OutputFormat::Json;
self
}
#[must_use]
pub fn compact(mut self) -> Self {
self.format = OutputFormat::Compact;
self
}
#[must_use]
pub fn without_time(mut self) -> Self {
self.without_time = true;
self
}
#[must_use]
pub fn default_filter(mut self, directives: impl Into<String>) -> Self {
self.default_filter = Some(directives.into());
self
}
#[must_use]
pub fn env_var(mut self, name: impl Into<String>) -> Self {
self.env_var = Some(name.into());
self
}
pub fn file_sink(mut self, path: impl Into<PathBuf>) -> std::io::Result<Self> {
let path = path.into();
std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)?;
self.file_path = Some(path);
Ok(self)
}
pub fn init(self) -> InitGuard {
init_with(self)
}
fn build_filter(&self) -> EnvFilter {
let from_env = match &self.env_var {
Some(name) => EnvFilter::try_from_env(name),
None => EnvFilter::try_from_default_env(),
};
from_env.unwrap_or_else(|_| {
if let Some(directives) = &self.default_filter {
EnvFilter::new(directives.clone())
} else if self.deny_by_default {
EnvFilter::new("off")
} else {
level_filter(self.level)
}
})
}
fn fmt_layer(
&self,
writer: BoxMakeWriter,
ansi: bool,
) -> Box<dyn Layer<Registry> + Send + Sync> {
let base = fmt::layer()
.with_target(false)
.with_ansi(ansi)
.with_writer(writer);
match self.format {
OutputFormat::Json => {
let layer = base.json();
if self.without_time {
layer.without_time().boxed()
} else {
layer.boxed()
}
}
OutputFormat::Compact => {
let layer = base.compact();
if self.without_time {
layer.without_time().boxed()
} else {
layer.boxed()
}
}
OutputFormat::Human => {
if self.without_time {
base.without_time().boxed()
} else {
base.boxed()
}
}
}
}
fn install(self) {
let filter = self.build_filter();
let (writer, ansi): (BoxMakeWriter, bool) = match &self.file_path {
Some(path) => match std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
{
Ok(file) => (BoxMakeWriter::new(Mutex::new(file)), false),
Err(_) => (BoxMakeWriter::new(std::io::stderr), true),
},
None => (BoxMakeWriter::new(std::io::stderr), true),
};
let layer = self.fmt_layer(writer, ansi);
Registry::default().with(layer).with(filter).init();
}
}
pub fn init_with(config: InitConfig) -> InitGuard {
let tool = config.tool.clone();
INIT.get_or_init(move || config.install());
TOOL.with(|name| *name.borrow_mut() = tool);
InitGuard
}
#[cfg(feature = "prometheus")]
pub(crate) fn get_tool_name() -> String {
TOOL.with(|name| name.borrow().clone())
}
pub mod tracing {
pub use tracing::*;
}
pub fn with_op<F, R>(op: &str, body: F) -> R
where
F: FnOnce() -> R,
{
OP_STACK.with(|stack| stack.borrow_mut().push(op.to_string()));
let tool = TOOL.with(|t| t.borrow().clone());
let ops_chain = OP_STACK.with(|stack| {
stack
.borrow()
.iter()
.map(|name| format!("op=\"{name}\""))
.collect::<Vec<_>>()
.join(" ")
});
let span = tracing::info_span!(
parent: tracing::Span::current(),
"santh_op",
tool = %tool,
op = op,
ops = %ops_chain
);
let result = span.in_scope(body);
OP_STACK.with(|stack| {
stack.borrow_mut().pop();
});
result
}
#[macro_export]
macro_rules! santh_span {
($tool:expr, $op:expr, $target:expr, $body:block) => {{
let span = $crate::tracing::info_span!("santh", tool = $tool, op = $op, target = $target);
let _enter = span.enter();
$body
}};
}
fn level_filter(level: LogLevel) -> EnvFilter {
use tracing::Level;
let level = match level {
LogLevel::Trace => Level::TRACE,
LogLevel::Debug => Level::DEBUG,
LogLevel::Info => Level::INFO,
LogLevel::Warn => Level::WARN,
LogLevel::Error => Level::ERROR,
};
EnvFilter::default().add_directive(level.into())
}