use crate::infrastructure::telemetry::collector::MetricsCollector;
use actix_web::{
Error,
dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready},
};
use futures::future::{LocalBoxFuture, Ready, ok};
use std::sync::Arc;
use std::time::Instant;
pub struct MetricsMiddleware {
metrics: Arc<MetricsCollector>,
}
impl MetricsMiddleware {
pub fn new(metrics: Arc<MetricsCollector>) -> Self {
Self { metrics }
}
}
impl<S, B> Transform<S, ServiceRequest> for MetricsMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = MetricsMiddlewareService<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(MetricsMiddlewareService {
service,
metrics: self.metrics.clone(),
})
}
}
pub struct MetricsMiddlewareService<S> {
service: S,
metrics: Arc<MetricsCollector>,
}
impl<S, B> Service<ServiceRequest> for MetricsMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let metrics = self.metrics.clone();
let path = req.path().to_string();
let method = req.method().to_string();
let start_time = Instant::now();
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
let status = res.status().as_u16().to_string();
let duration = start_time.elapsed();
metrics.record_request(&path, &method, &status);
metrics.record_request_duration(&path, &method, duration);
if !res.status().is_success() {
metrics.record_error(&path, &status);
}
Ok(res)
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::{
App, Error, HttpResponse,
dev::{Service, ServiceResponse, Transform},
http::{StatusCode, header::ContentType},
test::{self, TestRequest},
web,
};
use futures::future::{Ready, ready};
use std::sync::{Arc, Mutex};
use std::task::{Context, Poll};
use std::time::Duration;
#[derive(Default, Clone)]
struct MetricsCall {
path: String,
method: String,
status: String,
is_error: bool,
duration_recorded: bool,
}
struct TestMetricsCollector {
calls: Arc<Mutex<Vec<MetricsCall>>>,
}
impl TestMetricsCollector {
fn new() -> Self {
Self {
calls: Arc::new(Mutex::new(Vec::new())),
}
}
fn get_calls(&self) -> Vec<MetricsCall> {
let calls = self.calls.lock().unwrap();
calls.clone()
}
fn record_request(&self, endpoint: &str, method: &str, status: &str) {
let mut calls = self.calls.lock().unwrap();
calls.push(MetricsCall {
path: endpoint.to_string(),
method: method.to_string(),
status: status.to_string(),
is_error: false,
duration_recorded: false,
});
}
fn record_request_duration(&self, endpoint: &str, method: &str, _duration: Duration) {
let mut calls = self.calls.lock().unwrap();
let existing_call = calls
.iter_mut()
.find(|call| call.path == endpoint && call.method == method);
if let Some(call) = existing_call {
call.duration_recorded = true;
} else {
calls.push(MetricsCall {
path: endpoint.to_string(),
method: method.to_string(),
status: String::new(),
is_error: false,
duration_recorded: true,
});
}
}
fn record_error(&self, endpoint: &str, error_type: &str) {
let mut calls = self.calls.lock().unwrap();
calls.push(MetricsCall {
path: endpoint.to_string(),
method: String::new(),
status: error_type.to_string(),
is_error: true,
duration_recorded: false,
});
}
}
struct TestService {
status_code: StatusCode,
}
impl TestService {
fn new(status: StatusCode) -> Self {
Self {
status_code: status,
}
}
}
impl Service<ServiceRequest> for TestService {
type Response = ServiceResponse<actix_web::body::BoxBody>;
type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
fn poll_ready(&self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let response = HttpResponse::build(self.status_code)
.content_type(ContentType::plaintext())
.body("Test response");
ready(Ok(req.into_response(response)))
}
}
trait MetricsInterface {
fn record_request(&self, endpoint: &str, method: &str, status: &str);
fn record_request_duration(&self, endpoint: &str, method: &str, duration: Duration);
fn record_error(&self, endpoint: &str, error_type: &str);
}
impl MetricsInterface for crate::infrastructure::telemetry::collector::MetricsCollector {
fn record_request(&self, endpoint: &str, method: &str, status: &str) {
self.record_request(endpoint, method, status);
}
fn record_request_duration(&self, endpoint: &str, method: &str, duration: Duration) {
self.record_request_duration(endpoint, method, duration);
}
fn record_error(&self, endpoint: &str, error_type: &str) {
self.record_error(endpoint, error_type);
}
}
impl MetricsInterface for TestMetricsCollector {
fn record_request(&self, endpoint: &str, method: &str, status: &str) {
self.record_request(endpoint, method, status);
}
fn record_request_duration(&self, endpoint: &str, method: &str, duration: Duration) {
self.record_request_duration(endpoint, method, duration);
}
fn record_error(&self, endpoint: &str, error_type: &str) {
self.record_error(endpoint, error_type);
}
}
struct TestMetricsMiddleware<T: MetricsInterface + 'static> {
metrics: Arc<T>,
}
impl<T: MetricsInterface + 'static> TestMetricsMiddleware<T> {
fn new(metrics: Arc<T>) -> Self {
Self { metrics }
}
}
impl<S, B, T> Transform<S, ServiceRequest> for TestMetricsMiddleware<T>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
T: MetricsInterface + 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = TestMetricsMiddlewareService<S, T>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ok(TestMetricsMiddlewareService {
service,
metrics: self.metrics.clone(),
})
}
}
struct TestMetricsMiddlewareService<S, T: MetricsInterface + 'static> {
service: S,
metrics: Arc<T>,
}
impl<S, B, T> Service<ServiceRequest> for TestMetricsMiddlewareService<S, T>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
T: MetricsInterface + 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, req: ServiceRequest) -> Self::Future {
let metrics = self.metrics.clone();
let path = req.path().to_string();
let method = req.method().to_string();
let start_time = Instant::now();
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
let status = res.status().as_u16().to_string();
let duration = start_time.elapsed();
metrics.record_request(&path, &method, &status);
metrics.record_request_duration(&path, &method, duration);
if !res.status().is_success() {
metrics.record_error(&path, &status);
}
Ok(res)
})
}
}
#[test]
fn test_metrics_middleware_new() {
let collector = Arc::new(TestMetricsCollector::new());
let middleware = TestMetricsMiddleware::new(collector);
assert!(Arc::strong_count(&middleware.metrics) > 0);
}
#[test]
fn test_transform_creates_service() {
let collector = Arc::new(TestMetricsCollector::new());
let middleware = TestMetricsMiddleware::new(collector);
let test_service = TestService::new(StatusCode::OK);
let transform_future = middleware.new_transform(test_service);
let service =
futures::executor::block_on(transform_future).expect("Transform should succeed");
assert!(Arc::strong_count(&service.metrics) > 0);
}
#[test]
fn test_service_records_successful_request() {
let collector = Arc::new(TestMetricsCollector::new());
let test_service = TestService::new(StatusCode::OK);
let middleware_service = TestMetricsMiddlewareService {
service: test_service,
metrics: collector.clone(),
};
let req = TestRequest::get().uri("/test").to_srv_request();
let response = futures::executor::block_on(middleware_service.call(req))
.expect("Service call should succeed");
assert_eq!(response.status(), StatusCode::OK);
let calls = collector.get_calls();
assert!(
!calls.is_empty(),
"Should have recorded at least one metrics call"
);
let request_call = calls
.iter()
.find(|call| call.path == "/test" && call.method == "GET" && call.status == "200");
assert!(
request_call.is_some(),
"Should have recorded request metrics"
);
let duration_call = calls
.iter()
.find(|call| call.path == "/test" && call.method == "GET" && call.duration_recorded);
assert!(
duration_call.is_some(),
"Should have recorded duration metrics"
);
let error_call = calls.iter().find(|call| call.is_error);
assert!(
error_call.is_none(),
"Should not have recorded error metrics"
);
}
#[test]
fn test_service_records_error_request() {
let collector = Arc::new(TestMetricsCollector::new());
let test_service = TestService::new(StatusCode::NOT_FOUND);
let middleware_service = TestMetricsMiddlewareService {
service: test_service,
metrics: collector.clone(),
};
let req = TestRequest::get().uri("/test").to_srv_request();
let response = futures::executor::block_on(middleware_service.call(req))
.expect("Service call should succeed");
assert_eq!(response.status(), StatusCode::NOT_FOUND);
let calls = collector.get_calls();
let request_call = calls
.iter()
.find(|call| call.path == "/test" && call.method == "GET" && call.status == "404");
assert!(
request_call.is_some(),
"Should have recorded request metrics"
);
let duration_call = calls
.iter()
.find(|call| call.path == "/test" && call.method == "GET" && call.duration_recorded);
assert!(
duration_call.is_some(),
"Should have recorded duration metrics"
);
let error_call = calls
.iter()
.find(|call| call.is_error && call.status == "404");
assert!(error_call.is_some(), "Should have recorded error metrics");
}
#[actix_web::test]
async fn test_middleware_integration() {
let collector = Arc::new(TestMetricsCollector::new());
async fn test_handler() -> HttpResponse {
HttpResponse::Ok().body("Test response")
}
let app = test::init_service(
App::new()
.wrap(TestMetricsMiddleware::new(collector.clone()))
.route("/test", web::get().to(test_handler)),
)
.await;
let req = TestRequest::get().uri("/test").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let calls = collector.get_calls();
assert!(!calls.is_empty(), "Should have recorded metrics");
}
#[actix_web::test]
async fn test_middleware_with_error_response() {
let collector = Arc::new(TestMetricsCollector::new());
async fn error_handler() -> HttpResponse {
HttpResponse::InternalServerError().body("Error occurred")
}
let app = test::init_service(
App::new()
.wrap(TestMetricsMiddleware::new(collector.clone()))
.route("/error", web::get().to(error_handler)),
)
.await;
let req = TestRequest::get().uri("/error").to_request();
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
let calls = collector.get_calls();
let error_calls = calls.iter().filter(|call| call.is_error).count();
assert!(error_calls > 0, "Should have recorded error metrics");
}
}