use std::{
collections::HashMap,
future::{ready, Ready},
sync::Arc,
time::{Duration, Instant},
};
use actix_service::{Service, Transform};
use actix_web::{
dev::{forward_ready, ServiceRequest, ServiceResponse},
Error,
};
use parking_lot::RwLock;
#[derive(Debug, Clone, Default)]
pub struct MetricsData {
pub total_requests: u64,
pub status_counts: HashMap<u16, u64>,
pub total_duration: Duration,
}
#[derive(Clone, Default)]
pub struct MetricsStore {
inner: Arc<RwLock<MetricsData>>,
}
impl MetricsStore {
pub fn new() -> Self {
Self::default()
}
pub fn snapshot(&self) -> MetricsData {
self.inner.read().clone()
}
pub fn reset(&self) {
*self.inner.write() = MetricsData::default();
}
fn record(&self, status: u16, duration: Duration) {
let mut data = self.inner.write();
data.total_requests += 1;
*data.status_counts.entry(status).or_default() += 1;
data.total_duration += duration;
}
}
#[derive(Clone)]
pub struct Metrics {
store: MetricsStore,
}
impl Metrics {
pub fn new(store: MetricsStore) -> Self {
Self { store }
}
}
impl<S, B> Transform<S, ServiceRequest> for Metrics
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = MetricsMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(MetricsMiddleware {
service,
store: self.store.clone(),
}))
}
}
pub struct MetricsMiddleware<S> {
service: S,
store: MetricsStore,
}
impl<S, B> Service<ServiceRequest> for MetricsMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future =
std::pin::Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>>>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let start = Instant::now();
let store = self.store.clone();
let fut = self.service.call(req);
Box::pin(async move {
let result = fut.await;
let duration = start.elapsed();
let status = match &result {
Ok(res) => res.status().as_u16(),
Err(_) => 500,
};
store.record(status, duration);
result
})
}
}