actix_logger/
lib.rs

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