kegani 0.1.1

A developer-friendly, ergonomic, production-ready Rust web framework
Documentation
//! Request/Response logging middleware
//!
//! Provides structured logging for HTTP requests and responses.

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;

/// Logger middleware for request/response logging
pub struct Logger {
    level: Level,
    include_body: bool,
}

impl Logger {
    /// Create a new Logger middleware
    pub fn new() -> Self {
        Self {
            level: Level::INFO,
            include_body: false,
        }
    }

    /// Set the log level
    pub fn level(mut self, level: Level) -> Self {
        self.level = level;
        self
    }

    /// Include request/response body in logs
    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();

        // Extract request ID if present
        let request_id = req
            .extensions()
            .get::<String>()
            .cloned()
            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());

        // Log request
        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();

            // Log response
            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))
        })
    }
}