Skip to content

Commit c264c31

Browse files
authored
feat(fortuna): Traced client for better observability (#1651)
* feat(fortuna): Traced client for better observability
1 parent a0c5d11 commit c264c31

File tree

8 files changed

+314
-102
lines changed

8 files changed

+314
-102
lines changed

apps/fortuna/Cargo.lock

+1-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/fortuna/Cargo.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "fortuna"
3-
version = "6.2.2"
3+
version = "6.2.3"
44
edition = "2021"
55

66
[dependencies]

apps/fortuna/src/chain.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
pub(crate) mod eth_gas_oracle;
22
pub(crate) mod ethereum;
33
pub(crate) mod reader;
4+
pub(crate) mod traced_client;

apps/fortuna/src/chain/ethereum.rs

+76-33
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use {
22
crate::{
3+
api::ChainId,
34
chain::{
45
eth_gas_oracle::EthProviderOracle,
56
reader::{
@@ -9,6 +10,10 @@ use {
910
EntropyReader,
1011
RequestedWithCallbackEvent,
1112
},
13+
traced_client::{
14+
RpcMetrics,
15+
TracedClient,
16+
},
1217
},
1318
config::EthereumConfig,
1419
},
@@ -22,7 +27,6 @@ use {
2227
abi::RawLog,
2328
contract::{
2429
abigen,
25-
ContractError,
2630
EthLogDecode,
2731
},
2832
core::types::Address,
@@ -34,6 +38,7 @@ use {
3438
},
3539
prelude::{
3640
BlockId,
41+
JsonRpcClient,
3742
PendingTransaction,
3843
TransactionRequest,
3944
},
@@ -67,15 +72,19 @@ abigen!(
6772
"../../target_chains/ethereum/entropy_sdk/solidity/abis/IEntropy.json"
6873
);
6974

70-
pub type SignablePythContract = PythRandom<
75+
pub type SignablePythContractInner<T> = PythRandom<
7176
LegacyTxMiddleware<
7277
GasOracleMiddleware<
73-
NonceManagerMiddleware<SignerMiddleware<Provider<Http>, LocalWallet>>,
74-
EthProviderOracle<Provider<Http>>,
78+
NonceManagerMiddleware<SignerMiddleware<Provider<T>, LocalWallet>>,
79+
EthProviderOracle<Provider<T>>,
7580
>,
7681
>,
7782
>;
83+
pub type SignablePythContract = SignablePythContractInner<Http>;
84+
pub type InstrumentedSignablePythContract = SignablePythContractInner<TracedClient>;
85+
7886
pub type PythContract = PythRandom<Provider<Http>>;
87+
pub type InstrumentedPythContract = PythRandom<Provider<TracedClient>>;
7988

8089
/// Middleware that converts a transaction into a legacy transaction if use_legacy_tx is true.
8190
/// We can not use TransformerMiddleware because keeper calls fill_transaction first which bypasses
@@ -157,32 +166,7 @@ impl<M: Middleware> Middleware for LegacyTxMiddleware<M> {
157166
}
158167
}
159168

160-
impl SignablePythContract {
161-
pub async fn from_config(
162-
chain_config: &EthereumConfig,
163-
private_key: &str,
164-
) -> Result<SignablePythContract> {
165-
let provider = Provider::<Http>::try_from(&chain_config.geth_rpc_addr)?;
166-
let chain_id = provider.get_chainid().await?;
167-
let gas_oracle = EthProviderOracle::new(provider.clone());
168-
let wallet__ = private_key
169-
.parse::<LocalWallet>()?
170-
.with_chain_id(chain_id.as_u64());
171-
172-
let address = wallet__.address();
173-
174-
Ok(PythRandom::new(
175-
chain_config.contract_addr,
176-
Arc::new(LegacyTxMiddleware::new(
177-
chain_config.legacy_tx,
178-
GasOracleMiddleware::new(
179-
NonceManagerMiddleware::new(SignerMiddleware::new(provider, wallet__), address),
180-
gas_oracle,
181-
),
182-
)),
183-
))
184-
}
185-
169+
impl<T: JsonRpcClient + 'static + Clone> SignablePythContractInner<T> {
186170
/// Submit a request for a random number to the contract.
187171
///
188172
/// This method is a version of the autogenned `request` method that parses the emitted logs
@@ -249,10 +233,54 @@ impl SignablePythContract {
249233
Err(anyhow!("Request failed").into())
250234
}
251235
}
236+
237+
pub async fn from_config_and_provider(
238+
chain_config: &EthereumConfig,
239+
private_key: &str,
240+
provider: Provider<T>,
241+
) -> Result<SignablePythContractInner<T>> {
242+
let chain_id = provider.get_chainid().await?;
243+
let gas_oracle = EthProviderOracle::new(provider.clone());
244+
let wallet__ = private_key
245+
.parse::<LocalWallet>()?
246+
.with_chain_id(chain_id.as_u64());
247+
248+
let address = wallet__.address();
249+
250+
Ok(PythRandom::new(
251+
chain_config.contract_addr,
252+
Arc::new(LegacyTxMiddleware::new(
253+
chain_config.legacy_tx,
254+
GasOracleMiddleware::new(
255+
NonceManagerMiddleware::new(SignerMiddleware::new(provider, wallet__), address),
256+
gas_oracle,
257+
),
258+
)),
259+
))
260+
}
261+
}
262+
263+
impl SignablePythContract {
264+
pub async fn from_config(chain_config: &EthereumConfig, private_key: &str) -> Result<Self> {
265+
let provider = Provider::<Http>::try_from(&chain_config.geth_rpc_addr)?;
266+
Self::from_config_and_provider(chain_config, private_key, provider).await
267+
}
268+
}
269+
270+
impl InstrumentedSignablePythContract {
271+
pub async fn from_config(
272+
chain_config: &EthereumConfig,
273+
private_key: &str,
274+
chain_id: ChainId,
275+
metrics: Arc<RpcMetrics>,
276+
) -> Result<Self> {
277+
let provider = TracedClient::new(chain_id, &chain_config.geth_rpc_addr, metrics)?;
278+
Self::from_config_and_provider(chain_config, private_key, provider).await
279+
}
252280
}
253281

254282
impl PythContract {
255-
pub fn from_config(chain_config: &EthereumConfig) -> Result<PythContract> {
283+
pub fn from_config(chain_config: &EthereumConfig) -> Result<Self> {
256284
let provider = Provider::<Http>::try_from(&chain_config.geth_rpc_addr)?;
257285

258286
Ok(PythRandom::new(
@@ -262,8 +290,23 @@ impl PythContract {
262290
}
263291
}
264292

293+
impl InstrumentedPythContract {
294+
pub fn from_config(
295+
chain_config: &EthereumConfig,
296+
chain_id: ChainId,
297+
metrics: Arc<RpcMetrics>,
298+
) -> Result<Self> {
299+
let provider = TracedClient::new(chain_id, &chain_config.geth_rpc_addr, metrics)?;
300+
301+
Ok(PythRandom::new(
302+
chain_config.contract_addr,
303+
Arc::new(provider),
304+
))
305+
}
306+
}
307+
265308
#[async_trait]
266-
impl EntropyReader for PythContract {
309+
impl<T: JsonRpcClient + 'static> EntropyReader for PythRandom<Provider<T>> {
267310
async fn get_request(
268311
&self,
269312
provider_address: Address,
@@ -330,7 +373,7 @@ impl EntropyReader for PythContract {
330373
user_random_number: [u8; 32],
331374
provider_revelation: [u8; 32],
332375
) -> Result<U256> {
333-
let result: Result<U256, ContractError<Provider<Http>>> = self
376+
let result = self
334377
.reveal_with_callback(
335378
provider,
336379
sequence_number,
+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use {
2+
crate::api::ChainId,
3+
anyhow::Result,
4+
axum::async_trait,
5+
ethers::{
6+
prelude::Http,
7+
providers::{
8+
HttpClientError,
9+
JsonRpcClient,
10+
Provider,
11+
},
12+
},
13+
prometheus_client::{
14+
encoding::EncodeLabelSet,
15+
metrics::{
16+
counter::Counter,
17+
family::Family,
18+
histogram::Histogram,
19+
},
20+
registry::Registry,
21+
},
22+
std::{
23+
str::FromStr,
24+
sync::Arc,
25+
},
26+
tokio::{
27+
sync::RwLock,
28+
time::Instant,
29+
},
30+
};
31+
32+
#[derive(Debug, Clone, PartialEq, Eq, Hash, EncodeLabelSet)]
33+
pub struct RpcLabel {
34+
chain_id: ChainId,
35+
method: String,
36+
}
37+
38+
#[derive(Debug)]
39+
pub struct RpcMetrics {
40+
count: Family<RpcLabel, Counter>,
41+
latency: Family<RpcLabel, Histogram>,
42+
errors_count: Family<RpcLabel, Counter>,
43+
}
44+
45+
impl RpcMetrics {
46+
pub async fn new(metrics_registry: Arc<RwLock<Registry>>) -> Self {
47+
let count = Family::default();
48+
let mut guard = metrics_registry.write().await;
49+
let sub_registry = guard.sub_registry_with_prefix("rpc_requests");
50+
sub_registry.register(
51+
"count",
52+
"The number of RPC requests made to the chain with the specified method.",
53+
count.clone(),
54+
);
55+
56+
let latency = Family::<RpcLabel, Histogram>::new_with_constructor(|| {
57+
Histogram::new(
58+
[
59+
0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0, 10.0, 20.0,
60+
]
61+
.into_iter(),
62+
)
63+
});
64+
sub_registry.register(
65+
"latency",
66+
"The latency of RPC requests to the chain with the specified method.",
67+
latency.clone(),
68+
);
69+
70+
let errors_count = Family::default();
71+
sub_registry.register(
72+
"errors_count",
73+
"The number of RPC requests made to the chain that failed.",
74+
errors_count.clone(),
75+
);
76+
77+
Self {
78+
count,
79+
latency,
80+
errors_count,
81+
}
82+
}
83+
}
84+
85+
#[derive(Debug, Clone)]
86+
pub struct TracedClient {
87+
inner: Http,
88+
89+
chain_id: ChainId,
90+
metrics: Arc<RpcMetrics>,
91+
}
92+
93+
#[async_trait]
94+
impl JsonRpcClient for TracedClient {
95+
type Error = HttpClientError;
96+
97+
async fn request<
98+
T: serde::Serialize + Send + Sync + std::fmt::Debug,
99+
R: serde::de::DeserializeOwned + Send,
100+
>(
101+
&self,
102+
method: &str,
103+
params: T,
104+
) -> Result<R, HttpClientError> {
105+
let start = Instant::now();
106+
let label = &RpcLabel {
107+
chain_id: self.chain_id.clone(),
108+
method: method.to_string(),
109+
};
110+
self.metrics.count.get_or_create(label).inc();
111+
let res = match self.inner.request(method, params).await {
112+
Ok(result) => Ok(result),
113+
Err(e) => {
114+
self.metrics.errors_count.get_or_create(label).inc();
115+
Err(e)
116+
}
117+
};
118+
119+
let latency = start.elapsed().as_secs_f64();
120+
self.metrics.latency.get_or_create(label).observe(latency);
121+
res
122+
}
123+
}
124+
125+
impl TracedClient {
126+
pub fn new(
127+
chain_id: ChainId,
128+
url: &str,
129+
metrics: Arc<RpcMetrics>,
130+
) -> Result<Provider<TracedClient>> {
131+
Ok(Provider::new(TracedClient {
132+
inner: Http::from_str(url)?,
133+
chain_id,
134+
metrics,
135+
}))
136+
}
137+
}

0 commit comments

Comments
 (0)