Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 8 additions & 3 deletions crates/bridges/evm/erigon-bridge/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
6 changes: 4 additions & 2 deletions crates/bridges/evm/erigon-bridge/src/metrics.rs
Original file line number Diff line number Diff line change
@@ -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,
};
3 changes: 3 additions & 0 deletions crates/phaser-metrics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
7 changes: 7 additions & 0 deletions crates/phaser-metrics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
205 changes: 205 additions & 0 deletions crates/phaser-metrics/src/tracing_layer.rs
Original file line number Diff line number Diff line change
@@ -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<IntCounterVec> = 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<Level>,
}

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<String>) -> Self {
Self {
service_name: service_name.into(),
..Default::default()
}
}

/// Set custom tracked levels
pub fn with_levels(mut self, levels: Vec<Level>) -> 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<String>) -> 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<S> Layer<S> 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"));
}
}
13 changes: 10 additions & 3 deletions crates/phaser-query/src/bin/phaser-query.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions crates/phaser-query/src/sync/metrics.rs
Original file line number Diff line number Diff line change
@@ -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};