sword-layers 0.3.0

Tower layers for the Sword web framework
Documentation
mod config;
mod dev;
mod time;

use dev::DevFormatter;
use std::{io, sync::OnceLock};
use time::TimestampFormatter;
use tracing_subscriber::{
    EnvFilter, Registry,
    layer::{Layer, SubscriberExt},
};

pub use config::*;

type BoxLayer = Box<dyn Layer<Registry> + Send + Sync + 'static>;
static TRACING_INIT: OnceLock<()> = OnceLock::new();

#[derive(Debug, Clone)]
pub struct TracingSubscriber {
    config: TracingConfig,
}

impl TracingSubscriber {
    pub fn env_filter(&self) -> EnvFilter {
        if self.config.use_env_filter {
            EnvFilter::try_from_default_env()
                .or_else(|_| EnvFilter::try_new(self.config.filter.clone()))
                .unwrap_or_else(|_| EnvFilter::new("info"))
        } else {
            EnvFilter::try_new(self.config.filter.clone())
                .unwrap_or_else(|_| EnvFilter::new("info"))
        }
    }

    pub fn fmt_layer(&self) -> BoxLayer {
        let config = &self.config;
        let timer = TimestampFormatter::from_config(config);

        let builder = tracing_subscriber::fmt::layer()
            .with_ansi(config.format != LogFormat::Json)
            .with_thread_ids(config.has_field(TracingField::ThreadId))
            .with_thread_names(config.has_field(TracingField::ThreadName))
            .with_writer(io::stdout)
            .with_timer(timer.clone())
            .with_target(config.has_field(TracingField::Target))
            .with_file(config.has_field(TracingField::File))
            .with_line_number(config.has_field(TracingField::LineNumber));

        match config.format {
            LogFormat::Full => Box::new(builder),
            LogFormat::Pretty => Box::new(builder.pretty()),
            LogFormat::Compact => Box::new(builder.compact()),
            LogFormat::Json => Box::new(builder.json().flatten_event(true)),

            LogFormat::Dev => {
                let dev_fmt = DevFormatter::new(config);
                let builder = builder.event_format(dev_fmt);

                Box::new(builder)
            }
        }
    }

    pub fn layer(&self) -> BoxLayer {
        Box::new(self.fmt_layer().with_filter(self.env_filter()))
    }

    pub fn init(self) -> Result<(), tracing::subscriber::SetGlobalDefaultError> {
        if !self.config.enabled {
            return Ok(());
        }

        if TRACING_INIT.get().is_some() || tracing::dispatcher::has_been_set() {
            return Ok(());
        }

        let config = self.config.clone();
        let subscriber = Registry::default().with(self.layer());

        match tracing::subscriber::set_global_default(subscriber) {
            Ok(()) => {
                let _ = TRACING_INIT.set(());

                tracing::info!(
                    target: "sword.startup.tracing",
                    format = ?config.format,
                    default_filter = %config.filter,
                    use_env_filter = config.use_env_filter,
                    "Initialized tracing subscriber"
                );
            }
            Err(_) if tracing::dispatcher::has_been_set() => {
                let _ = TRACING_INIT.set(());
            }
            Err(err) => return Err(err),
        }

        Ok(())
    }
}

impl From<TracingConfig> for TracingSubscriber {
    fn from(config: TracingConfig) -> Self {
        Self { config }
    }
}