#![cfg(feature = "web")]
use actix_service::{forward_ready, Service, Transform};
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
Error, HttpMessage,
};
use futures_util::future::{ready, LocalBoxFuture, Ready};
use std::{rc::Rc, time::Instant};
use crate::auth::UserContext;
use crate::middleware::RequestId;
pub struct RequestLogMiddleware;
impl<S, B> Transform<S, ServiceRequest> for RequestLogMiddleware
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = RequestLogMiddlewareService<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(RequestLogMiddlewareService {
service: Rc::new(service),
}))
}
}
pub struct RequestLogMiddlewareService<S> {
service: Rc<S>,
}
impl<S, B> Service<ServiceRequest> for RequestLogMiddlewareService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + '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 srv = self.service.clone();
let start = Instant::now();
let method = req.method().to_string();
let path = req.path().to_string();
let peer = req
.connection_info()
.realip_remote_addr()
.map(|s| s.to_string());
let rid = req
.extensions()
.get::<RequestId>()
.map(|r| r.0.clone())
.unwrap_or_else(|| "missing".to_string());
let (user_id, tenant_id) = match req.extensions().get::<UserContext>() {
Some(ctx) => (Some(ctx.subject.clone()), ctx.tenant_id.clone()),
None => (None, None),
};
Box::pin(async move {
#[cfg(feature = "observability")]
let span = tracing::info_span!(
"http_request",
request_id = %rid,
http.method = %method,
http.path = %path,
user.id = %user_id.clone().unwrap_or_else(|| "-".into()),
tenant.id = %tenant_id.clone().unwrap_or_else(|| "-".into()),
client.ip = %peer.clone().unwrap_or_else(|| "-".into()),
);
#[cfg(feature = "observability")]
let _enter = span.enter();
let result = srv.call(req).await;
let elapsed_ms = start.elapsed().as_millis() as u64;
match result {
Ok(res) => {
let status = res.status().as_u16();
#[cfg(feature = "observability")]
tracing::info!(
http.status_code = status,
duration_ms = elapsed_ms,
"request completed"
);
Ok(res)
}
Err(err) => {
#[cfg(feature = "observability")]
tracing::error!(
duration_ms = elapsed_ms,
error = %err,
"request failed"
);
Err(err)
}
}
})
}
}