1use 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#[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 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 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 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 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 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 request_fn.replace(CustomRequestFn {
175 inner_fn: Rc::new(f),
176 });
177 } else {
178 debug!(
180 "Attempted to register custom request logging function for nonexistent label: {}",
181 label
182 );
183 }
184
185 self
186 }
187
188 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 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
289pub 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 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 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#[derive(Debug, Clone)]
471struct Format(Vec<FormatText>);
472
473impl Default for Format {
474 fn default() -> Self {
476 Self::new(r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#)
477 }
478}
479
480impl Format {
481 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#[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
721pub(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}