acton_htmx/observability/
mod.rs

1//! Observability (logging, tracing, metrics)
2//!
3//! Provides structured logging, distributed tracing, and metrics collection
4//! via OpenTelemetry integration.
5
6pub mod metrics;
7
8use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
9
10/// Initialize observability stack
11///
12/// Sets up:
13/// - Structured logging with JSON formatting (production) or pretty formatting (dev)
14/// - Environment-based log level filtering
15/// - Request ID correlation
16///
17/// # Errors
18///
19/// Returns an error if:
20/// - The tracing subscriber global default cannot be set (already initialized)
21/// - Environment filter parsing fails for invalid `RUST_LOG` values
22///
23/// # Example
24///
25/// ```rust,no_run
26/// use acton_htmx::observability;
27///
28/// # fn main() -> anyhow::Result<()> {
29/// observability::init()?;
30/// tracing::info!("Application started");
31/// # Ok(())
32/// # }
33/// ```
34pub fn init() -> anyhow::Result<()> {
35    let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
36        if cfg!(debug_assertions) {
37            EnvFilter::new("debug,acton_htmx=trace")
38        } else {
39            EnvFilter::new("info")
40        }
41    });
42
43    #[cfg(debug_assertions)]
44    {
45        // Pretty formatting for development
46        tracing_subscriber::registry()
47            .with(env_filter)
48            .with(tracing_subscriber::fmt::layer().pretty())
49            .init();
50    }
51
52    #[cfg(not(debug_assertions))]
53    {
54        // JSON formatting for production
55        tracing_subscriber::registry()
56            .with(env_filter)
57            .with(tracing_subscriber::fmt::layer().json())
58            .init();
59    }
60
61    Ok(())
62}
63
64/// Observability configuration
65#[derive(Debug, Clone)]
66pub struct ObservabilityConfig {
67    /// Service name for tracing
68    pub service_name: String,
69
70    /// Enable OpenTelemetry metrics
71    pub metrics_enabled: bool,
72
73    /// Enable distributed tracing
74    pub tracing_enabled: bool,
75}
76
77impl Default for ObservabilityConfig {
78    fn default() -> Self {
79        Self {
80            service_name: "acton-htmx".to_string(),
81            metrics_enabled: false,
82            tracing_enabled: false,
83        }
84    }
85}
86
87impl ObservabilityConfig {
88    /// Create new observability config
89    pub fn new(service_name: impl Into<String>) -> Self {
90        Self {
91            service_name: service_name.into(),
92            ..Default::default()
93        }
94    }
95
96    /// Enable metrics collection
97    #[must_use]
98    pub const fn with_metrics(mut self) -> Self {
99        self.metrics_enabled = true;
100        self
101    }
102
103    /// Enable distributed tracing
104    #[must_use]
105    pub const fn with_tracing(mut self) -> Self {
106        self.tracing_enabled = true;
107        self
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114
115    #[test]
116    fn test_default_config() {
117        let config = ObservabilityConfig::default();
118        assert_eq!(config.service_name, "acton-htmx");
119        assert!(!config.metrics_enabled);
120        assert!(!config.tracing_enabled);
121    }
122
123    #[test]
124    fn test_builder() {
125        let config = ObservabilityConfig::new("my-app")
126            .with_metrics()
127            .with_tracing();
128
129        assert_eq!(config.service_name, "my-app");
130        assert!(config.metrics_enabled);
131        assert!(config.tracing_enabled);
132    }
133}