salvo_extra/
logging.rs

1//! A simple logging middleware.
2//!
3//! # Example
4//!
5//! ```no_run
6//! use salvo_core::prelude::*;
7//! use salvo_extra::logging::Logger;
8//!
9//!
10//! #[handler]
11//! async fn hello() -> &'static str {
12//!     "Hello World"
13//! }
14//!
15//! #[tokio::main]
16//! async fn main() {
17//!     let router = Router::new().get(hello);
18//!     let service = Service::new(router).hoop(Logger::new());
19//!
20//!     let acceptor = TcpListener::new("0.0.0.0:8698").bind().await;
21//!     Server::new(acceptor).serve(service).await;
22//! }
23//! ```
24use std::time::Instant;
25
26use tracing::{Instrument, Level};
27
28use salvo_core::http::{Request, ResBody, Response, StatusCode};
29use salvo_core::{Depot, FlowCtrl, Handler, async_trait};
30
31/// A simple logger middleware.
32#[derive(Default, Debug)]
33pub struct Logger {
34    /// Whether to log status error.
35    ///
36    /// If true, the logger will try log [`StatusError`][salvo_core::http::StatusError] information if response body is [`ResBody::Error`].
37    ///
38    /// **Note**: If you have handled the error before logging and the body is not [`ResBody::Error`],
39    /// the error information cannot be recorded.
40    pub log_status_error: bool,
41}
42impl Logger {
43    /// Create new `Logger` middleware.
44    #[inline]
45    #[must_use]
46    pub fn new() -> Self {
47        Self {
48            log_status_error: true,
49        }
50    }
51
52    /// Set whether to log [`StatusError`][salvo_core::http::StatusError] information if response body is [`ResBody::Error`].
53    ///
54    /// **Note**: If you have handled the error before logging and the body is not [`ResBody::Error`],
55    /// the error information cannot be recorded.
56     #[must_use]
57    pub fn log_status_error(mut self, log_status_error: bool) -> Self {
58        self.log_status_error = log_status_error;
59        self
60    }
61}
62
63#[async_trait]
64impl Handler for Logger {
65    async fn handle(
66        &self,
67        req: &mut Request,
68        depot: &mut Depot,
69        res: &mut Response,
70        ctrl: &mut FlowCtrl,
71    ) {
72        let span = tracing::span!(
73            Level::INFO,
74            "Request",
75            remote_addr = %req.remote_addr().to_string(),
76            version = ?req.version(),
77            method = %req.method(),
78            path = %req.uri(),
79        );
80
81        async move {
82            let now = Instant::now();
83            ctrl.call_next(req, depot, res).await;
84            let duration = now.elapsed();
85
86            let status = res.status_code.unwrap_or(match &res.body {
87                ResBody::None => StatusCode::NOT_FOUND,
88                ResBody::Error(e) => e.code,
89                _ => StatusCode::OK,
90            });
91            if let ResBody::Error(error) = &res.body {
92                tracing::info!(
93                    %status,
94                    ?duration,
95                    ?error,
96                    "Response"
97                );
98            } else {
99                tracing::info!(
100                    %status,
101                    ?duration,
102                    "Response"
103                );
104            }
105        }
106        .instrument(span)
107        .await
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use salvo_core::prelude::*;
114    use salvo_core::test::{ResponseExt, TestClient};
115    use tracing_test::traced_test;
116
117    use super::*;
118
119    #[tokio::test]
120    #[traced_test]
121    async fn test_log() {
122        #[handler]
123        async fn hello() -> &'static str {
124            "hello"
125        }
126
127        let router = Router::new()
128            .hoop(Logger::new())
129            .push(Router::with_path("hello").get(hello));
130
131        TestClient::get("http://127.0.0.1:5801/hello")
132            .send(router)
133            .await
134            .take_string()
135            .await
136            .unwrap();
137        assert!(logs_contain("duration"));
138    }
139}