Skip to content
This repository was archived by the owner on Jun 1, 2026. It is now read-only.

Commit 1687fc0

Browse files
committed
Add consumer status reporting and metrics
Introduces a new ConsumerStatusReporter trait and implementation for tracking consumer processing statistics in Redis. Adds consumer metrics collection and Prometheus reporting in the API. Refactors all consumer runners to accept a ConsumerStatusReporter, and updates the streamer crate to invoke status reporting on success and error. Also adds new primitives for consumer status, updates cacher keys, and adjusts job reporting to use a similar pattern.
1 parent fd28140 commit 1687fc0

19 files changed

Lines changed: 445 additions & 112 deletions

File tree

‎Cargo.lock‎

Lines changed: 0 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎apps/api/src/metrics/client.rs‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ impl MetricsClient {
1616
super::price::init_price_metrics(registry.registry_mut());
1717
super::fiat::init_fiat_metrics(registry.registry_mut());
1818
super::job::init_job_metrics(registry.registry_mut());
19+
super::consumer::init_consumer_metrics(registry.registry_mut());
1920

2021
Self { registry, database, cacher }
2122
}
@@ -24,6 +25,7 @@ impl MetricsClient {
2425
super::parser::update_parser_metrics(&self.database);
2526
super::price::update_price_metrics(&self.database);
2627
super::job::update_job_metrics(&self.cacher).await;
28+
super::consumer::update_consumer_metrics(&self.cacher).await;
2729

2830
self.registry.encode()
2931
}

‎apps/api/src/metrics/consumer.rs‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
use cacher::CacherClient;
2+
use primitives::ConsumerStatus;
3+
use prometheus_client::encoding::EncodeLabelSet;
4+
use prometheus_client::metrics::family::Family;
5+
use prometheus_client::metrics::gauge::Gauge;
6+
use prometheus_client::registry::Registry;
7+
use std::sync::OnceLock;
8+
9+
const CONSUMERS_STATUS_PREFIX: &str = "consumers:status:";
10+
11+
static CONSUMER_PROCESSED: OnceLock<Family<ConsumerLabels, Gauge>> = OnceLock::new();
12+
static CONSUMER_ERRORS: OnceLock<Family<ConsumerLabels, Gauge>> = OnceLock::new();
13+
static CONSUMER_LAST_SUCCESS_AT: OnceLock<Family<ConsumerLabels, Gauge>> = OnceLock::new();
14+
static CONSUMER_AVG_DURATION_MS: OnceLock<Family<ConsumerLabels, Gauge>> = OnceLock::new();
15+
static CONSUMER_UNIQUE_ERRORS: OnceLock<Family<ConsumerLabels, Gauge>> = OnceLock::new();
16+
static CONSUMER_LAST_ERROR_AT: OnceLock<Family<ConsumerLabels, Gauge>> = OnceLock::new();
17+
18+
#[derive(Clone, Debug, Hash, PartialEq, Eq, EncodeLabelSet)]
19+
struct ConsumerLabels {
20+
consumer: String,
21+
}
22+
23+
pub fn init_consumer_metrics(registry: &mut Registry) {
24+
let processed = Family::<ConsumerLabels, Gauge>::default();
25+
let errors = Family::<ConsumerLabels, Gauge>::default();
26+
let last_success = Family::<ConsumerLabels, Gauge>::default();
27+
let avg_duration = Family::<ConsumerLabels, Gauge>::default();
28+
let unique_errors = Family::<ConsumerLabels, Gauge>::default();
29+
let last_error = Family::<ConsumerLabels, Gauge>::default();
30+
31+
registry.register("consumer_processed", "Messages processed", processed.clone());
32+
registry.register("consumer_errors", "Errors encountered", errors.clone());
33+
registry.register("consumer_last_success_at", "Last successful processing (unix timestamp)", last_success.clone());
34+
registry.register("consumer_avg_duration_ms", "Average processing duration in milliseconds", avg_duration.clone());
35+
registry.register("consumer_unique_errors", "Number of unique error types", unique_errors.clone());
36+
registry.register("consumer_last_error_at", "Most recent error (unix timestamp)", last_error.clone());
37+
38+
CONSUMER_PROCESSED.set(processed).ok();
39+
CONSUMER_ERRORS.set(errors).ok();
40+
CONSUMER_LAST_SUCCESS_AT.set(last_success).ok();
41+
CONSUMER_AVG_DURATION_MS.set(avg_duration).ok();
42+
CONSUMER_UNIQUE_ERRORS.set(unique_errors).ok();
43+
CONSUMER_LAST_ERROR_AT.set(last_error).ok();
44+
}
45+
46+
pub async fn update_consumer_metrics(cacher: &CacherClient) {
47+
let keys = match cacher.keys(&format!("{}*", CONSUMERS_STATUS_PREFIX)).await {
48+
Ok(k) => k,
49+
Err(_) => return,
50+
};
51+
52+
for key in &keys {
53+
let name = key.strip_prefix(CONSUMERS_STATUS_PREFIX).unwrap_or(key);
54+
let Ok(status) = cacher.get_value::<ConsumerStatus>(key).await else {
55+
continue;
56+
};
57+
let labels = ConsumerLabels { consumer: name.to_string() };
58+
59+
if let Some(family) = CONSUMER_PROCESSED.get() {
60+
family.get_or_create(&labels).set(status.total_processed as i64);
61+
}
62+
if let Some(family) = CONSUMER_ERRORS.get() {
63+
family.get_or_create(&labels).set(status.total_errors as i64);
64+
}
65+
if let (Some(family), Some(ts)) = (CONSUMER_LAST_SUCCESS_AT.get(), status.last_success) {
66+
family.get_or_create(&labels).set(ts as i64);
67+
}
68+
if let Some(family) = CONSUMER_AVG_DURATION_MS.get() {
69+
family.get_or_create(&labels).set(status.avg_duration as i64);
70+
}
71+
if let Some(family) = CONSUMER_UNIQUE_ERRORS.get() {
72+
family.get_or_create(&labels).set(status.errors.len() as i64);
73+
}
74+
if let Some(family) = CONSUMER_LAST_ERROR_AT.get() {
75+
let last_error_at = status.errors.iter().map(|e| e.last_seen).max();
76+
if let Some(ts) = last_error_at {
77+
family.get_or_create(&labels).set(ts as i64);
78+
}
79+
}
80+
}
81+
}

‎apps/api/src/metrics/mod.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
mod client;
2+
mod consumer;
23
mod fiat;
34
mod job;
45
mod parser;

‎apps/daemon/src/consumers/fetch_nft_assets_addresses_consumer.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ use cacher::{CacheKey, CacherClient};
77
use primitives::Chain;
88
use settings::Settings;
99
use storage::Database;
10-
use streamer::{ChainAddressPayload, ConsumerConfig, QueueName, ShutdownReceiver, StreamConnection, StreamProducer, StreamReader, consumer::MessageConsumer, run_consumer};
10+
use streamer::{
11+
ChainAddressPayload, ConsumerConfig, ConsumerStatusReporter, QueueName, ShutdownReceiver, StreamConnection, StreamProducer, StreamReader, consumer::MessageConsumer,
12+
run_consumer,
13+
};
1114

1215
pub struct FetchNftAssetsAddressesConsumer {
1316
#[allow(dead_code)]
@@ -27,6 +30,7 @@ impl FetchNftAssetsAddressesConsumer {
2730
cacher: CacherClient,
2831
consumer_config: ConsumerConfig,
2932
shutdown_rx: ShutdownReceiver,
33+
reporter: Arc<dyn ConsumerStatusReporter>,
3034
) -> Result<(), Box<dyn Error + Send + Sync>> {
3135
let queue = QueueName::FetchNftAssociations;
3236
let name = format!("{}.{}", queue, chain.as_ref());
@@ -41,7 +45,7 @@ impl FetchNftAssetsAddressesConsumer {
4145
cacher,
4246
nft_client,
4347
};
44-
run_consumer::<ChainAddressPayload, Self, usize>(&name, stream_reader, queue, Some(chain.as_ref()), consumer, consumer_config, shutdown_rx).await
48+
run_consumer::<ChainAddressPayload, Self, usize>(&name, stream_reader, queue, Some(chain.as_ref()), consumer, consumer_config, shutdown_rx, reporter).await
4549
}
4650
}
4751

0 commit comments

Comments
 (0)