Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[EASY] Add metrics for logging native price requests #3006

Merged
merged 6 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from 4 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
60 changes: 35 additions & 25 deletions crates/shared/src/price_estimation/factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -189,25 +189,35 @@ impl<'a> PriceEstimatorFactory<'a> {
let estimator = self.get_estimator(driver)?.native.clone();
Ok((
driver.name.clone(),
Arc::new(NativePriceEstimator::new(
Arc::new(self.sanitized(estimator)),
self.network.native_token,
native_token_price_estimation_amount,
Arc::new(InstrumentedPriceEstimator::new(
NativePriceEstimator::new(
Arc::new(self.sanitized(estimator)),
self.network.native_token,
native_token_price_estimation_amount,
),
driver.name.to_string(),
)),
))
}
NativePriceEstimatorSource::OneInchSpotPriceApi => {
let name = "OneInchSpotPriceApi".to_string();
Copy link
Contributor

@squadgazzz squadgazzz Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UpperCamelCase should be avoided since driver.name uses flatcase. We should use a similar format for other names as well. If not flatcase everywhere, maybe snake_case.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I agree with you, but this is something systematic within the natice prices, we should change that in another PR alongside the configuration, because this happens also in the configuration of the services :/

Ok((
name.clone(),
Arc::new(InstrumentedPriceEstimator::new(
native::OneInch::new(
self.components.http_factory.create(),
self.args.one_inch_url.clone(),
self.args.one_inch_api_key.clone(),
self.network.chain_id,
self.network.block_stream.clone(),
self.components.tokens.clone(),
),
name,
)),
))
}
NativePriceEstimatorSource::OneInchSpotPriceApi => Ok((
"OneInchSpotPriceApi".into(),
Arc::new(native::OneInch::new(
self.components.http_factory.create(),
self.args.one_inch_url.clone(),
self.args.one_inch_api_key.clone(),
self.network.chain_id,
self.network.block_stream.clone(),
self.components.tokens.clone(),
)),
)),
NativePriceEstimatorSource::CoinGecko => {
let name = "CoinGecko".to_string();
let coin_gecko = native::CoinGecko::new(
self.components.http_factory.create(),
self.args.coin_gecko.coin_gecko_url.clone(),
Expand Down Expand Up @@ -240,12 +250,15 @@ impl<'a> PriceEstimatorFactory<'a> {
.unwrap(),
};

Arc::new(BufferedRequest::with_config(coin_gecko, configuration))
Arc::new(InstrumentedPriceEstimator::new(
BufferedRequest::with_config(coin_gecko, configuration),
name.clone() + "Buffered",
))
} else {
Arc::new(coin_gecko)
Arc::new(InstrumentedPriceEstimator::new(coin_gecko, name.clone()))
};

Ok(("CoinGecko".into(), coin_gecko))
Ok((name, coin_gecko))
}
}
}
Expand Down Expand Up @@ -395,12 +408,9 @@ impl PriceEstimatorCreating for ExternalPriceEstimator {
}
}

fn instrument(
estimator: impl PriceEstimating,
fn instrument<T: PriceEstimating>(
estimator: T,
name: impl Into<String>,
) -> Arc<InstrumentedPriceEstimator> {
Arc::new(InstrumentedPriceEstimator::new(
Box::new(estimator),
name.into(),
))
) -> Arc<InstrumentedPriceEstimator<T>> {
Arc::new(InstrumentedPriceEstimator::new(estimator, name.into()))
}
71 changes: 61 additions & 10 deletions crates/shared/src/price_estimation/instrumented.rs
Original file line number Diff line number Diff line change
@@ -1,21 +1,28 @@
use {
crate::price_estimation::{PriceEstimating, PriceEstimationError, Query},
crate::price_estimation::{
native::{NativePriceEstimateResult, NativePriceEstimating},
PriceEstimating,
PriceEstimationError,
Query,
},
ethcontract::jsonrpc::futures_util::future::BoxFuture,
futures::future::FutureExt,
primitive_types::H160,
prometheus::{HistogramVec, IntCounterVec},
std::{sync::Arc, time::Instant},
tracing::Instrument,
};

/// An instrumented price estimator.
pub struct InstrumentedPriceEstimator {
inner: Box<dyn PriceEstimating>,
pub struct InstrumentedPriceEstimator<T> {
inner: T,
name: String,
metrics: &'static Metrics,
}

impl InstrumentedPriceEstimator {
impl<T> InstrumentedPriceEstimator<T> {
/// Wraps an existing price estimator in an instrumented one.
pub fn new(inner: Box<dyn PriceEstimating>, name: String) -> Self {
pub fn new(inner: T, name: String) -> Self {
let metrics = Metrics::instance(observe::metrics::get_storage_registry()).unwrap();
for result in ["success", "failure"] {
metrics
Expand All @@ -29,9 +36,21 @@ impl InstrumentedPriceEstimator {
metrics,
}
}

/// Determines the result of a price estimate as either "success" or
/// "failure".
fn estimate_result<B>(&self, estimate: Result<&B, &PriceEstimationError>) -> &str {
// Count as a successful request if the answer is ok (no error) or if the error
// is No Liquidity
if estimate.is_ok() || matches!(estimate, Err(PriceEstimationError::NoLiquidity)) {
"success"
} else {
"failure"
}
}
}

impl PriceEstimating for InstrumentedPriceEstimator {
impl<T: PriceEstimating> PriceEstimating for InstrumentedPriceEstimator<T> {
fn estimate(
&self,
query: Arc<Query>,
Expand All @@ -43,9 +62,7 @@ impl PriceEstimating for InstrumentedPriceEstimator {
.price_estimation_times
.with_label_values(&[self.name.as_str()])
.observe(start.elapsed().as_secs_f64());

let success = !matches!(&estimate, Err(PriceEstimationError::EstimatorInternal(_)));
let result = if success { "success" } else { "failure" };
let result = self.estimate_result(estimate.as_ref());
self.metrics
.price_estimates
.with_label_values(&[self.name.as_str(), result])
Expand All @@ -58,6 +75,32 @@ impl PriceEstimating for InstrumentedPriceEstimator {
}
}

impl<T: NativePriceEstimating> NativePriceEstimating for InstrumentedPriceEstimator<T> {
fn estimate_native_price(&self, token: H160) -> BoxFuture<'_, NativePriceEstimateResult> {
async move {
let start = Instant::now();
let estimate = self.inner.estimate_native_price(token).await;
self.metrics
.native_price_estimation_times
.with_label_values(&[self.name.as_str()])
.observe(start.elapsed().as_secs_f64());
let result = self.estimate_result(estimate.as_ref());
self.metrics
.native_price_estimates
.with_label_values(&[self.name.as_str(), result])
.inc();
Copy link
Contributor

@squadgazzz squadgazzz Sep 24, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we really need 2 metrics? Can we use only the native_price_estimation_times with 2 labels and use the _count metric instead of declaring one more with increment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@squadgazzz in the end, because of this comment: #3006 (comment)
it was decided to directly reuse the current metrics 👌


estimate
}
.instrument(tracing::info_span!("native estimator", name = &self.name,))
.boxed()
}
}

/// Most native estimators are regular estimators with a wrapper. In
/// those cases, the native price requests will get counted as regular
/// price_estimates. Therefore, native_price_estimates only counts requests
/// for estimators which are exclusively native price estimators.
#[derive(prometheus_metric_storage::MetricStorage)]
struct Metrics {
/// price estimates
Expand All @@ -67,6 +110,14 @@ struct Metrics {
/// price estimation times
#[metric(labels("estimator_type"))]
price_estimation_times: HistogramVec,

/// number of native price requests sent to each estimator
m-lord-renkse marked this conversation as resolved.
Show resolved Hide resolved
#[metric(labels("estimator_type", "result"))]
native_price_estimates: IntCounterVec,

/// native price estimation times
#[metric(labels("estimator_type"))]
native_price_estimation_times: HistogramVec,
}

#[cfg(test)]
Expand Down Expand Up @@ -117,7 +168,7 @@ mod tests {
async { Err(PriceEstimationError::EstimatorInternal(anyhow!(""))) }.boxed()
});

let instrumented = InstrumentedPriceEstimator::new(Box::new(estimator), "foo".to_string());
let instrumented = InstrumentedPriceEstimator::new(estimator, "foo".to_string());

let _ = instrumented.estimate(queries[0].clone()).await;
let _ = instrumented.estimate(queries[1].clone()).await;
Expand Down
11 changes: 1 addition & 10 deletions crates/shared/src/price_estimation/trade_finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use {

/// A `TradeFinding`-based price estimator with request sharing and rate
/// limiting.
#[derive(Clone)]
pub struct TradeEstimator {
inner: Arc<Inner>,
sharing: RequestSharing<Arc<Query>, BoxFuture<'static, Result<Estimate, PriceEstimationError>>>,
Expand Down Expand Up @@ -104,16 +105,6 @@ impl Inner {
}
}

impl Clone for TradeEstimator {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess this was done because it required in the pass a specific Clone implementation. But this currently does what the #[derive(Clone)] should do.

fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
sharing: self.sharing.clone(),
rate_limiter: self.rate_limiter.clone(),
}
}
}

impl PriceEstimating for TradeEstimator {
fn estimate(&self, query: Arc<Query>) -> futures::future::BoxFuture<'_, PriceEstimateResult> {
self.estimate(query).boxed()
Expand Down
Loading