Skip to content

Commit 84c2535

Browse files
authored
feat(o11y): implementing metrics and Grafana panels for chain abstraction (#926)
1 parent e9cc292 commit 84c2535

File tree

8 files changed

+239
-2
lines changed

8 files changed

+239
-2
lines changed

src/handlers/chain_agnostic/route.rs

+69-2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use {
1010
ChainAbstractionInitialTxInfo, MessageSource,
1111
},
1212
error::RpcError,
13+
metrics::{ChainAbstractionNoBridgingNeededType, ChainAbstractionTransactionType},
1314
state::AppState,
1415
storage::irn::OperationType,
1516
utils::{
@@ -119,6 +120,9 @@ async fn handler_internal(
119120
"The transaction is a native token transfer with value: {:?}",
120121
transfer_value
121122
);
123+
state
124+
.metrics
125+
.add_ca_no_bridging_needed(ChainAbstractionNoBridgingNeededType::NativeTokenTransfer);
122126
return Ok(no_bridging_needed_response.into_response());
123127
}
124128
let transaction_data = initial_transaction.input.clone();
@@ -141,6 +145,9 @@ async fn handler_internal(
141145
.is_none()
142146
{
143147
error!("The destination address is not a supported bridging asset contract");
148+
state.metrics.add_ca_no_bridging_needed(
149+
ChainAbstractionNoBridgingNeededType::AssetNotSupported,
150+
);
144151
return Ok(no_bridging_needed_response.into_response());
145152
};
146153

@@ -164,6 +171,11 @@ async fn handler_internal(
164171
state.metrics.clone(),
165172
)
166173
.await?;
174+
state.metrics.add_ca_gas_estimation(
175+
simulated_gas_used,
176+
initial_transaction.chain_id.clone(),
177+
ChainAbstractionTransactionType::Transfer,
178+
);
167179
// Save the initial tx gas estimation to the cache
168180
{
169181
let state = state.clone();
@@ -222,6 +234,9 @@ async fn handler_internal(
222234
}
223235
if asset_transfer_value.is_zero() {
224236
error!("The transaction does not change any supported bridging assets");
237+
state.metrics.add_ca_no_bridging_needed(
238+
ChainAbstractionNoBridgingNeededType::AssetNotSupported,
239+
);
225240
return Ok(no_bridging_needed_response.into_response());
226241
}
227242

@@ -244,6 +259,9 @@ async fn handler_internal(
244259
Some((symbol, decimals)) => (symbol, decimals),
245260
None => {
246261
error!("The destination address is not a supported bridging asset contract");
262+
state.metrics.add_ca_no_bridging_needed(
263+
ChainAbstractionNoBridgingNeededType::AssetNotSupported,
264+
);
247265
return Ok(no_bridging_needed_response.into_response());
248266
}
249267
};
@@ -260,6 +278,9 @@ async fn handler_internal(
260278
.await?;
261279
let erc20_balance = U256::from_be_bytes(erc20_balance.into());
262280
if erc20_balance >= asset_transfer_value {
281+
state
282+
.metrics
283+
.add_ca_no_bridging_needed(ChainAbstractionNoBridgingNeededType::SufficientFunds);
263284
return Ok(no_bridging_needed_response.into_response());
264285
}
265286
let erc20_topup_value = asset_transfer_value - erc20_balance;
@@ -276,6 +297,7 @@ async fn handler_internal(
276297
)
277298
.await?
278299
else {
300+
state.metrics.add_ca_insufficient_funds();
279301
return Ok(Json(PrepareResponse::Error(PrepareResponseError {
280302
error: BridgingError::InsufficientFunds,
281303
}))
@@ -302,6 +324,14 @@ async fn handler_internal(
302324
)
303325
.await?;
304326
let Some(best_route) = quotes.first() else {
327+
state
328+
.metrics
329+
.add_ca_no_routes_found(construct_metrics_bridging_route(
330+
bridge_chain_id.clone(),
331+
bridge_contract.to_string(),
332+
request_payload.transaction.chain_id.clone(),
333+
asset_transfer_contract.to_string(),
334+
));
305335
return Ok(Json(PrepareResponse::Error(PrepareResponseError {
306336
error: BridgingError::NoRoutesAvailable,
307337
}))
@@ -325,6 +355,7 @@ async fn handler_internal(
325355
"The current bridging asset balance on {} is {} less than the required topup amount:{}. The bridging fee is:{}",
326356
from_address, current_bridging_asset_balance, required_topup_amount, bridging_fee
327357
);
358+
state.metrics.add_ca_insufficient_funds();
328359
return Ok(Json(PrepareResponse::Error(PrepareResponseError {
329360
error: BridgingError::InsufficientFunds,
330361
}))
@@ -346,6 +377,14 @@ async fn handler_internal(
346377
)
347378
.await?;
348379
let Some(best_route) = quotes.first() else {
380+
state
381+
.metrics
382+
.add_ca_no_routes_found(construct_metrics_bridging_route(
383+
bridge_chain_id.clone(),
384+
bridge_contract.to_string(),
385+
request_payload.transaction.chain_id.clone(),
386+
asset_transfer_contract.to_string(),
387+
));
349388
return Ok(Json(PrepareResponse::Error(PrepareResponseError {
350389
error: BridgingError::NoRoutesAvailable,
351390
}))
@@ -448,7 +487,7 @@ async fn handler_internal(
448487
.await?;
449488
for (index, simulation_result) in simulation_results.simulation_results.iter().enumerate() {
450489
// Making sure the simulation input matches the transaction input
451-
let curr_route = routes.get(index).ok_or_else(|| {
490+
let curr_route = routes.get_mut(index).ok_or_else(|| {
452491
RpcError::SimulationFailed("The route index is out of bounds".to_string())
453492
})?;
454493
if simulation_result.transaction.input != curr_route.input {
@@ -458,9 +497,25 @@ async fn handler_internal(
458497
));
459498
}
460499

461-
routes[index].gas_limit = U64::from(
500+
curr_route.gas_limit = U64::from(
462501
(simulation_result.transaction.gas * (100 + ESTIMATED_GAS_SLIPPAGE as u64)) / 100,
463502
);
503+
504+
// Get the transaction type for metrics based on the assumption that the first transaction is an approval
505+
// and the rest are bridging transactions
506+
let tx_type = if simulation_results.simulation_results.len() == 1 {
507+
// If there is only one transaction, it's a bridging transaction
508+
ChainAbstractionTransactionType::Bridge
509+
} else if index == 0 {
510+
ChainAbstractionTransactionType::Approve
511+
} else {
512+
ChainAbstractionTransactionType::Bridge
513+
};
514+
state.metrics.add_ca_gas_estimation(
515+
simulation_result.transaction.gas,
516+
curr_route.chain_id.clone(),
517+
tx_type,
518+
);
464519
}
465520

466521
// Save the bridging transaction to the IRN
@@ -578,3 +633,15 @@ async fn handler_internal(
578633
.into_response(),
579634
);
580635
}
636+
637+
fn construct_metrics_bridging_route(
638+
from_chain_id: String,
639+
from_contract: String,
640+
to_chain_id: String,
641+
to_contract: String,
642+
) -> String {
643+
format!(
644+
"{}:{}->{}:{}",
645+
from_chain_id, from_contract, to_chain_id, to_contract
646+
)
647+
}

src/metrics.rs

+81
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,20 @@ use {
2020
},
2121
};
2222

23+
#[derive(strum_macros::Display)]
24+
pub enum ChainAbstractionTransactionType {
25+
Transfer,
26+
Approve,
27+
Bridge,
28+
}
29+
30+
#[derive(strum_macros::Display)]
31+
pub enum ChainAbstractionNoBridgingNeededType {
32+
NativeTokenTransfer,
33+
AssetNotSupported,
34+
SufficientFunds,
35+
}
36+
2337
#[derive(Debug)]
2438
pub struct Metrics {
2539
pub rpc_call_counter: Counter<u64>,
@@ -70,6 +84,12 @@ pub struct Metrics {
7084

7185
// IRN client
7286
pub irn_latency_tracker: Histogram<f64>,
87+
88+
// Chain Abstracton
89+
pub ca_gas_estimation_tracker: Histogram<f64>,
90+
pub ca_no_routes_found_counter: Counter<u64>,
91+
pub ca_insufficient_funds_counter: Counter<u64>,
92+
pub ca_no_bridging_needed_counter: Counter<u64>,
7393
}
7494

7595
impl Metrics {
@@ -266,6 +286,26 @@ impl Metrics {
266286
.with_description("The number of chain RPC calls that had no available providers")
267287
.init();
268288

289+
let ca_gas_estimation_tracker = meter
290+
.f64_histogram("gas_estimation")
291+
.with_description("The gas estimation for transactions")
292+
.init();
293+
294+
let ca_no_routes_found_counter = meter
295+
.u64_counter("ca_no_routes_found")
296+
.with_description("The number of times no routes were found for a CA")
297+
.init();
298+
299+
let ca_insufficient_funds_counter = meter
300+
.u64_counter("ca_insufficient_funds")
301+
.with_description("The number of times insufficient funds were responded for a CA")
302+
.init();
303+
304+
let ca_no_bridging_needed_counter = meter
305+
.u64_counter("ca_no_bridging_needed")
306+
.with_description("The number of times no bridging was needed for a CA")
307+
.init();
308+
269309
Metrics {
270310
rpc_call_counter,
271311
rpc_call_retries,
@@ -305,6 +345,10 @@ impl Metrics {
305345
rate_limiting_latency_tracker,
306346
rate_limited_entries_counter,
307347
rate_limited_responses_counter,
348+
ca_gas_estimation_tracker,
349+
ca_no_routes_found_counter,
350+
ca_insufficient_funds_counter,
351+
ca_no_bridging_needed_counter,
308352
}
309353
}
310354
}
@@ -642,6 +686,43 @@ impl Metrics {
642686
);
643687
}
644688

689+
pub fn add_ca_gas_estimation(
690+
&self,
691+
gas: u64,
692+
chain_id: String,
693+
tx_type: ChainAbstractionTransactionType,
694+
) {
695+
self.ca_gas_estimation_tracker.record(
696+
&otel::Context::new(),
697+
gas as f64,
698+
&[
699+
otel::KeyValue::new("chain_id", chain_id),
700+
otel::KeyValue::new("tx_type", tx_type.to_string()),
701+
],
702+
);
703+
}
704+
705+
pub fn add_ca_no_routes_found(&self, route: String) {
706+
self.ca_no_routes_found_counter.add(
707+
&otel::Context::new(),
708+
1,
709+
&[otel::KeyValue::new("route", route)],
710+
);
711+
}
712+
713+
pub fn add_ca_insufficient_funds(&self) {
714+
self.ca_insufficient_funds_counter
715+
.add(&otel::Context::new(), 1, &[]);
716+
}
717+
718+
pub fn add_ca_no_bridging_needed(&self, ca_type: ChainAbstractionNoBridgingNeededType) {
719+
self.ca_no_bridging_needed_counter.add(
720+
&otel::Context::new(),
721+
1,
722+
&[otel::KeyValue::new("type", ca_type.to_string())],
723+
);
724+
}
725+
645726
/// Gathering system CPU(s) and Memory usage metrics
646727
pub async fn gather_system_metrics(&self) {
647728
let mut system = System::new_with_specifics(

terraform/monitoring/dashboard.jsonnet

+6
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,12 @@ dashboard.new(
192192
row.new('Account names (ENS gateway) Metrics'),
193193
panels.names.registered(ds, vars) { gridPos: pos_short._3 },
194194

195+
row.new('Chain Abstraction'),
196+
panels.chain_abstraction.gas_estimation(ds, vars) { gridPos: pos_short._4 },
197+
panels.chain_abstraction.insufficient_funds(ds, vars) { gridPos: pos_short._4 },
198+
panels.chain_abstraction.no_bridging(ds, vars) { gridPos: pos_short._4 },
199+
panels.chain_abstraction.no_routes(ds, vars) { gridPos: pos_short._4 },
200+
195201
row.new('Redis'),
196202
panels.redis.cpu(ds, vars) { gridPos: pos._2 },
197203
panels.redis.memory(ds, vars) { gridPos: pos._2 },
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
local grafana = import '../../grafonnet-lib/grafana.libsonnet';
2+
local defaults = import '../../grafonnet-lib/defaults.libsonnet';
3+
4+
local panels = grafana.panels;
5+
local targets = grafana.targets;
6+
7+
{
8+
new(ds, vars)::
9+
panels.timeseries(
10+
title = 'Gas estimations',
11+
datasource = ds.prometheus,
12+
)
13+
.configure(defaults.configuration.timeseries)
14+
.addTarget(targets.prometheus(
15+
datasource = ds.prometheus,
16+
expr = 'sum by(chain_id) (rate(gas_estimation_sum[$__rate_interval])) / sum by(chain_id) (rate(gas_estimation_count[$__rate_interval]))',
17+
legendFormat = 'Gas estimation',
18+
))
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
local grafana = import '../../grafonnet-lib/grafana.libsonnet';
2+
local defaults = import '../../grafonnet-lib/defaults.libsonnet';
3+
4+
local panels = grafana.panels;
5+
local targets = grafana.targets;
6+
7+
{
8+
new(ds, vars)::
9+
panels.timeseries(
10+
title = 'Insufficient funds responses',
11+
datasource = ds.prometheus,
12+
)
13+
.configure(defaults.configuration.timeseries)
14+
.addTarget(targets.prometheus(
15+
datasource = ds.prometheus,
16+
expr = 'sum(increase(ca_insufficient_funds_total{}[$__rate_interval]))',
17+
legendFormat = 'Insufficient funds responses counter',
18+
))
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
local grafana = import '../../grafonnet-lib/grafana.libsonnet';
2+
local defaults = import '../../grafonnet-lib/defaults.libsonnet';
3+
4+
local panels = grafana.panels;
5+
local targets = grafana.targets;
6+
7+
{
8+
new(ds, vars)::
9+
panels.timeseries(
10+
title = 'No bridging needed responses',
11+
datasource = ds.prometheus,
12+
)
13+
.configure(defaults.configuration.timeseries)
14+
.addTarget(targets.prometheus(
15+
datasource = ds.prometheus,
16+
expr = 'sum by(type) (increase(ca_no_bridging_needed_total{}[$__rate_interval]))',
17+
legendFormat = 'No bridging needed responses counter',
18+
))
19+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
local grafana = import '../../grafonnet-lib/grafana.libsonnet';
2+
local defaults = import '../../grafonnet-lib/defaults.libsonnet';
3+
4+
local panels = grafana.panels;
5+
local targets = grafana.targets;
6+
7+
{
8+
new(ds, vars)::
9+
panels.timeseries(
10+
title = 'No bridging routes found',
11+
datasource = ds.prometheus,
12+
)
13+
.configure(defaults.configuration.timeseries)
14+
.addTarget(targets.prometheus(
15+
datasource = ds.prometheus,
16+
expr = 'sum by(route) (increase(ca_no_routes_found_total{}[$__rate_interval]))',
17+
legendFormat = 'No routes responses counter',
18+
))
19+
}

terraform/monitoring/panels/panels.libsonnet

+7
Original file line numberDiff line numberDiff line change
@@ -90,4 +90,11 @@ local redis = panels.aws.redis;
9090
endpoints_latency: (import 'non_rpc/endpoints_latency.libsonnet').new,
9191
cache_latency: (import 'non_rpc/cache_latency.libsonnet').new,
9292
},
93+
94+
chain_abstraction: {
95+
gas_estimation: (import 'chain_abstraction/gas_estimation.libsonnet').new,
96+
insufficient_funds: (import 'chain_abstraction/insufficient_funds.libsonnet').new,
97+
no_bridging: (import 'chain_abstraction/no_bridging.libsonnet').new,
98+
no_routes: (import 'chain_abstraction/no_routes.libsonnet').new,
99+
},
93100
}

0 commit comments

Comments
 (0)