gitscope
← rgit / 0.1.2 / src/layers/logger.rs
https://dev.kronoz.win/r/rgit.git
src/layers/logger.rs · Rust
Blame Raw
  1//! Logs each and every request out in a format similar to that of Apache's logs.
  2
  3use std::{
  4    fmt::Debug,
  5    net::SocketAddr,
  6    task::{Context, Poll},
  7    time::Instant,
  8};
  9
 10use axum::{
 11    extract,
 12    http::{HeaderValue, Method, Request, Response},
 13};
 14use futures::future::{Future, FutureExt, Join, Map, Ready};
 15use tower_service::Service;
 16use tracing::{error, info, instrument::Instrumented, Instrument, Span};
 17use uuid::Uuid;
 18
 19use super::UnwrapInfallible;
 20
 21pub trait GenericError: std::error::Error + Debug + Send + Sync {}
 22
 23#[derive(Clone)]
 24pub struct LoggingMiddleware<S>(pub S);
 25
 26impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for LoggingMiddleware<S>
 27where
 28    S: Service<Request<ReqBody>, Response = Response<ResBody>, Error = std::convert::Infallible>
 29        + Clone
 30        + Send
 31        + 'static,
 32    S::Future: Send + 'static,
 33    S::Response: Default + Debug,
 34    ReqBody: Send + Debug + 'static,
 35    ResBody: Default + Send + 'static,
 36{
 37    type Response = S::Response;
 38    type Error = S::Error;
 39    type Future = Map<
 40        Join<Instrumented<S::Future>, Ready<PendingLogMessage>>,
 41        fn((<S::Future as Future>::Output, PendingLogMessage)) -> <S::Future as Future>::Output,
 42    >;
 43
 44    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
 45        self.0.poll_ready(cx)
 46    }
 47
 48    fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
 49        let request_id = Uuid::new_v4();
 50        let span = tracing::info_span!("web", "request_id" = request_id.to_string().as_str());
 51
 52        let log_message = PendingLogMessage {
 53            span: span.clone(),
 54            request_id,
 55            ip: req
 56                .extensions()
 57                .get::<extract::ConnectInfo<std::net::SocketAddr>>()
 58                .map_or_else(|| "0.0.0.0:0".parse().unwrap(), |v| v.0),
 59            method: req.method().clone(),
 60            uri: req.uri().path().to_string(),
 61            start: Instant::now(),
 62            user_agent: req.headers().get(axum::http::header::USER_AGENT).cloned(),
 63        };
 64
 65        futures::future::join(
 66            self.0.call(req).instrument(span),
 67            futures::future::ready(log_message),
 68        )
 69        .map(|(response, pending_log_message)| {
 70            let mut response = response.unwrap_infallible();
 71            pending_log_message.log(&response);
 72            response.headers_mut().insert(
 73                "X-Request-ID",
 74                HeaderValue::try_from(pending_log_message.request_id.to_string()).unwrap(),
 75            );
 76            Ok(response)
 77        })
 78    }
 79}
 80
 81pub struct PendingLogMessage {
 82    span: Span,
 83    request_id: Uuid,
 84    ip: SocketAddr,
 85    method: Method,
 86    uri: String,
 87    start: Instant,
 88    user_agent: Option<HeaderValue>,
 89}
 90
 91impl PendingLogMessage {
 92    pub fn log<ResBody>(&self, response: &Response<ResBody>) {
 93        let _enter = self.span.enter();
 94
 95        if response.status().is_server_error() {
 96            error!(
 97                "{ip} - \"{method} {uri}\" {status} {duration:?} \"{user_agent}\" \"{error:?}\"",
 98                ip = self.ip,
 99                method = self.method,
100                uri = self.uri,
101                status = response.status().as_u16(),
102                duration = self.start.elapsed(),
103                user_agent = self
104                    .user_agent
105                    .as_ref()
106                    .and_then(|v| v.to_str().ok())
107                    .unwrap_or("unknown"),
108                error = match response.extensions().get::<Box<dyn GenericError>>() {
109                    Some(e) => Err(e),
110                    None => Ok(()),
111                }
112            );
113        } else {
114            info!(
115                "{ip} - \"{method} {uri}\" {status} {duration:?} \"{user_agent}\" \"{error:?}\"",
116                ip = self.ip,
117                method = self.method,
118                uri = self.uri,
119                status = response.status().as_u16(),
120                duration = self.start.elapsed(),
121                user_agent = self
122                    .user_agent
123                    .as_ref()
124                    .and_then(|v| v.to_str().ok())
125                    .unwrap_or("unknown"),
126                error = match response.extensions().get::<Box<dyn GenericError>>() {
127                    Some(e) => Err(e),
128                    None => Ok(()),
129                }
130            );
131        }
132    }
133}