use std::{
collections::{HashMap, HashSet},
time::Duration,
};
use metrics::gauge;
use tokio::time::{interval, MissedTickBehavior};
use tycho_simulation::tycho_client::feed::SynchronizerState;
use crate::feed::market_data::MarketData;
pub(crate) struct MetricsSampler {
market_data: MarketData,
sample_interval: Duration,
}
impl MetricsSampler {
pub(crate) fn new(market_data: MarketData, sample_interval: Duration) -> Self {
Self { market_data, sample_interval }
}
pub(crate) async fn run(&self) {
let mut ticker = interval(self.sample_interval);
ticker.set_missed_tick_behavior(MissedTickBehavior::Skip);
loop {
ticker.tick().await;
let Some(state) = self.market_data.try_read() else {
continue;
};
let pool_counts = state.pool_counts_by_protocol().clone();
let sync_states = state.protocol_sync_states().clone();
drop(state);
emit_market_metrics(&pool_counts, &sync_states);
}
}
}
fn emit_market_metrics(
pool_counts: &HashMap<String, u64>,
sync_states: &HashMap<String, SynchronizerState>,
) {
let protocols: HashSet<&String> = pool_counts
.keys()
.chain(sync_states.keys())
.collect();
for protocol in protocols {
let count = pool_counts
.get(protocol)
.copied()
.unwrap_or(0);
gauge!("market_pools_per_protocol", "protocol" => protocol.clone()).set(count as f64);
}
for (protocol, sync_state) in sync_states {
gauge!("market_protocol_sync_state", "protocol" => protocol.clone())
.set(sync_state_code(sync_state));
if let Some(block_number) = sync_state_block_number(sync_state) {
gauge!("market_protocol_block", "protocol" => protocol.clone())
.set(block_number as f64);
}
}
}
fn sync_state_code(sync_state: &SynchronizerState) -> f64 {
match sync_state {
SynchronizerState::Started => 1.0,
SynchronizerState::Ready(_) => 2.0,
SynchronizerState::Delayed(_) => 3.0,
SynchronizerState::Stale(_) => 4.0,
SynchronizerState::Advanced(_) => 5.0,
SynchronizerState::Ended(_) => 6.0,
}
}
fn sync_state_block_number(sync_state: &SynchronizerState) -> Option<u64> {
match sync_state {
SynchronizerState::Started | SynchronizerState::Ended(_) => None,
SynchronizerState::Ready(header) |
SynchronizerState::Delayed(header) |
SynchronizerState::Stale(header) |
SynchronizerState::Advanced(header) => Some(header.number),
}
}
#[cfg(test)]
mod tests {
use std::collections::HashMap;
use metrics_util::debugging::{DebugValue, DebuggingRecorder};
use tycho_simulation::tycho_client::feed::{BlockHeader, SynchronizerState};
use super::*;
fn header(number: u64) -> BlockHeader {
BlockHeader { number, ..Default::default() }
}
fn find_gauge(
recorded: &[(
metrics_util::CompositeKey,
Option<metrics::Unit>,
Option<metrics::SharedString>,
DebugValue,
)],
name: &str,
labels: &[(&str, &str)],
) -> f64 {
let value = recorded
.iter()
.find(|(key, _, _, _)| {
key.key().name() == name &&
labels
.iter()
.all(|(label_key, label_value)| {
key.key()
.labels()
.any(|l| l.key() == *label_key && l.value() == *label_value)
})
})
.map(|(_, _, _, value)| value)
.unwrap_or_else(|| panic!("missing {name}{labels:?}, got {recorded:?}"));
match value {
DebugValue::Gauge(v) => v.0,
other => panic!("expected gauge for {name}{labels:?}, got {other:?}"),
}
}
#[test]
fn emit_market_metrics_records_pool_and_sync_gauges() {
let recorder = DebuggingRecorder::new();
let snapshotter = recorder.snapshotter();
let pool_counts =
HashMap::from([("uniswap_v2".to_string(), 100u64), ("curve".to_string(), 5u64)]);
let sync_states = HashMap::from([
("uniswap_v2".to_string(), SynchronizerState::Ready(header(42))),
("uniswap_v3".to_string(), SynchronizerState::Stale(header(30))),
]);
metrics::with_local_recorder(&recorder, || {
emit_market_metrics(&pool_counts, &sync_states);
});
let recorded = snapshotter.snapshot().into_vec();
assert_eq!(
find_gauge(&recorded, "market_pools_per_protocol", &[("protocol", "uniswap_v2")]),
100.0
);
assert_eq!(
find_gauge(&recorded, "market_pools_per_protocol", &[("protocol", "curve")]),
5.0
);
assert_eq!(
find_gauge(&recorded, "market_pools_per_protocol", &[("protocol", "uniswap_v3")]),
0.0
);
assert_eq!(
find_gauge(&recorded, "market_protocol_sync_state", &[("protocol", "uniswap_v2")]),
2.0
);
assert_eq!(
find_gauge(&recorded, "market_protocol_sync_state", &[("protocol", "uniswap_v3")]),
4.0
);
assert_eq!(
find_gauge(&recorded, "market_protocol_block", &[("protocol", "uniswap_v2")]),
42.0
);
assert_eq!(
find_gauge(&recorded, "market_protocol_block", &[("protocol", "uniswap_v3")]),
30.0
);
}
}