use actix_web::{
dev::{Service, ServiceRequest, ServiceResponse, Transform},
Error, HttpMessage,
};
use std::future::{ready, Ready};
use std::pin::Pin;
use std::task::{Context, Poll};
use tracing::{info, warn, Level};
use std::time::Instant;
pub struct Logger {
level: Level,
include_body: bool,
}
impl Logger {
pub fn new() -> Self {
Self {
level: Level::INFO,
include_body: false,
}
}
pub fn level(mut self, level: Level) -> Self {
self.level = level;
self
}
pub fn include_body(mut self) -> Self {
self.include_body = true;
self
}
}
impl Default for Logger {
fn default() -> Self {
Self::new()
}
}
impl<S, B> Transform<S, ServiceRequest> for Logger
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = LoggerMiddleware<S>;
type InitError = ();
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(LoggerMiddleware {
service,
level: self.level,
include_body: self.include_body,
}))
}
}
pub struct LoggerMiddleware<S> {
service: S,
#[allow(dead_code)]
level: Level,
#[allow(dead_code)]
include_body: bool,
}
impl<S, B> Service<ServiceRequest> for LoggerMiddleware<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 = Pin<Box<dyn std::future::Future<Output = Result<Self::Response, Self::Error>>>>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let start = Instant::now();
let method = req.method().clone();
let path = req.path().to_string();
let version = req.version();
let request_id = req
.extensions()
.get::<String>()
.cloned()
.unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
info!(
request_id = %request_id,
method = %method,
path = %path,
version = ?version,
"Incoming request"
);
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
let duration = start.elapsed();
let status = res.status();
let status_code = status.as_u16();
if status.is_server_error() {
warn!(
request_id = %request_id,
status = status_code,
duration_ms = duration.as_millis(),
"Request completed with server error"
);
} else {
info!(
request_id = %request_id,
status = status_code,
duration_ms = duration.as_millis(),
"Request completed"
);
}
Ok(res.map_body(|_, body| body))
})
}
}