Skip to main content

actix_web/middleware/
logger.rs

1//! For middleware documentation, see [`Logger`].
2
3use std::{
4    borrow::Cow,
5    collections::HashSet,
6    env,
7    fmt::{self, Display as _},
8    future::Future,
9    marker::PhantomData,
10    pin::Pin,
11    rc::Rc,
12    task::{Context, Poll},
13};
14
15use actix_service::{Service, Transform};
16#[expect(deprecated, reason = "preserve the public ready future type")]
17use actix_utils::future::{ready, Ready};
18use bytes::Bytes;
19use futures_core::ready;
20use log::{debug, warn, Level};
21use pin_project_lite::pin_project;
22#[cfg(feature = "unicode")]
23use regex::Regex;
24#[cfg(not(feature = "unicode"))]
25use regex_lite::Regex;
26use time::{format_description::well_known::Rfc3339, OffsetDateTime};
27
28use crate::{
29    body::{BodySize, MessageBody},
30    http::header::HeaderName,
31    service::{ServiceRequest, ServiceResponse},
32    Error, Result,
33};
34
35/// Middleware for logging request and response summaries to the terminal.
36///
37/// This middleware uses the `log` crate to output information. Enable `log`'s output for the
38/// "actix_web" scope using [`env_logger`](https://docs.rs/env_logger) or similar crate.
39///
40/// # Default Format
41/// The [`default`](Logger::default) Logger uses the following format:
42///
43/// ```plain
44/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
45///
46/// Example Output:
47/// 127.0.0.1:54278 "GET /test HTTP/1.1" 404 20 "-" "HTTPie/2.2.0" 0.001074
48/// ```
49///
50/// # Examples
51/// ```
52/// use actix_web::{middleware::Logger, App};
53///
54/// // access logs are printed with the INFO level so ensure it is enabled by default
55/// env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
56///
57/// let app = App::new()
58///     // .wrap(Logger::default())
59///     .wrap(Logger::new("%a %{User-Agent}i"));
60/// ```
61///
62/// # Format
63/// Variable | Description
64/// -------- | -----------
65/// `%%` | The percent sign
66/// `%a` | Peer IP address (or IP address of reverse proxy if used)
67/// `%t` | Time when the request started processing (in RFC 3339 format)
68/// `%r` | First line of request (Example: `GET /test HTTP/1.1`)
69/// `%s` | Response status code
70/// `%b` | Size of response in bytes, including HTTP headers
71/// `%T` | Time taken to serve the request, in seconds to 6 decimal places
72/// `%D` | Time taken to serve the request, in milliseconds
73/// `%U` | Request URL
74/// `%{r}a` | "Real IP" remote address **\***
75/// `%{FOO}i` | `request.headers["FOO"]`
76/// `%{FOO}o` | `response.headers["FOO"]`
77/// `%{FOO}e` | `env_var["FOO"]`
78/// `%{FOO}xi` | [Custom request replacement](Logger::custom_request_replace) labelled "FOO"
79/// `%{FOO}xo` | [Custom response replacement](Logger::custom_response_replace) labelled "FOO"
80///
81/// # Security
82/// **\*** "Real IP" remote address is calculated using
83/// [`ConnectionInfo::realip_remote_addr()`](crate::dev::ConnectionInfo::realip_remote_addr())
84///
85/// If you use this value, ensure that all requests come from trusted hosts. Otherwise, it is
86/// trivial for the remote client to falsify their source IP address.
87#[derive(Debug)]
88pub struct Logger(Rc<Inner>);
89
90#[derive(Debug, Clone)]
91struct Inner {
92    format: Format,
93    exclude: HashSet<String>,
94    exclude_regex: Vec<Regex>,
95    log_target: Cow<'static, str>,
96    log_level: Level,
97}
98
99impl Logger {
100    /// Create `Logger` middleware with the specified `format`.
101    pub fn new(format: &str) -> Logger {
102        Logger(Rc::new(Inner {
103            format: Format::new(format),
104            exclude: HashSet::new(),
105            exclude_regex: Vec::new(),
106            log_target: Cow::Borrowed(module_path!()),
107            log_level: Level::Info,
108        }))
109    }
110
111    /// Ignore and do not log access info for specified path.
112    pub fn exclude<T: Into<String>>(mut self, path: T) -> Self {
113        Rc::get_mut(&mut self.0)
114            .unwrap()
115            .exclude
116            .insert(path.into());
117        self
118    }
119
120    /// Ignore and do not log access info for paths that match regex.
121    pub fn exclude_regex<T: Into<String>>(mut self, path: T) -> Self {
122        let inner = Rc::get_mut(&mut self.0).unwrap();
123        inner.exclude_regex.push(Regex::new(&path.into()).unwrap());
124        self
125    }
126
127    /// Sets the logging target to `target`.
128    ///
129    /// By default, the log target is `module_path!()` of the log call location. In our case, that
130    /// would be `actix_web::middleware::logger`.
131    ///
132    /// # Examples
133    /// Using `.log_target("http_log")` would have this effect on request logs:
134    /// ```diff
135    /// - [2015-10-21T07:28:00Z INFO  actix_web::middleware::logger] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
136    /// + [2015-10-21T07:28:00Z INFO  http_log] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
137    ///                               ^^^^^^^^
138    /// ```
139    pub fn log_target(mut self, target: impl Into<Cow<'static, str>>) -> Self {
140        let inner = Rc::get_mut(&mut self.0).unwrap();
141        inner.log_target = target.into();
142        self
143    }
144
145    /// Sets the log level to `level`.
146    ///
147    /// By default, the log level is `Level::Info`.
148    ///
149    /// # Examples
150    /// Using `.log_level(Level::Debug)` would have this effect on request logs:
151    /// ```diff
152    /// - [2015-10-21T07:28:00Z INFO  actix_web::middleware::logger] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
153    /// + [2015-10-21T07:28:00Z DEBUG  actix_web::middleware::logger] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
154    ///                         ^^^^^^
155    /// ```
156    pub fn log_level(mut self, level: log::Level) -> Self {
157        let inner = Rc::get_mut(&mut self.0).unwrap();
158        inner.log_level = level;
159        self
160    }
161
162    /// Register a function that receives a ServiceRequest and returns a String for use in the
163    /// log line. The label passed as the first argument should match a replacement substring in
164    /// the logger format like `%{label}xi`.
165    ///
166    /// It is convention to print "-" to indicate no output instead of an empty string.
167    ///
168    /// # Examples
169    /// ```
170    /// # use actix_web::http::{header::HeaderValue};
171    /// # use actix_web::middleware::Logger;
172    /// # fn parse_jwt_id (_req: Option<&HeaderValue>) -> String { "jwt_uid".to_owned() }
173    /// Logger::new("example %{JWT_ID}xi")
174    ///     .custom_request_replace("JWT_ID", |req| parse_jwt_id(req.headers().get("Authorization")));
175    /// ```
176    pub fn custom_request_replace(
177        mut self,
178        label: &str,
179        f: impl Fn(&ServiceRequest) -> String + 'static,
180    ) -> Self {
181        let inner = Rc::get_mut(&mut self.0).unwrap();
182
183        let ft = inner.format.0.iter_mut().find(
184            |ft| matches!(ft, FormatText::CustomRequest(unit_label, _) if label == unit_label),
185        );
186
187        if let Some(FormatText::CustomRequest(_, request_fn)) = ft {
188            // replace into None or previously registered fn using same label
189            request_fn.replace(CustomRequestFn {
190                inner_fn: Rc::new(f),
191            });
192        } else {
193            // non-printed request replacement function diagnostic
194            debug!(
195                "Attempted to register custom request logging function for nonexistent label: {}",
196                label
197            );
198        }
199
200        self
201    }
202
203    /// Register a function that receives a `ServiceResponse` and returns a string for use in the
204    /// log line.
205    ///
206    /// The label passed as the first argument should match a replacement substring in
207    /// the logger format like `%{label}xo`.
208    ///
209    /// It is convention to print "-" to indicate no output instead of an empty string.
210    ///
211    /// The replacement function does not have access to the response body.
212    ///
213    /// # Examples
214    /// ```
215    /// # use actix_web::{dev::ServiceResponse, middleware::Logger};
216    /// fn log_if_error(res: &ServiceResponse) -> String {
217    ///     if res.status().as_u16() >= 400 {
218    ///         "ERROR".to_string()
219    ///     } else {
220    ///         "-".to_string()
221    ///     }
222    /// }
223    ///
224    /// Logger::new("example %{ERROR_STATUS}xo")
225    ///     .custom_response_replace("ERROR_STATUS", |res| log_if_error(res) );
226    /// ```
227    pub fn custom_response_replace(
228        mut self,
229        label: &str,
230        f: impl Fn(&ServiceResponse) -> String + 'static,
231    ) -> Self {
232        let inner = Rc::get_mut(&mut self.0).unwrap();
233
234        let ft = inner.format.0.iter_mut().find(
235            |ft| matches!(ft, FormatText::CustomResponse(unit_label, _) if label == unit_label),
236        );
237
238        if let Some(FormatText::CustomResponse(_, res_fn)) = ft {
239            *res_fn = Some(CustomResponseFn {
240                inner_fn: Rc::new(f),
241            });
242        } else {
243            debug!(
244                "Attempted to register custom response logging function for non-existent label: {}",
245                label
246            );
247        }
248
249        self
250    }
251}
252
253impl Default for Logger {
254    /// Create `Logger` middleware with format:
255    ///
256    /// ```plain
257    /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
258    /// ```
259    fn default() -> Logger {
260        Logger(Rc::new(Inner {
261            format: Format::default(),
262            exclude: HashSet::new(),
263            exclude_regex: Vec::new(),
264            log_target: Cow::Borrowed(module_path!()),
265            log_level: Level::Info,
266        }))
267    }
268}
269
270#[expect(deprecated, reason = "preserve the public ready future type")]
271impl<S, B> Transform<S, ServiceRequest> for Logger
272where
273    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
274    B: MessageBody,
275{
276    type Response = ServiceResponse<StreamLog<B>>;
277    type Error = Error;
278    type Transform = LoggerMiddleware<S>;
279    type InitError = ();
280    type Future = Ready<Result<Self::Transform, Self::InitError>>;
281
282    fn new_transform(&self, service: S) -> Self::Future {
283        for unit in &self.0.format.0 {
284            if let FormatText::CustomRequest(label, None) = unit {
285                warn!(
286                    "No custom request replacement function was registered for label: {}",
287                    label
288                );
289            }
290
291            if let FormatText::CustomResponse(label, None) = unit {
292                warn!(
293                    "No custom response replacement function was registered for label: {}",
294                    label
295                );
296            }
297        }
298
299        ready(Ok(LoggerMiddleware {
300            service,
301            inner: Rc::clone(&self.0),
302        }))
303    }
304}
305
306/// Logger middleware service.
307pub struct LoggerMiddleware<S> {
308    inner: Rc<Inner>,
309    service: S,
310}
311
312impl<S, B> Service<ServiceRequest> for LoggerMiddleware<S>
313where
314    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
315    B: MessageBody,
316{
317    type Response = ServiceResponse<StreamLog<B>>;
318    type Error = Error;
319    type Future = LoggerResponse<S, B>;
320
321    actix_service::forward_ready!(service);
322
323    fn call(&self, req: ServiceRequest) -> Self::Future {
324        let excluded = self.inner.exclude.contains(req.path())
325            || self
326                .inner
327                .exclude_regex
328                .iter()
329                .any(|r| r.is_match(req.path()));
330
331        if excluded {
332            LoggerResponse {
333                fut: self.service.call(req),
334                format: None,
335                time: OffsetDateTime::now_utc(),
336                log_target: Cow::Borrowed(""),
337                log_level: self.inner.log_level,
338                _phantom: PhantomData,
339            }
340        } else {
341            let now = OffsetDateTime::now_utc();
342            let mut format = self.inner.format.clone();
343
344            for unit in &mut format.0 {
345                unit.render_request(now, &req);
346            }
347
348            LoggerResponse {
349                fut: self.service.call(req),
350                format: Some(format),
351                time: now,
352                log_target: self.inner.log_target.clone(),
353                log_level: self.inner.log_level,
354                _phantom: PhantomData,
355            }
356        }
357    }
358}
359
360pin_project! {
361    pub struct LoggerResponse<S, B>
362    where
363        B: MessageBody,
364        S: Service<ServiceRequest>,
365    {
366        #[pin]
367        fut: S::Future,
368        time: OffsetDateTime,
369        format: Option<Format>,
370        log_target: Cow<'static, str>,
371        log_level: Level,
372        _phantom: PhantomData<B>,
373    }
374}
375
376impl<S, B> Future for LoggerResponse<S, B>
377where
378    B: MessageBody,
379    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
380{
381    type Output = Result<ServiceResponse<StreamLog<B>>, Error>;
382
383    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
384        let this = self.project();
385
386        let res = match ready!(this.fut.poll(cx)) {
387            Ok(res) => res,
388            Err(err) => return Poll::Ready(Err(err)),
389        };
390
391        if let Some(error) = res.response().error() {
392            debug!("Error in response: {:?}", error);
393        }
394
395        let res = if let Some(ref mut format) = this.format {
396            // to avoid polluting all the Logger types with the body parameter we swap the body
397            // out temporarily since it's not usable in custom response functions anyway
398
399            let (req, res) = res.into_parts();
400            let (res, body) = res.into_parts();
401
402            let temp_res = ServiceResponse::new(req, res.map_into_boxed_body());
403
404            for unit in &mut format.0 {
405                unit.render_response(&temp_res);
406            }
407
408            // re-construct original service response
409            let (req, res) = temp_res.into_parts();
410            ServiceResponse::new(req, res.set_body(body))
411        } else {
412            res
413        };
414
415        let time = *this.time;
416        let format = this.format.take();
417        let log_target = this.log_target.clone();
418        let log_level = *this.log_level;
419
420        Poll::Ready(Ok(res.map_body(move |_, body| StreamLog {
421            body,
422            time,
423            format,
424            size: 0,
425            log_target,
426            log_level,
427        })))
428    }
429}
430
431pin_project! {
432    pub struct StreamLog<B> {
433        #[pin]
434        body: B,
435        format: Option<Format>,
436        size: usize,
437        time: OffsetDateTime,
438        log_target: Cow<'static, str>,
439        log_level: Level
440    }
441
442    impl<B> PinnedDrop for StreamLog<B> {
443        fn drop(this: Pin<&mut Self>) {
444            if let Some(ref format) = this.format {
445                let render = |fmt: &mut fmt::Formatter<'_>| {
446                    for unit in &format.0 {
447                        unit.render(fmt, this.size, this.time)?;
448                    }
449                    Ok(())
450                };
451
452                log::log!(
453                    target: this.log_target.as_ref(),
454                    this.log_level,
455                    "{}", FormatDisplay(&render)
456                );
457            }
458        }
459    }
460}
461
462impl<B: MessageBody> MessageBody for StreamLog<B> {
463    type Error = B::Error;
464
465    #[inline]
466    fn size(&self) -> BodySize {
467        self.body.size()
468    }
469
470    fn poll_next(
471        self: Pin<&mut Self>,
472        cx: &mut Context<'_>,
473    ) -> Poll<Option<Result<Bytes, Self::Error>>> {
474        let this = self.project();
475
476        match ready!(this.body.poll_next(cx)) {
477            Some(Ok(chunk)) => {
478                *this.size += chunk.len();
479                Poll::Ready(Some(Ok(chunk)))
480            }
481            Some(Err(err)) => Poll::Ready(Some(Err(err))),
482            None => Poll::Ready(None),
483        }
484    }
485}
486
487/// A formatting style for the `Logger` consisting of multiple concatenated `FormatText` items.
488#[derive(Debug, Clone)]
489struct Format(Vec<FormatText>);
490
491impl Default for Format {
492    /// Return the default formatting style for the `Logger`:
493    fn default() -> Format {
494        Format::new(r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#)
495    }
496}
497
498impl Format {
499    /// Create a `Format` from a format string.
500    ///
501    /// Returns `None` if the format string syntax is incorrect.
502    pub fn new(s: &str) -> Format {
503        log::trace!("Access log format: {}", s);
504        let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|x[io])|[%atPrUsbTD]?)").unwrap();
505
506        let mut idx = 0;
507        let mut results = Vec::new();
508        for cap in fmt.captures_iter(s) {
509            let m = cap.get(0).unwrap();
510            let pos = m.start();
511            if idx != pos {
512                results.push(FormatText::Str(s[idx..pos].to_owned()));
513            }
514            idx = m.end();
515
516            if let Some(key) = cap.get(2) {
517                results.push(match cap.get(3).unwrap().as_str() {
518                    "a" => {
519                        if key.as_str() == "r" {
520                            FormatText::RealIpRemoteAddr
521                        } else {
522                            unreachable!("regex and code mismatch")
523                        }
524                    }
525                    "i" => FormatText::RequestHeader(HeaderName::try_from(key.as_str()).unwrap()),
526                    "o" => FormatText::ResponseHeader(HeaderName::try_from(key.as_str()).unwrap()),
527                    "e" => FormatText::EnvironHeader(key.as_str().to_owned()),
528                    "xi" => FormatText::CustomRequest(key.as_str().to_owned(), None),
529                    "xo" => FormatText::CustomResponse(key.as_str().to_owned(), None),
530                    _ => unreachable!(),
531                })
532            } else {
533                let m = cap.get(1).unwrap();
534                results.push(match m.as_str() {
535                    "%" => FormatText::Percent,
536                    "a" => FormatText::RemoteAddr,
537                    "t" => FormatText::RequestTime,
538                    "r" => FormatText::RequestLine,
539                    "s" => FormatText::ResponseStatus,
540                    "b" => FormatText::ResponseSize,
541                    "U" => FormatText::UrlPath,
542                    "T" => FormatText::Time,
543                    "D" => FormatText::TimeMillis,
544                    _ => FormatText::Str(m.as_str().to_owned()),
545                });
546            }
547        }
548        if idx != s.len() {
549            results.push(FormatText::Str(s[idx..].to_owned()));
550        }
551
552        Format(results)
553    }
554}
555
556/// A string of text to be logged.
557///
558/// This is either one of the data fields supported by the `Logger`, or a custom `String`.
559#[non_exhaustive]
560#[derive(Debug, Clone)]
561enum FormatText {
562    Str(String),
563    Percent,
564    RequestLine,
565    RequestTime,
566    ResponseStatus,
567    ResponseSize,
568    Time,
569    TimeMillis,
570    RemoteAddr,
571    RealIpRemoteAddr,
572    UrlPath,
573    RequestHeader(HeaderName),
574    ResponseHeader(HeaderName),
575    EnvironHeader(String),
576    CustomRequest(String, Option<CustomRequestFn>),
577    CustomResponse(String, Option<CustomResponseFn>),
578}
579
580#[derive(Clone)]
581struct CustomRequestFn {
582    inner_fn: Rc<dyn Fn(&ServiceRequest) -> String>,
583}
584
585impl CustomRequestFn {
586    fn call(&self, req: &ServiceRequest) -> String {
587        (self.inner_fn)(req)
588    }
589}
590
591impl fmt::Debug for CustomRequestFn {
592    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
593        f.write_str("custom_request_fn")
594    }
595}
596
597#[derive(Clone)]
598struct CustomResponseFn {
599    inner_fn: Rc<dyn Fn(&ServiceResponse) -> String>,
600}
601
602impl CustomResponseFn {
603    fn call(&self, res: &ServiceResponse) -> String {
604        (self.inner_fn)(res)
605    }
606}
607
608impl fmt::Debug for CustomResponseFn {
609    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
610        f.write_str("custom_response_fn")
611    }
612}
613
614impl FormatText {
615    fn render(
616        &self,
617        fmt: &mut fmt::Formatter<'_>,
618        size: usize,
619        entry_time: OffsetDateTime,
620    ) -> Result<(), fmt::Error> {
621        match self {
622            FormatText::Str(ref string) => fmt.write_str(string),
623            FormatText::Percent => "%".fmt(fmt),
624            FormatText::ResponseSize => size.fmt(fmt),
625            FormatText::Time => {
626                let rt = OffsetDateTime::now_utc() - entry_time;
627                let rt = rt.as_seconds_f64();
628                fmt.write_fmt(format_args!("{:.6}", rt))
629            }
630            FormatText::TimeMillis => {
631                let rt = OffsetDateTime::now_utc() - entry_time;
632                let rt = (rt.whole_nanoseconds() as f64) / 1_000_000.0;
633                fmt.write_fmt(format_args!("{:.6}", rt))
634            }
635            FormatText::EnvironHeader(ref name) => {
636                if let Ok(val) = env::var(name) {
637                    fmt.write_fmt(format_args!("{}", val))
638                } else {
639                    "-".fmt(fmt)
640                }
641            }
642            _ => Ok(()),
643        }
644    }
645
646    fn render_response(&mut self, res: &ServiceResponse) {
647        match self {
648            FormatText::ResponseStatus => {
649                *self = FormatText::Str(format!("{}", res.status().as_u16()))
650            }
651
652            FormatText::ResponseHeader(ref name) => {
653                let s = if let Some(val) = res.headers().get(name) {
654                    String::from_utf8_lossy(val.as_bytes()).into_owned()
655                } else {
656                    "-".to_owned()
657                };
658                *self = FormatText::Str(s.to_string())
659            }
660
661            FormatText::CustomResponse(_, res_fn) => {
662                let text = match res_fn {
663                    Some(res_fn) => FormatText::Str(res_fn.call(res)),
664                    None => FormatText::Str("-".to_owned()),
665                };
666
667                *self = text;
668            }
669
670            _ => {}
671        }
672    }
673
674    fn render_request(&mut self, now: OffsetDateTime, req: &ServiceRequest) {
675        match self {
676            FormatText::RequestLine => {
677                *self = if req.query_string().is_empty() {
678                    FormatText::Str(format!(
679                        "{} {} {:?}",
680                        req.method(),
681                        req.path(),
682                        req.version()
683                    ))
684                } else {
685                    FormatText::Str(format!(
686                        "{} {}?{} {:?}",
687                        req.method(),
688                        req.path(),
689                        req.query_string(),
690                        req.version()
691                    ))
692                };
693            }
694            FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()),
695            FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()),
696            FormatText::RequestHeader(ref name) => {
697                let s = if let Some(val) = req.headers().get(name) {
698                    String::from_utf8_lossy(val.as_bytes()).into_owned()
699                } else {
700                    "-".to_owned()
701                };
702                *self = FormatText::Str(s);
703            }
704            FormatText::RemoteAddr => {
705                let s = if let Some(peer) = req.connection_info().peer_addr() {
706                    FormatText::Str((*peer).to_string())
707                } else {
708                    FormatText::Str("-".to_string())
709                };
710                *self = s;
711            }
712            FormatText::RealIpRemoteAddr => {
713                let s = if let Some(remote) = req.connection_info().realip_remote_addr() {
714                    FormatText::Str(remote.to_string())
715                } else {
716                    FormatText::Str("-".to_string())
717                };
718                *self = s;
719            }
720            FormatText::CustomRequest(_, request_fn) => {
721                let s = match request_fn {
722                    Some(f) => FormatText::Str(f.call(req)),
723                    None => FormatText::Str("-".to_owned()),
724                };
725
726                *self = s;
727            }
728            _ => {}
729        }
730    }
731}
732
733/// Converter to get a String from something that writes to a Formatter.
734pub(crate) struct FormatDisplay<'a>(&'a dyn Fn(&mut fmt::Formatter<'_>) -> Result<(), fmt::Error>);
735
736impl fmt::Display for FormatDisplay<'_> {
737    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
738        (self.0)(fmt)
739    }
740}
741
742#[cfg(test)]
743mod tests {
744    use std::future::ready;
745
746    use actix_service::IntoService;
747
748    use super::*;
749    use crate::{
750        http::{header, StatusCode},
751        test::{self, TestRequest},
752        HttpResponse,
753    };
754
755    #[actix_rt::test]
756    async fn test_logger() {
757        let srv = |req: ServiceRequest| {
758            ready(Ok(req.into_response(
759                HttpResponse::build(StatusCode::OK)
760                    .insert_header(("X-Test", "ttt"))
761                    .finish(),
762            )))
763        };
764        let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test");
765
766        let srv = logger.new_transform(srv.into_service()).await.unwrap();
767
768        let req = TestRequest::default()
769            .insert_header((
770                header::USER_AGENT,
771                header::HeaderValue::from_static("ACTIX-WEB"),
772            ))
773            .to_srv_request();
774        let _res = srv.call(req).await;
775    }
776
777    #[actix_rt::test]
778    async fn test_logger_exclude_regex() {
779        let srv = |req: ServiceRequest| {
780            ready(Ok(req.into_response(
781                HttpResponse::build(StatusCode::OK)
782                    .insert_header(("X-Test", "ttt"))
783                    .finish(),
784            )))
785        };
786        let logger =
787            Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test").exclude_regex("\\w");
788
789        let srv = logger.new_transform(srv.into_service()).await.unwrap();
790
791        let req = TestRequest::default()
792            .insert_header((
793                header::USER_AGENT,
794                header::HeaderValue::from_static("ACTIX-WEB"),
795            ))
796            .to_srv_request();
797        let _res = srv.call(req).await.unwrap();
798    }
799
800    #[actix_rt::test]
801    async fn test_escape_percent() {
802        let mut format = Format::new("%%{r}a");
803
804        let req = TestRequest::default()
805            .insert_header((
806                header::FORWARDED,
807                header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"),
808            ))
809            .to_srv_request();
810
811        let now = OffsetDateTime::now_utc();
812        for unit in &mut format.0 {
813            unit.render_request(now, &req);
814        }
815
816        let req = TestRequest::default().to_http_request();
817        let res = ServiceResponse::new(req, HttpResponse::Ok().finish());
818        for unit in &mut format.0 {
819            unit.render_response(&res);
820        }
821
822        let entry_time = OffsetDateTime::now_utc();
823        let render = |fmt: &mut fmt::Formatter<'_>| {
824            for unit in &format.0 {
825                unit.render(fmt, 1024, entry_time)?;
826            }
827            Ok(())
828        };
829        let s = format!("{}", FormatDisplay(&render));
830        assert_eq!(s, "%{r}a");
831    }
832
833    #[actix_rt::test]
834    async fn test_url_path() {
835        let mut format = Format::new("%T %U");
836        let req = TestRequest::default()
837            .insert_header((
838                header::USER_AGENT,
839                header::HeaderValue::from_static("ACTIX-WEB"),
840            ))
841            .uri("/test/route/yeah")
842            .to_srv_request();
843
844        let now = OffsetDateTime::now_utc();
845        for unit in &mut format.0 {
846            unit.render_request(now, &req);
847        }
848
849        let req = TestRequest::default().to_http_request();
850        let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish());
851        for unit in &mut format.0 {
852            unit.render_response(&res);
853        }
854
855        let render = |fmt: &mut fmt::Formatter<'_>| {
856            for unit in &format.0 {
857                unit.render(fmt, 1024, now)?;
858            }
859            Ok(())
860        };
861        let s = format!("{}", FormatDisplay(&render));
862        assert!(s.contains("/test/route/yeah"));
863    }
864
865    #[actix_rt::test]
866    async fn test_default_format() {
867        let mut format = Format::default();
868
869        let req = TestRequest::default()
870            .insert_header((
871                header::USER_AGENT,
872                header::HeaderValue::from_static("ACTIX-WEB"),
873            ))
874            .peer_addr("127.0.0.1:8081".parse().unwrap())
875            .to_srv_request();
876
877        let now = OffsetDateTime::now_utc();
878        for unit in &mut format.0 {
879            unit.render_request(now, &req);
880        }
881
882        let req = TestRequest::default().to_http_request();
883        let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish());
884        for unit in &mut format.0 {
885            unit.render_response(&res);
886        }
887
888        let entry_time = OffsetDateTime::now_utc();
889        let render = |fmt: &mut fmt::Formatter<'_>| {
890            for unit in &format.0 {
891                unit.render(fmt, 1024, entry_time)?;
892            }
893            Ok(())
894        };
895        let s = format!("{}", FormatDisplay(&render));
896        assert!(s.contains("GET / HTTP/1.1"));
897        assert!(s.contains("127.0.0.1"));
898        assert!(s.contains("200 1024"));
899        assert!(s.contains("ACTIX-WEB"));
900    }
901
902    #[actix_rt::test]
903    async fn test_request_time_format() {
904        let mut format = Format::new("%t");
905        let req = TestRequest::default().to_srv_request();
906
907        let now = OffsetDateTime::now_utc();
908        for unit in &mut format.0 {
909            unit.render_request(now, &req);
910        }
911
912        let req = TestRequest::default().to_http_request();
913        let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish());
914        for unit in &mut format.0 {
915            unit.render_response(&res);
916        }
917
918        let render = |fmt: &mut fmt::Formatter<'_>| {
919            for unit in &format.0 {
920                unit.render(fmt, 1024, now)?;
921            }
922            Ok(())
923        };
924        let s = format!("{}", FormatDisplay(&render));
925        assert!(s.contains(&now.format(&Rfc3339).unwrap()));
926    }
927
928    #[actix_rt::test]
929    async fn test_remote_addr_format() {
930        let mut format = Format::new("%{r}a");
931
932        let req = TestRequest::default()
933            .insert_header((
934                header::FORWARDED,
935                header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"),
936            ))
937            .to_srv_request();
938
939        let now = OffsetDateTime::now_utc();
940        for unit in &mut format.0 {
941            unit.render_request(now, &req);
942        }
943
944        let req = TestRequest::default().to_http_request();
945        let res = ServiceResponse::new(req, HttpResponse::Ok().finish());
946        for unit in &mut format.0 {
947            unit.render_response(&res);
948        }
949
950        let entry_time = OffsetDateTime::now_utc();
951        let render = |fmt: &mut fmt::Formatter<'_>| {
952            for unit in &format.0 {
953                unit.render(fmt, 1024, entry_time)?;
954            }
955            Ok(())
956        };
957        let s = format!("{}", FormatDisplay(&render));
958        assert!(s.contains("192.0.2.60"));
959    }
960
961    #[actix_rt::test]
962    async fn test_custom_closure_req_log() {
963        let mut logger = Logger::new("test %{CUSTOM}xi")
964            .custom_request_replace("CUSTOM", |_req: &ServiceRequest| -> String {
965                String::from("custom_log")
966            });
967        let mut unit = Rc::get_mut(&mut logger.0).unwrap().format.0[1].clone();
968
969        let label = match &unit {
970            FormatText::CustomRequest(label, _) => label,
971            ft => panic!("expected CustomRequest, found {:?}", ft),
972        };
973
974        assert_eq!(label, "CUSTOM");
975
976        let req = TestRequest::default().to_srv_request();
977        let now = OffsetDateTime::now_utc();
978
979        unit.render_request(now, &req);
980
981        let render = |fmt: &mut fmt::Formatter<'_>| unit.render(fmt, 1024, now);
982
983        let log_output = FormatDisplay(&render).to_string();
984        assert_eq!(log_output, "custom_log");
985    }
986
987    #[actix_rt::test]
988    async fn test_custom_closure_response_log() {
989        let mut logger = Logger::new("test %{CUSTOM}xo").custom_response_replace(
990            "CUSTOM",
991            |res: &ServiceResponse| -> String {
992                if res.status().as_u16() == 200 {
993                    String::from("custom_log")
994                } else {
995                    String::from("-")
996                }
997            },
998        );
999        let mut unit = Rc::get_mut(&mut logger.0).unwrap().format.0[1].clone();
1000
1001        let label = match &unit {
1002            FormatText::CustomResponse(label, _) => label,
1003            ft => panic!("expected CustomResponse, found {:?}", ft),
1004        };
1005
1006        assert_eq!(label, "CUSTOM");
1007
1008        let req = TestRequest::default().to_http_request();
1009        let resp_ok = ServiceResponse::new(req, HttpResponse::Ok().finish());
1010        let now = OffsetDateTime::now_utc();
1011        unit.render_response(&resp_ok);
1012
1013        let render = |fmt: &mut fmt::Formatter<'_>| unit.render(fmt, 1024, now);
1014
1015        let log_output = FormatDisplay(&render).to_string();
1016        assert_eq!(log_output, "custom_log");
1017    }
1018
1019    #[actix_rt::test]
1020    async fn test_closure_logger_in_middleware() {
1021        let captured = "custom log replacement";
1022
1023        let logger = Logger::new("%{CUSTOM}xi")
1024            .custom_request_replace("CUSTOM", move |_req: &ServiceRequest| -> String {
1025                captured.to_owned()
1026            });
1027
1028        let srv = logger.new_transform(test::ok_service()).await.unwrap();
1029
1030        let req = TestRequest::default().to_srv_request();
1031        srv.call(req).await.unwrap();
1032    }
1033}