diff --git a/Cargo.lock b/Cargo.lock index e5a6b10..d525d9d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4231,8 +4231,11 @@ dependencies = [ name = "phaser-metrics" version = "0.1.0" dependencies = [ + "once_cell", "phaser-bridge", "prometheus", + "tracing", + "tracing-subscriber", ] [[package]] diff --git a/crates/bridges/evm/erigon-bridge/src/main.rs b/crates/bridges/evm/erigon-bridge/src/main.rs index 87cd31c..708fdb1 100644 --- a/crates/bridges/evm/erigon-bridge/src/main.rs +++ b/crates/bridges/evm/erigon-bridge/src/main.rs @@ -83,12 +83,17 @@ struct Args { #[tokio::main] async fn main() -> Result<()> { - // Initialize tracing - tracing_subscriber::fmt() - .with_env_filter( + // Initialize tracing with metrics layer + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + + tracing_subscriber::registry() + .with( EnvFilter::try_from_default_env() .unwrap_or_else(|_| EnvFilter::new("erigon_bridge=info")), ) + .with(tracing_subscriber::fmt::layer()) + .with(metrics::MetricsLayer::new("erigon-bridge")) .init(); let args = Args::parse(); diff --git a/crates/bridges/evm/erigon-bridge/src/metrics.rs b/crates/bridges/evm/erigon-bridge/src/metrics.rs index 92d8bd6..295ed07 100644 --- a/crates/bridges/evm/erigon-bridge/src/metrics.rs +++ b/crates/bridges/evm/erigon-bridge/src/metrics.rs @@ -1,5 +1,7 @@ //! Prometheus metrics for erigon-bridge //! -//! Re-exports BridgeMetrics from phaser-metrics crate +//! Re-exports BridgeMetrics and MetricsLayer from phaser-metrics crate -pub use phaser_metrics::{gather_metrics, BridgeMetrics, SegmentMetrics, WorkerStage}; +pub use phaser_metrics::{ + gather_metrics, BridgeMetrics, MetricsLayer, SegmentMetrics, WorkerStage, +}; diff --git a/crates/phaser-metrics/Cargo.toml b/crates/phaser-metrics/Cargo.toml index cdcd516..cb8ab2f 100644 --- a/crates/phaser-metrics/Cargo.toml +++ b/crates/phaser-metrics/Cargo.toml @@ -6,3 +6,6 @@ edition = "2021" [dependencies] phaser-bridge = { path = "../phaser-bridge" } prometheus = "0.13" +tracing = "0.1" +tracing-subscriber = "0.3" +once_cell = "1.0" diff --git a/crates/phaser-metrics/src/lib.rs b/crates/phaser-metrics/src/lib.rs index 0b91330..912b4c0 100644 --- a/crates/phaser-metrics/src/lib.rs +++ b/crates/phaser-metrics/src/lib.rs @@ -3,10 +3,17 @@ //! Provides composable metrics using a trait-based pattern. Base metrics are defined //! in SegmentWorkerMetrics, and specialized metrics (BridgeMetrics, QueryMetrics) add //! service-specific metrics while inheriting common functionality via the SegmentMetrics trait. +//! +//! ## Log Metrics +//! +//! The `MetricsLayer` provides automatic tracking of log events as Prometheus metrics. +//! By default, it tracks ERROR, WARN, and INFO levels, skipping high-volume DEBUG and TRACE. mod segment_metrics; +mod tracing_layer; pub use segment_metrics::{BridgeMetrics, SegmentMetrics, SegmentWorkerMetrics, WorkerStage}; +pub use tracing_layer::{MetricsLayer, MetricsLayerConfig}; use prometheus::{register_int_gauge_vec, IntGaugeVec}; use std::sync::Arc; diff --git a/crates/phaser-metrics/src/tracing_layer.rs b/crates/phaser-metrics/src/tracing_layer.rs new file mode 100644 index 0000000..e52d533 --- /dev/null +++ b/crates/phaser-metrics/src/tracing_layer.rs @@ -0,0 +1,205 @@ +//! Tracing layer that exports log events as Prometheus metrics +//! +//! This layer intercepts tracing events (logs) and increments Prometheus counters +//! based on log level, service, module, and optionally line numbers. +//! +//! ## Design +//! +//! By default, only ERROR, WARN, and INFO levels are tracked to keep cardinality +//! manageable. DEBUG and TRACE logs are typically high-volume and not useful for +//! alerting or long-term tracking. +//! +//! ## Cardinality +//! +//! Without line numbers: ~200-500 time series (3 levels × services × modules) +//! With line numbers: ~3,000-5,000 time series (3 levels × services × log call sites) +//! +//! ## Usage +//! +//! ```rust,ignore +//! use tracing_subscriber::layer::SubscriberExt; +//! use phaser_metrics::MetricsLayer; +//! +//! tracing_subscriber::registry() +//! .with(EnvFilter::from_default_env()) +//! .with(tracing_subscriber::fmt::layer()) +//! .with(MetricsLayer::new("erigon-bridge")) +//! .init(); +//! ``` + +use once_cell::sync::OnceCell; +use prometheus::{register_int_counter_vec, IntCounterVec}; +use std::collections::HashSet; +use tracing::{Level, Subscriber}; +use tracing_subscriber::layer::{Context, Layer}; + +// Global metric registry (initialized once) +static LOG_EVENTS_TOTAL: OnceCell = OnceCell::new(); + +fn get_log_events_metric() -> &'static IntCounterVec { + LOG_EVENTS_TOTAL.get_or_init(|| { + register_int_counter_vec!( + "phaser_log_events_total", + "Total number of log events by level, service, and module", + &["level", "service", "module"] + ) + .expect("Failed to register phaser_log_events_total metric") + }) +} + +/// Configuration for the metrics layer +#[derive(Debug, Clone)] +pub struct MetricsLayerConfig { + /// Service name (e.g., "erigon-bridge", "phaser-query") + pub service_name: String, + + /// Log levels to track as metrics (default: ERROR, WARN, INFO) + /// DEBUG and TRACE are excluded by default to reduce cardinality + pub tracked_levels: HashSet, +} + +impl Default for MetricsLayerConfig { + fn default() -> Self { + let mut tracked_levels = HashSet::new(); + tracked_levels.insert(Level::ERROR); + tracked_levels.insert(Level::WARN); + tracked_levels.insert(Level::INFO); + + Self { + service_name: "phaser".to_string(), + tracked_levels, + } + } +} + +impl MetricsLayerConfig { + /// Create a new config with the given service name + pub fn new(service_name: impl Into) -> Self { + Self { + service_name: service_name.into(), + ..Default::default() + } + } + + /// Set custom tracked levels + pub fn with_levels(mut self, levels: Vec) -> Self { + self.tracked_levels = levels.into_iter().collect(); + self + } +} + +/// Tracing layer that exports log events as Prometheus metrics +/// +/// Tracks log events with labels: {level, service, module} +/// - level: ERROR, WARN, INFO (configurable) +/// - service: Service name (e.g., "erigon-bridge") +/// - module: Target module path (e.g., "erigon_bridge::segment_worker") +pub struct MetricsLayer { + config: MetricsLayerConfig, +} + +impl MetricsLayer { + /// Create a new metrics layer with default configuration + /// Tracks ERROR, WARN, INFO levels by default + pub fn new(service_name: impl Into) -> Self { + Self::with_config(MetricsLayerConfig::new(service_name)) + } + + /// Create a new metrics layer with custom configuration + pub fn with_config(config: MetricsLayerConfig) -> Self { + // Initialize the global metric + let _ = get_log_events_metric(); + + Self { config } + } +} + +impl Layer for MetricsLayer +where + S: Subscriber, +{ + fn on_event(&self, event: &tracing::Event<'_>, _ctx: Context<'_, S>) { + let metadata = event.metadata(); + let level = metadata.level(); + + // Skip levels we're not tracking + if !self.config.tracked_levels.contains(level) { + return; + } + + // Extract metadata (zero-cost - computed at compile time) + let level_str = level.as_str(); + let service = self.config.service_name.as_str(); + let module = metadata.target(); + + // Increment the counter with labels: {level, service, module} + get_log_events_metric() + .with_label_values(&[level_str, service, module]) + .inc(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tracing::{error, info, warn}; + use tracing_subscriber::layer::SubscriberExt; + + #[test] + fn test_metrics_layer_basic() { + // Create a subscriber with metrics layer + let metrics_layer = MetricsLayer::new("test_service"); + + let subscriber = tracing_subscriber::registry().with(metrics_layer); + + tracing::subscriber::with_default(subscriber, || { + info!("Test info message"); + warn!("Test warn message"); + error!("Test error message"); + }); + + // Verify metrics were recorded + let metrics = crate::gather_metrics().unwrap(); + assert!(metrics.contains("phaser_log_events_total")); + assert!(metrics.contains("level=")); + assert!(metrics.contains("service=")); + assert!(metrics.contains("module=")); + } + + #[test] + fn test_metrics_layer_filters_debug() { + use tracing::debug; + + let metrics_layer = MetricsLayer::new("test_filter"); + + let subscriber = tracing_subscriber::registry().with(metrics_layer); + + tracing::subscriber::with_default(subscriber, || { + // This should not be tracked (DEBUG not in default tracked_levels) + debug!("This should not create a metric"); + info!("This should create a metric"); + }); + + // Verify metrics were recorded and no panic occurred + let metrics = crate::gather_metrics().unwrap(); + assert!(metrics.contains("phaser_log_events_total")); + } + + #[test] + fn test_custom_levels() { + // Track only ERROR level + let config = MetricsLayerConfig::new("test_custom").with_levels(vec![Level::ERROR]); + + let metrics_layer = MetricsLayer::with_config(config); + let subscriber = tracing_subscriber::registry().with(metrics_layer); + + tracing::subscriber::with_default(subscriber, || { + info!("This should not be tracked"); + error!("This should be tracked"); + }); + + // Verify no panic + let metrics = crate::gather_metrics().unwrap(); + assert!(metrics.contains("phaser_log_events_total")); + } +} diff --git a/crates/phaser-query/src/bin/phaser-query.rs b/crates/phaser-query/src/bin/phaser-query.rs index d1543e8..1a61e90 100644 --- a/crates/phaser-query/src/bin/phaser-query.rs +++ b/crates/phaser-query/src/bin/phaser-query.rs @@ -50,14 +50,21 @@ struct Args { #[tokio::main] async fn main() -> Result<()> { - // Initialize logging - tracing_subscriber::fmt() - .with_env_filter( + // Initialize logging with metrics layer + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + + tracing_subscriber::registry() + .with( tracing_subscriber::EnvFilter::from_default_env() .add_directive("phaser_query=info".parse()?) .add_directive("phaser_bridge=info".parse()?) .add_directive("erigon_bridge=info".parse()?), ) + .with(tracing_subscriber::fmt::layer()) + .with(phaser_query::sync::metrics::MetricsLayer::new( + "phaser-query", + )) .init(); let args = Args::parse(); diff --git a/crates/phaser-query/src/sync/metrics.rs b/crates/phaser-query/src/sync/metrics.rs index 4d49fdd..7a8e6ce 100644 --- a/crates/phaser-query/src/sync/metrics.rs +++ b/crates/phaser-query/src/sync/metrics.rs @@ -1,5 +1,5 @@ //! Prometheus metrics for phaser-query sync service //! -//! Re-exports QueryMetrics from phaser-metrics crate +//! Re-exports QueryMetrics and MetricsLayer from phaser-metrics crate -pub use phaser_metrics::{gather_metrics, QueryMetrics as SyncMetrics}; +pub use phaser_metrics::{gather_metrics, MetricsLayer, QueryMetrics as SyncMetrics};