rust_observer 0.2.2

Express telemetry rust SDK
Documentation
mod config;
pub mod context;
mod exporters;
pub mod logging;
pub mod metrics;
pub mod tracing;

use crate::config::{ExporterType, ObservabilityConfig};
use crate::context::initialize_global_context;
use crate::exporters::stdout_exporter::{StdoutExporter, StdoutFormat};
use crate::logging::{LogLevel, LoggerBuilder};
use crate::metrics::MetricsBuilder;
use crate::tracing::TracingBuilder;

pub use chrono;
pub use serde_json;

pub fn initialize_observability(config_path: &str) -> Result<(), Box<dyn std::error::Error>> {
    let config = ObservabilityConfig::from_file(config_path)?;

    initialize_global_context(
        config.project.name.clone(),
        config.project.app_name.clone(),
        config.project.service_name.clone(),
    );

    // Initialize logging
    if config.logging.enabled {
        let mut log_builder = LoggerBuilder::new();
        log_builder = log_builder.with_log_level(match config.logging.level.as_str() {
            "DEBUG" => LogLevel::DEBUG,
            "INFO" => LogLevel::INFO,
            "WARN" => LogLevel::WARN,
            "ERROR" => LogLevel::ERROR,
            _ => LogLevel::INFO,
        });

        match config.logging.exporter.r#type {
            ExporterType::Stdout => {
                let format = match config
                    .logging
                    .exporter
                    .properties
                    .get("format")
                    .and_then(|v| v.as_str())
                {
                    Some("minimal") => StdoutFormat::Minimal,
                    _ => StdoutFormat::Extended,
                };
                log_builder = log_builder.with_exporter(Box::new(StdoutExporter::new(format)));
            } // Add other exporter types here
        }

        log_builder.initialize();
    }

    // Initialize tracing
    if config.tracing.enabled {
        let mut trace_builder = TracingBuilder::new();
        match config.tracing.exporter.r#type {
            ExporterType::Stdout => {
                let format = match config
                    .tracing
                    .exporter
                    .properties
                    .get("format")
                    .and_then(|v| v.as_str())
                {
                    Some("minimal") => StdoutFormat::Minimal,
                    _ => StdoutFormat::Extended,
                };
                trace_builder = trace_builder.with_exporter(Box::new(StdoutExporter::new(format)));
            } // Add other exporter types here
        }
        trace_builder.initialize();
    }

    // Initialize metrics
    if config.metrics.enabled {
        let mut metrics_builder = MetricsBuilder::new();
        metrics_builder = metrics_builder.with_interval_secs(config.metrics.interval_secs);

        match config.metrics.exporter.r#type {
            ExporterType::Stdout => {
                let format = match config
                    .metrics
                    .exporter
                    .properties
                    .get("format")
                    .and_then(|v| v.as_str())
                {
                    Some("minimal") => StdoutFormat::Minimal,
                    _ => StdoutFormat::Extended,
                };
                metrics_builder =
                    metrics_builder.with_exporter(Box::new(StdoutExporter::new(format)));
            } // Add other exporter types here
        }

        if config.metrics.http.enabled {
            metrics_builder = metrics_builder.with_http_metrics();
        }
        if config.metrics.grpc.enabled {
            metrics_builder = metrics_builder.with_grpc_metrics();
        }
        if config.metrics.websocket.enabled {
            metrics_builder = metrics_builder.with_websocket_metrics();
        }
        if config.metrics.system.enabled {
            metrics_builder = metrics_builder.with_system_metrics();
        }

        if config.metrics.process.enabled {
            metrics_builder = metrics_builder.with_process_metrics();
        }

        metrics_builder.initialize();
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::config::ObservabilityConfig;
    use crate::logging::{LogLevel, LogMessage};
    use crate::metrics::{Metrics, SystemMetrics};
    use crate::tracing::{SpanStatus, SpanType, TraceContext};
    use chrono::Utc;
    use std::collections::HashMap;

    #[test]
    fn test_config_parsing() {
        let config_str = r#"
            [project]
            name = "test_project"
            app_name = "test_app"
            service_name = "test_service"

            [logging]
            enabled = true
            level = "INFO"
            [logging.exporter]
            type = "stdout"
            format = "extended"

            [tracing]
            enabled = true
            [tracing.exporter]
            type = "stdout"
            format = "extended"

            [metrics]
            enabled = true
            interval_secs = 10
            [metrics.exporter]
            type = "stdout"
            format = "extended"
            [metrics.http]
            enabled = true
            [metrics.grpc]
            enabled = false
            [metrics.websocket]
            enabled = false
            [metrics.system]
            enabled = true
            [metrics.process]
            enabled = true
        "#;

        let config: ObservabilityConfig = toml::from_str(config_str).unwrap();

        assert_eq!(config.project.name, "test_project");
        assert_eq!(config.project.app_name, "test_app");
        assert_eq!(config.project.service_name, "test_service");
        assert!(config.logging.enabled);
        assert_eq!(config.logging.level, "INFO");
        assert!(config.tracing.enabled);
        assert!(config.metrics.enabled);
        assert_eq!(config.metrics.interval_secs, 10);
        assert!(config.metrics.http.enabled);
        assert!(!config.metrics.grpc.enabled);
        assert!(!config.metrics.websocket.enabled);
        assert!(config.metrics.system.enabled);
        assert!(config.metrics.process.enabled);
    }

    #[test]
    fn test_log_message_creation() {
        let log_message = LogMessage {
            timestamp: Utc::now(),
            level: LogLevel::INFO,
            project_name: "test_project".to_string(),
            app_name: "test_app".to_string(),
            service_name: "test_service".to_string(),
            replica_id: "test_replica".to_string(),
            target: "test_target",
            message: "Test log message".into(),
            context: None,
            attributes: HashMap::new(),
        };

        assert_eq!(log_message.level, LogLevel::INFO);
        assert_eq!(log_message.project_name, "test_project");
        assert_eq!(log_message.message, "Test log message");
    }

    #[test]
    fn test_metrics_creation() {
        let metrics = Metrics {
            timestamp: 0,
            interval_secs: 10,
            project_name: "test_project".to_string(),
            app_name: "test_app".to_string(),
            service_name: "test_service".to_string(),
            replica_id: "test_replica".to_string(),
            system_metrics: Some(SystemMetrics {
                cpu_usage: 50.0,
                cpu_cores_usage: vec![40.0, 60.0].into(),
                memory_total: 16000000000,
                memory_used: 8000000000,
                swap_total: 8000000000,
                swap_used: 1000000000,
                disk_total_space: 500000000000,
                disk_available_space: 250000000000,
            }),
            process_metrics: None,
            http_metrics: None,
            grpc_metrics: None,
            websocket_metrics: None,
        };

        assert_eq!(metrics.interval_secs, 10);
        assert_eq!(metrics.project_name, "test_project");
        assert!(metrics.system_metrics.is_some());
        let system_metrics = metrics.system_metrics.unwrap();
        assert_eq!(system_metrics.cpu_usage, 50.0);
        assert_eq!(system_metrics.memory_total, 16000000000);
    }

    #[test]
    fn test_trace_context_creation() {
        let trace_context = TraceContext::new(SpanType::Internal, "test_operation".to_string());

        assert_eq!(trace_context.span_type, SpanType::Internal.as_str());
        assert_eq!(trace_context.operation_name, "test_operation");
        assert!(trace_context.end_ts.is_none());
        assert_eq!(
            *trace_context.span_status.lock().unwrap(),
            SpanStatus::InProgress
        );
    }

    #[test]
    fn test_trace_context_end_span() {
        let mut trace_context = TraceContext::new(SpanType::Internal, "test_operation".to_string());
        trace_context.end_span();

        assert!(trace_context.end_ts.is_some());
        assert_eq!(*trace_context.span_status.lock().unwrap(), SpanStatus::OK);
    }

    #[test]
    fn test_trace_context_signal_error() {
        let trace_context = TraceContext::new(SpanType::Internal, "test_operation".to_string());
        trace_context.signal_error();

        assert_eq!(
            *trace_context.span_status.lock().unwrap(),
            SpanStatus::Error
        );
    }
}