road-runner-common 0.5.0

Shared Rust utilities for exchange ecosystem backend services.
Documentation
#![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;

/// Request log middleware (structured).
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();

        // request metadata
        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 = req
            .extensions()
            .get::<UserContext>()
            .map(|ctx| ctx.subject.clone());

        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()),
                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)
                }
            }
        })
    }
}