172 lines
5.6 KiB
Rust
172 lines
5.6 KiB
Rust
// select all requests for a timeline. sum bandwidth and request count. give `cost / byte` and `cost / request`.
|
|
use anyhow::Context;
|
|
use argh::FromArgs;
|
|
use entities::{rpc_accounting, rpc_key, user};
|
|
use ethers::types::Address;
|
|
use migration::{
|
|
sea_orm::{
|
|
self,
|
|
prelude::{DateTimeUtc, Decimal},
|
|
ColumnTrait, DatabaseConnection, EntityTrait, FromQueryResult, QueryFilter, QuerySelect,
|
|
},
|
|
Condition,
|
|
};
|
|
use serde::Serialize;
|
|
use serde_json::json;
|
|
use tracing::info;
|
|
|
|
/// count requests
|
|
#[derive(FromArgs, PartialEq, Debug, Eq)]
|
|
#[argh(subcommand, name = "rpc_accounting")]
|
|
pub struct RpcAccountingSubCommand {
|
|
/// the address of the user to check. If none, check all.
|
|
/// TODO: allow checking a single key
|
|
#[argh(option)]
|
|
address: Option<String>,
|
|
|
|
/// the chain id to check. If none, check all.
|
|
#[argh(option)]
|
|
chain_id: Option<u64>,
|
|
|
|
/// unix epoch timestamp of earliest entry
|
|
#[argh(option)]
|
|
start_timestamp: Option<u64>,
|
|
|
|
/// unix epoch timestamp of last entry
|
|
#[argh(option)]
|
|
end_timestamp: Option<u64>,
|
|
}
|
|
|
|
impl RpcAccountingSubCommand {
|
|
pub async fn main(self, db_conn: &DatabaseConnection) -> anyhow::Result<()> {
|
|
#[derive(Serialize, FromQueryResult)]
|
|
struct SelectResult {
|
|
total_frontend_requests: Decimal,
|
|
total_backend_retries: Decimal,
|
|
// total_cache_misses: Decimal,
|
|
total_cache_hits: Decimal,
|
|
total_response_bytes: Decimal,
|
|
total_error_responses: Decimal,
|
|
total_response_millis: Decimal,
|
|
first_period_datetime: DateTimeUtc,
|
|
last_period_datetime: DateTimeUtc,
|
|
}
|
|
|
|
let mut q = rpc_accounting::Entity::find()
|
|
.select_only()
|
|
.column_as(
|
|
rpc_accounting::Column::FrontendRequests.sum(),
|
|
"total_frontend_requests",
|
|
)
|
|
.column_as(
|
|
rpc_accounting::Column::BackendRequests.sum(),
|
|
"total_backend_retries",
|
|
)
|
|
// .column_as(
|
|
// rpc_accounting::Column::CacheMisses.sum(),
|
|
// "total_cache_misses",
|
|
// )
|
|
.column_as(rpc_accounting::Column::CacheHits.sum(), "total_cache_hits")
|
|
.column_as(
|
|
rpc_accounting::Column::SumResponseBytes.sum(),
|
|
"total_response_bytes",
|
|
)
|
|
.column_as(
|
|
// TODO: can we sum bools like this?
|
|
rpc_accounting::Column::ErrorResponse.sum(),
|
|
"total_error_responses",
|
|
)
|
|
.column_as(
|
|
rpc_accounting::Column::SumResponseMillis.sum(),
|
|
"total_response_millis",
|
|
)
|
|
.column_as(
|
|
rpc_accounting::Column::PeriodDatetime.min(),
|
|
"first_period_datetime",
|
|
)
|
|
.column_as(
|
|
rpc_accounting::Column::PeriodDatetime.max(),
|
|
"last_period_datetime",
|
|
);
|
|
|
|
let mut condition = Condition::all();
|
|
|
|
// TODO: do this with add_option? try operator is harder to use then
|
|
if let Some(address) = self.address {
|
|
let address = address.parse::<Address>()?;
|
|
|
|
// TODO: find_with_related
|
|
let u = user::Entity::find()
|
|
.filter(user::Column::Address.eq(address.as_bytes()))
|
|
.one(db_conn)
|
|
.await?
|
|
.context("no user found")?;
|
|
|
|
// TODO: select_only
|
|
let u_keys = rpc_key::Entity::find()
|
|
.filter(rpc_key::Column::UserId.eq(u.id))
|
|
.all(db_conn)
|
|
.await?;
|
|
|
|
anyhow::ensure!(!u_keys.is_empty(), "no user keys");
|
|
|
|
let u_key_ids: Vec<_> = u_keys.into_iter().map(|x| x.id).collect();
|
|
|
|
condition = condition.add(rpc_accounting::Column::RpcKeyId.is_in(u_key_ids));
|
|
}
|
|
|
|
if let Some(start_timestamp) = self.start_timestamp {
|
|
condition = condition.add(rpc_accounting::Column::PeriodDatetime.gte(start_timestamp))
|
|
}
|
|
|
|
if let Some(end_timestamp) = self.end_timestamp {
|
|
condition = condition.add(rpc_accounting::Column::PeriodDatetime.lte(end_timestamp))
|
|
}
|
|
|
|
if let Some(chain_id) = self.chain_id {
|
|
condition = condition.add(rpc_accounting::Column::ChainId.eq(chain_id))
|
|
}
|
|
|
|
q = q.filter(condition);
|
|
|
|
let stats = q
|
|
.into_model::<SelectResult>()
|
|
.one(db_conn)
|
|
.await?
|
|
.context("no query result")?;
|
|
|
|
if let Some(chain_id) = self.chain_id {
|
|
info!("stats for chain {}", chain_id);
|
|
} else {
|
|
info!("stats for all chains");
|
|
}
|
|
|
|
info!("stats: {:#}", json!(&stats));
|
|
|
|
let query_seconds: Decimal = stats
|
|
.last_period_datetime
|
|
.signed_duration_since(stats.first_period_datetime)
|
|
.num_seconds()
|
|
.into();
|
|
info!(%query_seconds);
|
|
|
|
let avg_request_per_second = (stats.total_frontend_requests / query_seconds).round_dp(2);
|
|
info!(%avg_request_per_second);
|
|
|
|
let cache_hit_rate = (stats.total_cache_hits / stats.total_frontend_requests
|
|
* Decimal::from(100))
|
|
.round_dp(2);
|
|
info!(%cache_hit_rate);
|
|
|
|
let avg_response_millis =
|
|
(stats.total_response_millis / stats.total_frontend_requests).round_dp(3);
|
|
info!(%avg_response_millis);
|
|
|
|
let avg_response_bytes =
|
|
(stats.total_response_bytes / stats.total_frontend_requests).round();
|
|
info!(%avg_response_bytes);
|
|
|
|
Ok(())
|
|
}
|
|
}
|