todors 0.10.7

todo app with CLI, REST & gRPC interfaces
use chrono::{SecondsFormat, Utc};
use std::time::Instant;

use super::middleware::*;
use crate::serializers::{to_json, Serialize};

#[derive(Default)]
pub struct LogMiddleware;

pub struct LogMiddlewareService<S> {
    service: S,
}

#[derive(Serialize, Default)]
struct Log {
    timestamp: String,
    method: String,
    path: String,
    query_params: String,
    http_version: String,
    client_ip: String,
    client_real_ip: String,
    request_headers: String,
    response_headers: String,
    status_code: String,
    latency: String,
}

impl Log {
    pub fn new(
        method: String,
        path: String,
        query_params: String,
        http_version: String,
        client_ip: String,
        client_real_ip: String,
    ) -> Self {
        Self {
            timestamp: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
            method,
            path,
            query_params,
            http_version,
            client_ip,
            client_real_ip,
            ..Default::default()
        }
    }
}

impl std::fmt::Display for Log {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        to_json(self).unwrap().fmt(f)
    }
}

impl<S, B> Transform<S, ServiceRequest> for LogMiddleware
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = ActixError;
    type InitError = ();
    type Transform = LogMiddlewareService<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(LogMiddlewareService { service }))
    }
}

impl<S, B> Service<ServiceRequest> for LogMiddlewareService<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = ActixError>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = ActixError;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    forward_ready!(service);

    #[inline]
    fn call(&self, req: ServiceRequest) -> Self::Future {
        let mut log = Log::new(
            req.method().to_string(),
            req.path().to_string(),
            req.query_string().to_string(),
            format!("{:?}", req.version()),
            req.connection_info()
                .peer_addr()
                .unwrap_or_default()
                .to_string(),
            req.connection_info()
                .realip_remote_addr()
                .unwrap_or_default()
                .to_string(),
        );

        let fut = self.service.call(req);

        Box::pin(async move {
            let start = Instant::now();
            let res = fut.await?;
            let elapsed = start.elapsed();

            log.request_headers = res
                .request()
                .headers()
                .iter()
                .fold(String::new(), |acc, (key, value)| {
                    format!("{}{}: {}\n", acc, key, value.to_str().unwrap())
                })
                .strip_suffix('\n')
                .unwrap_or_default()
                .to_string();
            log.response_headers = res
                .headers()
                .iter()
                .fold(String::new(), |acc, (key, value)| {
                    format!("{}{}: {}\n", acc, key, value.to_str().unwrap())
                })
                .strip_suffix('\n')
                .unwrap_or_default()
                .to_string();
            log.status_code = res.status().to_string();
            log.latency = format!("{:?}", elapsed);

            println!("{}", log);

            Ok(res)
        })
    }
}