use std::{
env::set_var,
path::PathBuf,
sync::atomic::{AtomicBool, Ordering},
};
#[cfg(feature = "full")]
use opentelemetry::trace::TracerProvider;
#[cfg(feature = "full")]
use opentelemetry_sdk::{metrics::SdkMeterProvider, trace::SdkTracerProvider};
#[cfg(feature = "full")]
use tracing::debug;
use tracing::{info, span, warn};
#[cfg(feature = "full")]
use tracing_opentelemetry::{MetricsLayer, OpenTelemetryLayer};
use tracing_subscriber::{layer::SubscriberExt, reload, util::SubscriberInitExt, EnvFilter, Layer};
#[cfg(feature = "full")]
use crate::otlp;
use crate::LoggerError;
static TRACING_SET: AtomicBool = AtomicBool::new(false);
#[derive(Debug, Default, Clone)]
pub struct TracingConfig {
pub service_name: String,
#[cfg(feature = "full")]
pub otlp: Option<TelemetryConfig>,
pub no_log_to_stdout: bool,
#[cfg(not(target_os = "windows"))]
pub log_to_syslog: bool,
pub log_to_file: Option<(PathBuf, String)>,
pub rust_log: Option<String>,
pub with_ansi_colors: bool,
}
#[cfg(feature = "full")]
#[derive(Debug, Default, Clone)]
pub struct TelemetryConfig {
pub version: Option<String>,
pub environment: Option<String>,
pub otlp_url: String,
pub enable_metering: bool,
}
#[derive(Default)]
pub struct LoggingGuards {
#[cfg(feature = "full")]
tracer_provider: Option<SdkTracerProvider>,
#[cfg(feature = "full")]
meter_provider: Option<SdkMeterProvider>,
rolling_appender_guard: Option<tracing_appender::non_blocking::WorkerGuard>,
}
impl Drop for LoggingGuards {
fn drop(&mut self) {
#[cfg(feature = "full")]
{
if let Some(tracer_provider) = &mut self.tracer_provider {
debug!("dropping OTLP tracer");
if let Err(err) = tracer_provider.shutdown() {
eprintln!("Trace provider shutdown error: {err:?}");
}
}
if let Some(meter_provider) = &mut self.meter_provider {
debug!("dropping OTLP meter");
if let Err(_err) = meter_provider.shutdown() {
}
}
}
}
}
pub fn tracing_init(tracing_config: &TracingConfig) -> LoggingGuards {
if let Some(rust_log) = &tracing_config.rust_log {
set_var("RUST_LOG", rust_log);
}
set_var("RUST_BACKTRACE", "full");
if TRACING_SET.swap(true, Ordering::Acquire) {
let span = span!(tracing::Level::INFO, "tracing_init");
let _guard = span.enter();
warn!("Tracing already initialized or crashed");
return LoggingGuards::default();
}
match tracing_init_(tracing_config) {
Ok(otel_guard) => {
let span = span!(tracing::Level::INFO, "tracing_init");
let _guard = span.enter();
info!("Tracing initialized with config {tracing_config:#?}",);
otel_guard
}
Err(err) => {
TRACING_SET.store(false, Ordering::Release);
eprintln!("Failed to initialize tracing: {err:?}");
LoggingGuards::default()
}
}
}
#[derive(Clone, Copy)]
struct FmtConfig {
with_level: bool,
with_target: bool,
with_thread_ids: bool,
with_line_number: bool,
with_file: bool,
with_ansi: bool,
}
impl FmtConfig {
const fn standard(with_ansi: bool) -> Self {
Self {
with_level: true,
with_target: true,
with_thread_ids: true,
with_line_number: true,
with_file: true,
with_ansi,
}
}
}
macro_rules! configure_fmt_layer {
($layer:expr, $config:expr) => {{
$layer
.with_level($config.with_level)
.with_target($config.with_target)
.with_thread_ids($config.with_thread_ids)
.with_line_number($config.with_line_number)
.with_file($config.with_file)
.with_ansi($config.with_ansi)
}};
}
fn tracing_init_(config: &TracingConfig) -> Result<LoggingGuards, LoggerError> {
let mut otel_guard = LoggingGuards::default();
let mut layers = vec![];
let filter = {
#[cfg(feature = "full")]
{
if config.otlp.is_some() {
let (filter, _reload_handle) = reload::Layer::new(
EnvFilter::from_default_env()
.add_directive("hyper=error".parse()?)
.add_directive("tonic=error".parse()?)
.add_directive("tower::buffer=off".parse()?)
.add_directive("opentelemetry-otlp=off".parse()?)
.add_directive("opentelemetry_sdk=error".parse()?)
.add_directive("h2=off".parse()?),
);
filter
} else {
let (filter, _reload_handle) = reload::Layer::new(EnvFilter::from_default_env());
filter
}
}
#[cfg(not(feature = "full"))]
{
let (filter, _reload_handle) = reload::Layer::new(EnvFilter::from_default_env());
filter
}
};
if !config.no_log_to_stdout {
let fmt_layer = configure_fmt_layer!(
tracing_subscriber::fmt::layer(),
FmtConfig::standard(config.with_ansi_colors)
)
.compact();
layers.push(fmt_layer.boxed());
}
if let Some((dir, name)) = &config.log_to_file {
if !dir.exists() {
std::fs::create_dir_all(dir).map_err(|err| {
LoggerError::IOError(format!("Failed to create logs directory: {dir:?}: {err:?}"))
})?;
}
let file_appender = tracing_appender::rolling::daily(dir, name);
let (non_blocking_writer, guard) = tracing_appender::non_blocking(file_appender);
otel_guard.rolling_appender_guard = Some(guard);
let fmt_layer = configure_fmt_layer!(
tracing_subscriber::fmt::layer().with_writer(non_blocking_writer),
FmtConfig::standard(false) )
.compact();
layers.push(fmt_layer.boxed());
}
#[cfg(all(not(target_os = "windows"), feature = "full"))]
if config.log_to_syslog {
let identity =
std::borrow::Cow::Owned(std::ffi::CString::new(config.service_name.clone())?);
let (options, facility) = Default::default();
if let Some(syslog) = syslog_tracing::Syslog::new(identity, options, facility) {
let syslog_layer = configure_fmt_layer!(
tracing_subscriber::fmt::layer().with_writer(syslog),
FmtConfig::standard(false) );
layers.push(syslog_layer.boxed());
}
}
#[cfg(feature = "full")]
if let Some(otlp_config) = &config.otlp {
let otlp_provider = otlp::init_tracer_provider(
&config.service_name,
&otlp_config.otlp_url,
otlp_config.version.clone(),
otlp_config.environment.clone(),
)?;
layers.push(
OpenTelemetryLayer::new(otlp_provider.tracer(config.service_name.clone())).boxed(),
);
let meter_provider = otlp_config
.enable_metering
.then(|| {
otlp::init_meter_provider(
&config.service_name,
&otlp_config.otlp_url,
otlp_config.version.clone(),
otlp_config.environment.clone(),
)
.map(|meter_provider| {
layers.push(MetricsLayer::new(meter_provider.clone()).boxed());
meter_provider
})
})
.transpose()?;
otel_guard.tracer_provider = Some(otlp_provider);
otel_guard.meter_provider = meter_provider;
}
tracing_subscriber::registry()
.with(filter)
.with(layers)
.try_init()?;
Ok(otel_guard)
}