road-runner-common 0.12.1

Shared Rust utilities for exchange ecosystem backend services.
Documentation
#[cfg(feature = "observability")]
use once_cell::sync::OnceCell;

#[cfg(feature = "observability")]
static TRACING_INIT: OnceCell<()> = OnceCell::new();

#[cfg(feature = "observability")]
static LOG_FORMAT: OnceCell<String> = OnceCell::new();

/// Whether `init_tracing` configured the human-readable "pretty" output.
/// Defaults to `false` (structured) when tracing was never initialized, so
/// callers branching on this keep machine-parseable fields in production.
pub fn is_pretty_format() -> bool {
    #[cfg(feature = "observability")]
    {
        LOG_FORMAT.get().map(|f| f == "pretty").unwrap_or(false)
    }
    #[cfg(not(feature = "observability"))]
    {
        false
    }
}

pub fn init_tracing(service_name: &str, log_level: Option<&str>, log_format: Option<&str>) {
    #[cfg(feature = "observability")]
    {
        use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};

        if TRACING_INIT.get().is_some() {
            return;
        }

        let format = log_format
            .map(|s| s.to_lowercase())
            .or_else(|| std::env::var("LOG_FORMAT").ok().map(|s| s.to_lowercase()))
            .unwrap_or_else(|| "pretty".to_string());

        let env_filter = tracing_subscriber::EnvFilter::try_from_default_env()
            .or_else(|_| {
                let lvl = log_level
                    .map(|s| s.to_string())
                    .or_else(|| std::env::var("LOG_LEVEL").ok())
                    .unwrap_or_else(|| "info".to_string());

                let fallback = format!("{},{}=debug", lvl, service_name);
                Ok::<_, anyhow::Error>(tracing_subscriber::EnvFilter::new(fallback))
            })
            .unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info"));

        let _ = LOG_FORMAT.set(format.clone());

        let registry = tracing_subscriber::registry().with(env_filter);

        if format == "pretty" {
            // Local/dev: compact single-line output; the `http_request{..}`
            // span prefix carries request context once per line.
            let layer = tracing_subscriber::fmt::layer()
                .compact()
                .with_target(false)
                .with_thread_ids(false)
                .with_file(false)
                .with_line_number(false);

            let _ = registry.with(layer).try_init();
        } else {
            // Production: one flat JSON object per line. Event fields are
            // flattened to the top level; request context lives in a single
            // `span` object instead of the noisy per-scope `spans` list.
            let layer = tracing_subscriber::fmt::layer()
                .json()
                .flatten_event(true)
                .with_current_span(true)
                .with_span_list(false)
                .with_target(true)
                .with_thread_ids(false)
                .with_file(false)
                .with_line_number(false);

            let _ = registry.with(layer).try_init();
        }

        TRACING_INIT.set(()).ok();
        tracing::info!("Observability initialized for service: {}", service_name);
    }

    #[cfg(not(feature = "observability"))]
    {
        let _ = (service_name, log_level, log_format);
    }
}