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