soltrace 0.1.1

Structured, Borsh-serialized Anchor events as a drop-in replacement for msg! in Solana programs.
Documentation
//! Log severity levels for SolTrace events.

use anchor_lang::prelude::*;
use core::fmt;

/// Severity level of a [`SolTraceEvent`](crate::SolTraceEvent).
///
/// Discriminants are stable across versions: they are `#[repr(u8)]` and
/// encoded directly in the Borsh wire format. An off-chain decoder can read
/// the level from a single byte without deserializing the full event.
/// Discriminant values will not change without a major semver bump.
#[repr(u8)]
#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, PartialEq, Eq, Debug)]
pub enum LogLevel {
    /// Fine-grained tracing, useful only during active development.
    /// Avoid in production; the overhead of even silent logging adds up.
    Trace = 0,
    /// Internal state snapshots for diagnosing logic bugs.
    /// Disable in mainnet builds via the `devnet-only` feature.
    Debug = 1,
    /// Confirmation that the instruction completed a meaningful step.
    /// Safe to leave enabled in production for indexer consumption.
    Info = 2,
    /// Something unexpected happened but the instruction can continue.
    /// Always emit; downstream monitors should alert on these.
    Warn = 3,
    /// A specific operation failed. The instruction may still return `Ok(())`
    /// if it recovered, but the failure is recorded for off-chain analysis.
    Error = 4,
}

impl LogLevel {
    /// Returns the level name as a `&'static str`, avoiding any heap allocation.
    ///
    /// Prefer this over `to_string()` in contexts where you do not need an owned
    /// `String`, such as when writing to a fixed-size buffer.
    pub fn as_str(&self) -> &'static str {
        match self {
            LogLevel::Trace => "trace",
            LogLevel::Debug => "debug",
            LogLevel::Info => "info",
            LogLevel::Warn => "warn",
            LogLevel::Error => "error",
        }
    }
}

impl fmt::Display for LogLevel {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(self.as_str())
    }
}

impl From<LogLevel> for u8 {
    /// Returns the stable wire discriminant for this level.
    ///
    /// Matches the `#[repr(u8)]` discriminant, so decoders that read raw Borsh
    /// bytes can call `u8::from(level)` to identify the level without full
    /// deserialization.
    fn from(level: LogLevel) -> u8 {
        level as u8
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn display_matches_as_str() {
        let cases = [
            (LogLevel::Trace, "trace"),
            (LogLevel::Debug, "debug"),
            (LogLevel::Info, "info"),
            (LogLevel::Warn, "warn"),
            (LogLevel::Error, "error"),
        ];
        for (level, expected) in cases {
            assert_eq!(level.to_string(), expected);
            assert_eq!(level.as_str(), expected);
        }
    }

    /// Discriminant stability regression test.
    ///
    /// These values are baked into the Borsh wire format. A change here is a
    /// breaking change for every off-chain decoder. If this test fails, a major
    /// version bump is required before merging.
    #[test]
    fn discriminant_stability() {
        assert_eq!(u8::from(LogLevel::Trace), 0);
        assert_eq!(u8::from(LogLevel::Debug), 1);
        assert_eq!(u8::from(LogLevel::Info), 2);
        assert_eq!(u8::from(LogLevel::Warn), 3);
        assert_eq!(u8::from(LogLevel::Error), 4);
    }
}