libobservability-rs 0.1.1

A library for observability
Documentation
pub use tracing::{debug, error, info, warn};
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};

#[derive(Debug, Clone)]
pub enum TracingBackend {
    Console,
    CloudWatch,
    #[cfg(feature = "sentry")]
    Sentry,
}
use std::str::FromStr;

impl FromStr for TracingBackend {
    type Err = String;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        TracingBackend::parse(s)
    }
}

impl TracingBackend {
    fn parse(backend: &str) -> Result<Self, String> {
        match backend.to_lowercase().as_str() {
            "console" => Ok(Self::Console),
            "cloudwatch" => Ok(Self::CloudWatch),
            #[cfg(feature = "sentry")]
            "sentry" => Ok(Self::Sentry),
            #[cfg(not(feature = "sentry"))]
            "sentry" => Err(
                "Sentry feature not enabled. Enable the 'sentry' feature to use this backend."
                    .to_string(),
            ),
            other => Err(format!(
                "Invalid tracing backend '{}'. Supported backends: console, cloudwatch{}",
                other,
                if cfg!(feature = "sentry") {
                    ", sentry"
                } else {
                    ""
                }
            )),
        }
    }
}

impl std::fmt::Display for TracingBackend {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Console => write!(f, "console"),
            Self::CloudWatch => write!(f, "cloudwatch"),
            #[cfg(feature = "sentry")]
            Self::Sentry => write!(f, "sentry"),
        }
    }
}

#[derive(Debug, Clone)]
pub struct TracingConfig {
    pub backend: TracingBackend,
    pub log_level: Option<String>,
    pub json_format: bool,
    pub include_timestamp: bool,
    pub include_target: bool,
    pub include_ansi: bool,
}

impl Default for TracingConfig {
    fn default() -> Self {
        Self {
            backend: TracingBackend::Console,
            log_level: None,
            json_format: false,
            include_timestamp: true,
            include_target: true,
            include_ansi: true,
        }
    }
}

pub struct Tracing {
    config: TracingConfig,
}

impl Default for Tracing {
    fn default() -> Self {
        Self::new()
    }
}

impl Tracing {
    pub fn new() -> Self {
        Self {
            config: TracingConfig::default(),
        }
    }

    pub fn with_backend(mut self, backend: impl AsRef<str>) -> Result<Self, String> {
        self.config.backend = TracingBackend::from_str(backend.as_ref())?;
        Ok(self)
    }

    pub fn with_backend_enum(mut self, backend: TracingBackend) -> Self {
        self.config.backend = backend;
        self
    }

    pub fn with_log_level(mut self, level: impl Into<String>) -> Self {
        self.config.log_level = Some(level.into());
        self
    }

    pub fn with_json_format(mut self, json: bool) -> Self {
        self.config.json_format = json;
        self
    }

    pub fn with_timestamp(mut self, timestamp: bool) -> Self {
        self.config.include_timestamp = timestamp;
        self
    }

    pub fn with_target(mut self, target: bool) -> Self {
        self.config.include_target = target;
        self
    }

    pub fn with_ansi(mut self, ansi: bool) -> Self {
        self.config.include_ansi = ansi;
        self
    }

    fn init_console(&self) -> Result<(), String> {
        let env_filter = if let Some(ref level) = self.config.log_level {
            EnvFilter::try_new(level).map_err(|e| format!("Invalid log level: {e}"))?
        } else {
            EnvFilter::from_default_env()
        };

        if self.config.json_format {
            if self.config.include_timestamp {
                let json_layer = fmt::layer()
                    .json()
                    .with_ansi(self.config.include_ansi)
                    .with_target(self.config.include_target);

                tracing_subscriber::registry()
                    .with(json_layer)
                    .with(env_filter)
                    .try_init()
                    .map_err(|e| format!("Failed to initialize tracing: {e}"))?;
            } else {
                let json_layer = fmt::layer()
                    .json()
                    .with_ansi(self.config.include_ansi)
                    .with_target(self.config.include_target)
                    .without_time();

                tracing_subscriber::registry()
                    .with(json_layer)
                    .with(env_filter)
                    .try_init()
                    .map_err(|e| format!("Failed to initialize tracing: {e}"))?;
            }
        } else if self.config.include_timestamp {
            let layer = fmt::layer()
                .with_ansi(self.config.include_ansi)
                .with_target(self.config.include_target);

            tracing_subscriber::registry()
                .with(layer)
                .with(env_filter)
                .try_init()
                .map_err(|e| format!("Failed to initialize tracing: {e}"))?;
        } else {
            let layer = fmt::layer()
                .with_ansi(self.config.include_ansi)
                .with_target(self.config.include_target)
                .without_time();

            tracing_subscriber::registry()
                .with(layer)
                .with(env_filter)
                .try_init()
                .map_err(|e| format!("Failed to initialize tracing: {e}"))?;
        }

        Ok(())
    }

    fn init_cloudwatch(&self) -> Result<(), String> {
        let aws_layer = fmt::layer()
            .json()
            .with_current_span(false)
            .with_ansi(false)
            .without_time()
            .with_target(false);

        let env_filter = if let Some(ref level) = self.config.log_level {
            EnvFilter::try_new(level).map_err(|e| format!("Invalid log level: {e}"))?
        } else {
            EnvFilter::from_default_env()
        };

        tracing_subscriber::registry()
            .with(aws_layer)
            .with(env_filter)
            .try_init()
            .map_err(|e| format!("Failed to initialize CloudWatch tracing: {e}"))?;

        Ok(())
    }

    #[cfg(feature = "sentry")]
    fn init_sentry(&self) -> Result<(), String> {
        use crate::sentry::guard;

        guard();
        let sentry_layer = sentry_tracing::layer();

        let env_filter = if let Some(ref level) = self.config.log_level {
            EnvFilter::try_new(level).map_err(|e| format!("Invalid log level: {e}"))?
        } else {
            EnvFilter::from_default_env()
        };

        tracing_subscriber::registry()
            .with(sentry_layer)
            .with(env_filter)
            .try_init()
            .map_err(|e| format!("Failed to initialize Sentry tracing: {e}"))?;

        Ok(())
    }

    pub fn build(self) -> Result<(), String> {
        match self.config.backend {
            TracingBackend::Console => {
                self.init_console()?;
                info!(
                    "Console tracing initialized with backend: {}",
                    self.config.backend
                );
            }
            TracingBackend::CloudWatch => {
                self.init_cloudwatch()?;
                info!(
                    "CloudWatch tracing initialized with backend: {}",
                    self.config.backend
                );
            }
            #[cfg(feature = "sentry")]
            TracingBackend::Sentry => {
                self.init_sentry()?;
                info!(
                    "Sentry tracing initialized with backend: {}",
                    self.config.backend
                );
            }
        }

        Ok(())
    }

    /// Convenience method for the old API - builds and panics on error
    pub fn build_or_panic(self) {
        if let Err(e) = self.build() {
            panic!("Failed to initialize tracing: {e}");
        }
    }

    /// Create a simple console tracer with default settings
    pub fn console() -> Result<(), String> {
        Self::new().build()
    }

    /// Create a CloudWatch tracer with AWS-recommended settings
    pub fn cloudwatch() -> Result<(), String> {
        Self::new()
            .with_backend_enum(TracingBackend::CloudWatch)
            .build()
    }

    /// Create a JSON console tracer for structured logging
    pub fn console_json() -> Result<(), String> {
        Self::new().with_json_format(true).build()
    }

    /// Create a tracer from environment variables
    /// Reads TRACING_BACKEND, RUST_LOG, and other common env vars
    pub fn from_env() -> Result<(), String> {
        let backend = std::env::var("TRACING_BACKEND").unwrap_or_else(|_| "console".to_string());

        let mut tracer = Self::new().with_backend(&backend)?;

        // Check for JSON format preference
        if std::env::var("TRACING_JSON").unwrap_or_default() == "true" {
            tracer = tracer.with_json_format(true);
        }

        // Check for ANSI color preference
        if std::env::var("NO_COLOR").is_ok() {
            tracer = tracer.with_ansi(false);
        }

        tracer.build()
    }
}