#1749 register fns for custom request-derived logging units

Open jdeepee
Coverage Reach
actix-http/src/h1/dispatcher.rs actix-http/src/h1/decoder.rs actix-http/src/h1/encoder.rs actix-http/src/h1/service.rs actix-http/src/h1/client.rs actix-http/src/h1/codec.rs actix-http/src/h1/payload.rs actix-http/src/h1/utils.rs actix-http/src/h1/mod.rs actix-http/src/h1/expect.rs actix-http/src/h1/upgrade.rs actix-http/src/header/common/content_disposition.rs actix-http/src/header/common/range.rs actix-http/src/header/common/content_range.rs actix-http/src/header/common/cache_control.rs actix-http/src/header/common/if_range.rs actix-http/src/header/common/mod.rs actix-http/src/header/common/content_type.rs actix-http/src/header/common/accept.rs actix-http/src/header/common/date.rs actix-http/src/header/common/last_modified.rs actix-http/src/header/common/allow.rs actix-http/src/header/shared/charset.rs actix-http/src/header/shared/entity.rs actix-http/src/header/shared/quality_item.rs actix-http/src/header/shared/httpdate.rs actix-http/src/header/shared/encoding.rs actix-http/src/header/map.rs actix-http/src/header/mod.rs actix-http/src/client/pool.rs actix-http/src/client/connector.rs actix-http/src/client/h1proto.rs actix-http/src/client/h2proto.rs actix-http/src/client/connection.rs actix-http/src/client/error.rs actix-http/src/client/config.rs actix-http/src/ws/codec.rs actix-http/src/ws/frame.rs actix-http/src/ws/proto.rs actix-http/src/ws/mod.rs actix-http/src/ws/mask.rs actix-http/src/ws/dispatcher.rs actix-http/src/response.rs actix-http/src/encoding/encoder.rs actix-http/src/encoding/decoder.rs actix-http/src/encoding/mod.rs actix-http/src/h2/dispatcher.rs actix-http/src/h2/service.rs actix-http/src/h2/mod.rs actix-http/src/message.rs actix-http/src/service.rs actix-http/src/body.rs actix-http/src/error.rs actix-http/src/config.rs actix-http/src/test.rs actix-http/src/builder.rs actix-http/src/request.rs actix-http/src/httpmessage.rs actix-http/src/helpers.rs actix-http/src/extensions.rs actix-http/src/time_parser.rs actix-http/src/payload.rs actix-http/src/cloneable.rs actix-http/src/httpcodes.rs actix-http/src/macros.rs src/types/json.rs src/types/payload.rs src/types/form.rs src/types/path.rs src/types/readlines.rs src/types/query.rs src/middleware/logger.rs src/middleware/normalize.rs src/middleware/defaultheaders.rs src/middleware/compress.rs src/middleware/errhandlers.rs src/middleware/condition.rs src/scope.rs src/test.rs src/request.rs src/resource.rs src/service.rs src/server.rs src/app.rs src/rmap.rs src/responder.rs src/guard.rs src/app_service.rs src/data.rs src/info.rs src/config.rs src/route.rs src/extract.rs src/request_data.rs src/handler.rs src/error.rs src/web.rs src/lib.rs awc/src/request.rs awc/src/ws.rs awc/src/sender.rs awc/src/response.rs awc/src/frozen.rs awc/src/builder.rs awc/src/connect.rs awc/src/test.rs awc/src/lib.rs awc/src/error.rs actix-files/src/named.rs actix-files/src/files.rs actix-files/src/service.rs actix-files/src/range.rs actix-files/src/chunked.rs actix-files/src/path_buf.rs actix-files/src/directory.rs actix-files/src/error.rs actix-files/src/lib.rs tests/test_server.rs tests/test_httpserver.rs actix-multipart/src/server.rs actix-multipart/src/extractor.rs actix-multipart/src/error.rs actix-web-actors/src/ws.rs actix-web-actors/src/context.rs actix-web-codegen/src/route.rs

No flags found

Use flags to group coverage reports by test type, project and/or folders.
Then setup custom commit statuses and notifications for each flag.

e.g., #unittest #integration

#production #enterprise

#frontend #backend

Learn more about Codecov Flags here.

Showing 1 of 2 files from the diff.
Other files ignored by Codecov
CHANGES.md has changed.

@@ -34,21 +34,19 @@
Loading
34 34
/// Default `Logger` could be created with `default` method, it uses the
35 35
/// default format:
36 36
///
37 -
/// ```ignore
38 -
///  %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
37 +
/// ```plain
38 +
/// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
39 39
/// ```
40 +
///
40 41
/// ```rust
41 -
/// use actix_web::middleware::Logger;
42 -
/// use actix_web::App;
42 +
/// use actix_web::{middleware::Logger, App};
43 43
///
44 -
/// fn main() {
45 -
///     std::env::set_var("RUST_LOG", "actix_web=info");
46 -
///     env_logger::init();
44 +
/// std::env::set_var("RUST_LOG", "actix_web=info");
45 +
/// env_logger::init();
47 46
///
48 -
///     let app = App::new()
49 -
///         .wrap(Logger::default())
50 -
///         .wrap(Logger::new("%a %{User-Agent}i"));
51 -
/// }
47 +
/// let app = App::new()
48 +
///     .wrap(Logger::default())
49 +
///     .wrap(Logger::new("%a %{User-Agent}i"));
52 50
/// ```
53 51
///
54 52
/// ## Format
@@ -80,6 +78,8 @@
Loading
80 78
///
81 79
/// `%{FOO}e`  os.environ['FOO']
82 80
///
81 +
/// `%{FOO}xi`  [custom request replacement](Logger::custom_request_replace) labelled "FOO"
82 +
///
83 83
/// # Security
84 84
///  **\*** It is calculated using
85 85
///  [`ConnectionInfo::realip_remote_addr()`](../dev/struct.ConnectionInfo.html#method.realip_remote_addr)
@@ -123,12 +123,52 @@
Loading
123 123
        inner.exclude_regex = regex_set;
124 124
        self
125 125
    }
126 +
127 +
    /// Register a function that receives a ServiceRequest and returns a String for use in the
128 +
    /// log line. The label passed as the first argument should match a replacement substring in
129 +
    /// the logger format like `%{label}xi`.
130 +
    ///
131 +
    /// It is convention to print "-" to indicate no output instead of an empty string.
132 +
    ///
133 +
    /// # Example
134 +
    /// ```rust
135 +
    /// # use actix_web::{http::HeaderValue, middleware::Logger};
136 +
    /// # fn parse_jwt_id (_req: Option<&HeaderValue>) -> String { "jwt_uid".to_owned() }
137 +
    /// Logger::new("example %{JWT_ID}xi")
138 +
    ///     .custom_request_replace("JWT_ID", |req| parse_jwt_id(req.headers().get("Authorization")));
139 +
    /// ```
140 +
    pub fn custom_request_replace(
141 +
        mut self,
142 +
        label: &str,
143 +
        f: impl Fn(&ServiceRequest) -> String + 'static,
144 +
    ) -> Self {
145 +
        let inner = Rc::get_mut(&mut self.0).unwrap();
146 +
147 +
        let ft = inner.format.0.iter_mut().find(|ft| {
148 +
            matches!(ft, FormatText::CustomRequest(unit_label, _) if label == unit_label)
149 +
        });
150 +
151 +
        if let Some(FormatText::CustomRequest(_, request_fn)) = ft {
152 +
            // replace into None or previously registered fn using same label
153 +
            request_fn.replace(CustomRequestFn {
154 +
                inner_fn: Rc::new(f),
155 +
            });
156 +
        } else {
157 +
            // non-printed request replacement function diagnostic
158 +
            debug!(
159 +
                "Attempted to register custom request logging function for nonexistent label: {}",
160 +
                label
161 +
            );
162 +
        }
163 +
164 +
        self
165 +
    }
126 166
}
127 167
128 168
impl Default for Logger {
129 169
    /// Create `Logger` middleware with format:
130 170
    ///
131 -
    /// ```ignore
171 +
    /// ```plain
132 172
    /// %a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T
133 173
    /// ```
134 174
    fn default() -> Logger {
@@ -153,6 +193,17 @@
Loading
153 193
    type Future = Ready<Result<Self::Transform, Self::InitError>>;
154 194
155 195
    fn new_transform(&self, service: S) -> Self::Future {
196 +
        for unit in &self.0.format.0 {
197 +
            // missing request replacement function diagnostic
198 +
            if let FormatText::CustomRequest(label, None) = unit {
199 +
                debug!(
200 +
                    "No custom request replacement function was registered for label {} in\
201 +
                    logger format.",
202 +
                    label
203 +
                );
204 +
            }
205 +
        }
206 +
156 207
        ok(LoggerMiddleware {
157 208
            service,
158 209
            inner: self.0.clone(),
@@ -311,7 +362,6 @@
Loading
311 362
/// A formatting style for the `Logger`, consisting of multiple
312 363
/// `FormatText`s concatenated into one line.
313 364
#[derive(Clone)]
314 -
#[doc(hidden)]
315 365
struct Format(Vec<FormatText>);
316 366
317 367
impl Default for Format {
@@ -327,7 +377,8 @@
Loading
327 377
    /// Returns `None` if the format string syntax is incorrect.
328 378
    pub fn new(s: &str) -> Format {
329 379
        log::trace!("Access log format: {}", s);
330 -
        let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe])|[atPrUsbTD]?)").unwrap();
380 +
        let fmt =
381 +
            Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|xi)|[atPrUsbTD]?)").unwrap();
331 382
332 383
        let mut idx = 0;
333 384
        let mut results = Vec::new();
@@ -355,6 +406,7 @@
Loading
355 406
                        HeaderName::try_from(key.as_str()).unwrap(),
356 407
                    ),
357 408
                    "e" => FormatText::EnvironHeader(key.as_str().to_owned()),
409 +
                    "xi" => FormatText::CustomRequest(key.as_str().to_owned(), None),
358 410
                    _ => unreachable!(),
359 411
                })
360 412
            } else {
@@ -384,7 +436,9 @@
Loading
384 436
/// A string of text to be logged. This is either one of the data
385 437
/// fields supported by the `Logger`, or a custom `String`.
386 438
#[doc(hidden)]
439 +
#[non_exhaustive]
387 440
#[derive(Debug, Clone)]
441 +
// TODO: remove pub on next breaking change
388 442
pub enum FormatText {
389 443
    Str(String),
390 444
    Percent,
@@ -400,6 +454,26 @@
Loading
400 454
    RequestHeader(HeaderName),
401 455
    ResponseHeader(HeaderName),
402 456
    EnvironHeader(String),
457 +
    CustomRequest(String, Option<CustomRequestFn>),
458 +
}
459 +
460 +
// TODO: remove pub on next breaking change
461 +
#[doc(hidden)]
462 +
#[derive(Clone)]
463 +
pub struct CustomRequestFn {
464 +
    inner_fn: Rc<dyn Fn(&ServiceRequest) -> String>,
465 +
}
466 +
467 +
impl CustomRequestFn {
468 +
    fn call(&self, req: &ServiceRequest) -> String {
469 +
        (self.inner_fn)(req)
470 +
    }
471 +
}
472 +
473 +
impl fmt::Debug for CustomRequestFn {
474 +
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475 +
        f.write_str("custom_request_fn")
476 +
    }
403 477
}
404 478
405 479
impl FormatText {
@@ -456,7 +530,7 @@
Loading
456 530
    }
457 531
458 532
    fn render_request(&mut self, now: OffsetDateTime, req: &ServiceRequest) {
459 -
        match *self {
533 +
        match &*self {
460 534
            FormatText::RequestLine => {
461 535
                *self = if req.query_string().is_empty() {
462 536
                    FormatText::Str(format!(
@@ -508,11 +582,20 @@
Loading
508 582
                };
509 583
                *self = s;
510 584
            }
585 +
            FormatText::CustomRequest(_, request_fn) => {
586 +
                let s = match request_fn {
587 +
                    Some(f) => FormatText::Str(f.call(req)),
588 +
                    None => FormatText::Str("-".to_owned()),
589 +
                };
590 +
591 +
                *self = s;
592 +
            }
511 593
            _ => (),
512 594
        }
513 595
    }
514 596
}
515 597
598 +
/// Converter to get a String from something that writes to a Formatter.
516 599
pub(crate) struct FormatDisplay<'a>(
517 600
    &'a dyn Fn(&mut Formatter<'_>) -> Result<(), fmt::Error>,
518 601
);
@@ -530,7 +613,7 @@
Loading
530 613
531 614
    use super::*;
532 615
    use crate::http::{header, StatusCode};
533 -
    use crate::test::TestRequest;
616 +
    use crate::test::{self, TestRequest};
534 617
535 618
    #[actix_rt::test]
536 619
    async fn test_logger() {
@@ -699,4 +782,45 @@
Loading
699 782
        println!("{}", s);
700 783
        assert!(s.contains("192.0.2.60"));
701 784
    }
785 +
786 +
    #[actix_rt::test]
787 +
    async fn test_custom_closure_log() {
788 +
        let mut logger = Logger::new("test %{CUSTOM}xi")
789 +
            .custom_request_replace("CUSTOM", |_req: &ServiceRequest| -> String {
790 +
                String::from("custom_log")
791 +
            });
792 +
        let mut unit = Rc::get_mut(&mut logger.0).unwrap().format.0[1].clone();
793 +
794 +
        let label = match &unit {
795 +
            FormatText::CustomRequest(label, _) => label,
796 +
            ft => panic!("expected CustomRequest, found {:?}", ft),
797 +
        };
798 +
799 +
        assert_eq!(label, "CUSTOM");
800 +
801 +
        let req = TestRequest::default().to_srv_request();
802 +
        let now = OffsetDateTime::now_utc();
803 +
804 +
        unit.render_request(now, &req);
805 +
806 +
        let render = |fmt: &mut Formatter<'_>| unit.render(fmt, 1024, now);
807 +
808 +
        let log_output = FormatDisplay(&render).to_string();
809 +
        assert_eq!(log_output, "custom_log");
810 +
    }
811 +
812 +
    #[actix_rt::test]
813 +
    async fn test_closure_logger_in_middleware() {
814 +
        let captured = "custom log replacement";
815 +
816 +
        let logger = Logger::new("%{CUSTOM}xi")
817 +
            .custom_request_replace("CUSTOM", move |_req: &ServiceRequest| -> String {
818 +
                captured.to_owned()
819 +
            });
820 +
821 +
        let mut srv = logger.new_transform(test::ok_service()).await.unwrap();
822 +
823 +
        let req = TestRequest::default().to_srv_request();
824 +
        srv.call(req).await.unwrap();
825 +
    }
702 826
}

Learn more Showing 6 files with coverage changes found.

actix-http/src/header/common/if_none_match.rs
Loading file...
actix-http/src/header/common/accept_language.rs
Loading file...
actix-http/src/header/common/if_unmodified_since.rs
Loading file...
New file actix-http/src/header/common/allow.rs
New
Loading file...
New file actix-http/src/header/common/last_modified.rs
New
Loading file...
Changes in actix-http/src/header/common/mod.rs
-12
Loading file...
Files Coverage
actix-files/src 0.00%
actix-http/src 0.10% 37.87%
actix-multipart/src 0.00%
actix-web-actors/src 0.00%
awc/src 17.98%
src -0.05% 87.02%
tests 100.00%
actix-web-codegen/src/route.rs 0.00%
Project Totals (125 files) 53.98%
Loading