1use 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#[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 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 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 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 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 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 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 request_fn.replace(CustomRequestFn {
190 inner_fn: Rc::new(f),
191 });
192 } else {
193 debug!(
195 "Attempted to register custom request logging function for nonexistent label: {}",
196 label
197 );
198 }
199
200 self
201 }
202
203 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 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
306pub 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 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 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#[derive(Debug, Clone)]
489struct Format(Vec<FormatText>);
490
491impl Default for Format {
492 fn default() -> Format {
494 Format::new(r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#)
495 }
496}
497
498impl Format {
499 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#[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
733pub(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}