tracing-better-stack 0.1.0

A tracing-subscriber layer for Better Stack (Logtail) logging
Documentation
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// Represents a single log event to be sent to Better Stack.
/// This struct is serializable to both JSON and MessagePack formats.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) struct LogEvent {
    /// ISO 8601 timestamp of when the event occurred
    #[serde(rename = "timestamp")]
    pub timestamp: DateTime<Utc>,

    /// Log level (TRACE, DEBUG, INFO, WARN, ERROR)
    pub level: String,

    /// The main log message
    pub message: String,

    /// The target/module that generated the log
    #[serde(skip_serializing_if = "Option::is_none")]
    pub target: Option<String>,

    /// File and line number where the log was generated
    #[serde(skip_serializing_if = "Option::is_none")]
    pub location: Option<LogLocation>,

    /// Additional structured fields
    #[serde(skip_serializing_if = "HashMap::is_empty")]
    pub fields: HashMap<String, LogValue>,

    /// Span information if include_spans is enabled
    #[serde(skip_serializing_if = "Option::is_none")]
    pub span: Option<SpanInfo>,
}

/// Represents the source location of a log event
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct LogLocation {
    pub file: Option<String>,
    pub line: Option<u32>,
}

/// Represents span information for contextualized logging
#[derive(Debug, Clone, Serialize, Deserialize)]
pub(crate) struct SpanInfo {
    pub name: String,
    pub fields: HashMap<String, LogValue>,
}

/// Represents a value in the log fields
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub(crate) enum LogValue {
    String(String),
    Number(i64),
    Float(f64),
    Bool(bool),
    Array(Vec<LogValue>),
    Object(HashMap<String, LogValue>),
}

impl From<String> for LogValue {
    fn from(s: String) -> Self {
        LogValue::String(s)
    }
}

impl From<&str> for LogValue {
    fn from(s: &str) -> Self {
        LogValue::String(s.to_string())
    }
}

impl From<i64> for LogValue {
    fn from(n: i64) -> Self {
        LogValue::Number(n)
    }
}

impl From<f64> for LogValue {
    fn from(f: f64) -> Self {
        LogValue::Float(f)
    }
}

impl From<bool> for LogValue {
    fn from(b: bool) -> Self {
        LogValue::Bool(b)
    }
}

impl LogEvent {
    /// Creates a new LogEvent with the given level and message
    pub fn new(level: impl Into<String>, message: impl Into<String>) -> Self {
        Self {
            timestamp: Utc::now(),
            level: level.into(),
            message: message.into(),
            target: None,
            location: None,
            fields: HashMap::new(),
            span: None,
        }
    }

    /// Sets the target/module for this log event
    pub fn with_target(mut self, target: impl Into<String>) -> Self {
        self.target = Some(target.into());
        self
    }

    /// Sets the source location for this log event
    pub fn with_location(mut self, file: Option<String>, line: Option<u32>) -> Self {
        if file.is_some() || line.is_some() {
            self.location = Some(LogLocation { file, line });
        }
        self
    }

    /// Adds a field to this log event
    pub fn add_field(&mut self, key: impl Into<String>, value: impl Into<LogValue>) {
        self.fields.insert(key.into(), value.into());
    }

    /// Sets span information for this log event
    pub fn with_span(mut self, name: String, fields: HashMap<String, LogValue>) -> Self {
        self.span = Some(SpanInfo { name, fields });
        self
    }
}