newton-core 0.4.16

newton protocol core sdk
use std::fmt::{self, Write};
use tracing::{Event, Subscriber};
use tracing_subscriber::{
    fmt::{
        format::{FmtSpan, FormatEvent},
        FmtContext,
    },
    EnvFilter,
};

/// Supported log output formats used with `tracing_subscriber`.
#[derive(Debug, Clone, Copy, Default, clap::ValueEnum)]
pub enum LogFormat {
    /// Human-readable, single-line logs (default)
    #[default]
    Full,
    /// Compact variant optimized for short line lengths
    Compact,
    /// Multi-line logs optimized for human readability
    Pretty,
    /// Newline-delimited JSON logs
    Json,
    /// Minimal format - only the message
    Minimal,
}

/// Logger configuration used to initialize the global subscriber.
#[derive(Debug, Clone)]
pub struct LoggerConfig {
    /// Desired log output format.
    pub format: LogFormat,
    /// Env filter string (e.g. `info`, `debug,my_crate=trace`).
    pub env_filter: String,
    /// Whether to enable ANSI colors in log output.
    pub ansi: bool,
    /// Whether to log span events (entry/exit from #[instrument]).
    /// Set to false to disable span logging for local development.
    pub log_spans: bool,
    /// Whether to show span context (the full span path) before log messages.
    /// When false, only the log message is shown without the span hierarchy.
    /// Can be controlled via SHOW_SPAN_CONTEXT environment variable (default: true).
    pub show_span_context: bool,
}

impl Default for LoggerConfig {
    fn default() -> Self {
        // Default: disable span logging (can be enabled by setting ENABLE_SPAN_LOGS=true)
        let log_spans = std::env::var("ENABLE_SPAN_LOGS")
            .map(|v| v == "true" || v == "1")
            .unwrap_or(false);
        // Default: show span context (can be disabled by setting SHOW_SPAN_CONTEXT=false)
        let show_span_context = std::env::var("SHOW_SPAN_CONTEXT").map(|v| v != "false").unwrap_or(true);
        let ansi = std::env::var("CHAIN_ID").map(|v| v == "31337").unwrap_or(false);

        Self {
            format: LogFormat::Full,
            env_filter: "info".to_string(),
            ansi: false,
            log_spans,
            show_span_context,
        }
    }
}

impl LoggerConfig {
    /// Create a new logger configuration with the given `format`.
    pub fn new(format: LogFormat) -> Self {
        Self {
            format,
            ..Default::default()
        }
    }

    /// Set the `env_filter` used to configure log verbosity.
    pub fn with_env_filter(mut self, filter: String) -> Self {
        self.env_filter = filter;
        self
    }

    /// Enable or disable ANSI colors in log output.
    pub fn with_ansi(mut self, ansi: bool) -> Self {
        self.ansi = ansi;
        self
    }

    /// Enable or disable span event logging (entry/exit from #[instrument]).
    pub fn with_spans(mut self, log_spans: bool) -> Self {
        self.log_spans = log_spans;
        self
    }

    /// Enable or disable span context display (the span path before log messages).
    pub fn with_span_context(mut self, show_span_context: bool) -> Self {
        self.show_span_context = show_span_context;
        self
    }
}

impl std::fmt::Display for LogFormat {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            LogFormat::Full => write!(f, "full"),
            LogFormat::Compact => write!(f, "compact"),
            LogFormat::Pretty => write!(f, "pretty"),
            LogFormat::Json => write!(f, "json"),
            LogFormat::Minimal => write!(f, "minimal"),
        }
    }
}

impl std::str::FromStr for LogFormat {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        match s.to_lowercase().as_str() {
            "full" => Ok(LogFormat::Full),
            "compact" => Ok(LogFormat::Compact),
            "pretty" => Ok(LogFormat::Pretty),
            "json" => Ok(LogFormat::Json),
            _ => Err(format!(
                "Invalid log format: '{}'. Valid options: full, compact, pretty, json",
                s
            )),
        }
    }
}

/// Check if we're running in a local development environment
fn is_local_environment() -> bool {
    // Check if we're using local Anvil chain (chain_id 31337)
    std::env::var("CHAIN_ID")
        .unwrap_or_else(|_| "31337".to_string())
        .parse::<u64>()
        .unwrap_or(31337)
        == 31337
}

/// Custom formatter that omits span context (the span path before messages)
struct NoSpanContextFormatter;

impl<S, N> FormatEvent<S, N> for NoSpanContextFormatter
where
    S: Subscriber + for<'a> tracing_subscriber::registry::LookupSpan<'a>,
    N: for<'writer> tracing_subscriber::fmt::FormatFields<'writer> + 'static,
{
    fn format_event(
        &self,
        ctx: &FmtContext<'_, S, N>,
        mut writer: tracing_subscriber::fmt::format::Writer<'_>,
        event: &Event<'_>,
    ) -> fmt::Result {
        use tracing_subscriber::fmt::time::{FormatTime, SystemTime};

        let metadata = event.metadata();
        let time_format = SystemTime;

        // Write timestamp
        time_format.format_time(&mut writer)?;
        write!(&mut writer, " ")?;

        // Write level
        write!(&mut writer, "{:5} ", metadata.level())?;

        // Write target (module path) if available
        if let Some(module_path) = metadata.module_path() {
            write!(&mut writer, "{}: ", module_path)?;
        }

        // Format fields (the actual message and fields) - this is where the span context would normally appear
        // By not including span context here, we hide the span path
        // Note: format_fields consumes the writer, but we can write the newline by creating a new writer
        // that wraps the same underlying buffer
        let mut field_writer = writer.by_ref();
        ctx.field_format().format_fields(field_writer, event)?;
        // Add newline after the message
        if is_local_environment() {
            writeln!(&mut writer)?;
        }
        Ok(())
    }
}

/// Initialize logger with LoggerConfig
pub fn init_logger(config: LoggerConfig) {
    // try to get from env first, fallback to config default
    let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.env_filter));

    // Configure span events based on log_spans setting
    let span_events = if config.log_spans { FmtSpan::FULL } else { FmtSpan::NONE };

    let _ = match config.format {
        LogFormat::Full => {
            let builder = tracing_subscriber::fmt()
                .with_env_filter(env_filter)
                .with_ansi(config.ansi)
                .with_span_events(span_events);

            // When span context is disabled, use custom formatter that omits span path
            if !config.show_span_context {
                builder.event_format(NoSpanContextFormatter).try_init()
            } else {
                builder.try_init()
            }
        }
        LogFormat::Compact => {
            let builder = tracing_subscriber::fmt()
                .compact()
                .with_env_filter(env_filter)
                .with_ansi(config.ansi)
                .with_span_events(span_events);

            if !config.show_span_context {
                builder
                    .event_format(
                        tracing_subscriber::fmt::format()
                            .with_target(true)
                            .with_level(true)
                            .compact(),
                    )
                    .try_init()
            } else {
                builder.try_init()
            }
        }
        LogFormat::Pretty => {
            let builder = tracing_subscriber::fmt()
                .pretty()
                .with_env_filter(env_filter)
                .with_ansi(config.ansi)
                .with_span_events(span_events);

            if !config.show_span_context {
                builder.event_format(NoSpanContextFormatter).try_init()
            } else {
                builder.try_init()
            }
        }
        LogFormat::Json => tracing_subscriber::fmt()
            .json()
            .with_env_filter(env_filter)
            .with_ansi(config.ansi)
            .with_span_events(span_events)
            .with_current_span(true) // Include current span name for Datadog log correlation
            .with_span_list(true) // Include full span hierarchy for trace correlation
            .flatten_event(true) // Flatten event fields for easier Datadog parsing
            .try_init(),
        LogFormat::Minimal => {
            let format = tracing_subscriber::fmt::format()
                .with_target(false)
                .with_level(false)
                .without_time()
                .compact();
            tracing_subscriber::fmt()
                .with_target(false)
                .with_level(false)
                .without_time()
                .with_file(false)
                .with_line_number(false)
                .event_format(format)
                .with_env_filter(env_filter)
                .with_ansi(config.ansi)
                .with_span_events(span_events)
                .try_init()
        }
    };
}