1use std::{
4 borrow::Cow,
5 collections::HashSet,
6 convert::TryFrom,
7 env,
8 fmt::{self, Display as _},
9 future::Future,
10 marker::PhantomData,
11 pin::Pin,
12 rc::Rc,
13 task::{Context, Poll},
14};
15
16use actix_service::{Service, Transform};
17use actix_utils::future::{ready, Ready};
18use bytes::Bytes;
19use futures_core::ready;
20use log::{debug, Level, warn};
21use pin_project_lite::pin_project;
22use regex::{Regex, RegexSet};
23use time::{format_description::well_known::Rfc3339, OffsetDateTime};
24
25use actix_http::{
26 body::{BodySize, MessageBody},
27};
28use actix_web::error::Error;
29use actix_web::dev::{ServiceRequest, ServiceResponse};
30use actix_web::error::Result;
31use http::{HeaderName, StatusCode};
32
33#[derive(Debug)]
97pub struct Logger(Rc<Inner>);
98
99#[derive(Debug, Clone)]
100struct Inner {
101 format: Format,
102 exclude: HashSet<String>,
103 exclude_regex: RegexSet,
104 log_target: Cow<'static, str>,
105 custom_level_func: Option<fn(StatusCode) -> Level>,
106 custom_resp_error_level_func: Option<fn(StatusCode) -> Level>,
107}
108
109impl Logger {
110 pub fn new(format: &str) -> Logger {
112 Logger(Rc::new(Inner {
113 format: Format::new(format),
114 exclude: HashSet::new(),
115 exclude_regex: RegexSet::empty(),
116 log_target: "http_logger".into(),
117 custom_level_func: None,
118 custom_resp_error_level_func: None,
119 }))
120 }
121
122 pub fn exclude<T: Into<String>>(mut self, path: T) -> Self {
124 Rc::get_mut(&mut self.0)
125 .unwrap()
126 .exclude
127 .insert(path.into());
128 self
129 }
130
131 pub fn exclude_regex<T: Into<String>>(mut self, path: T) -> Self {
133 let inner = Rc::get_mut(&mut self.0).unwrap();
134 let mut patterns = inner.exclude_regex.patterns().to_vec();
135 patterns.push(path.into());
136 let regex_set = RegexSet::new(patterns).unwrap();
137 inner.exclude_regex = regex_set;
138 self
139 }
140
141 pub fn log_target(mut self, target: impl Into<Cow<'static, str>>) -> Self {
153 let inner = Rc::get_mut(&mut self.0).unwrap();
154 inner.log_target = target.into();
155 self
156 }
157
158 pub fn custom_request_replace(
173 mut self,
174 label: &str,
175 f: impl Fn(&ServiceRequest) -> String + 'static,
176 ) -> Self {
177 let inner = Rc::get_mut(&mut self.0).unwrap();
178
179 let ft = inner.format.0.iter_mut().find(
180 |ft| matches!(ft, FormatText::CustomRequest(unit_label, _) if label == unit_label),
181 );
182
183 if let Some(FormatText::CustomRequest(_, request_fn)) = ft {
184 request_fn.replace(CustomRequestFn {
186 inner_fn: Rc::new(f),
187 });
188 } else {
189 debug!(
191 "Attempted to register custom request logging function for nonexistent label: {}",
192 label
193 );
194 }
195
196 self
197 }
198
199 pub fn custom_response_replace(
225 mut self,
226 label: &str,
227 f: impl Fn(&ServiceResponse) -> String + 'static,
228 ) -> Self {
229 let inner = Rc::get_mut(&mut self.0).unwrap();
230
231 let ft = inner.format.0.iter_mut().find(
232 |ft| matches!(ft, FormatText::CustomResponse(unit_label, _) if label == unit_label),
233 );
234
235 if let Some(FormatText::CustomResponse(_, res_fn)) = ft {
236 *res_fn = Some(CustomResponseFn {
237 inner_fn: Rc::new(f),
238 });
239 } else {
240 debug!(
241 "Attempted to register custom response logging function for non-existent label: {}",
242 label
243 );
244 }
245
246 self
247 }
248
249 pub fn custom_level(mut self, f: fn(StatusCode) -> Level) -> Self {
283 let inner = Rc::get_mut(&mut self.0).unwrap();
284 inner.custom_level_func = Some(f);
285 self
286 }
287 pub fn custom_error_resp_level(mut self, f: fn(StatusCode) -> Level) -> Self {
321 let inner = Rc::get_mut(&mut self.0).unwrap();
322 inner.custom_resp_error_level_func = Some(f);
323 self
324 }
325}
326
327impl Default for Logger {
328 fn default() -> Logger {
334 Logger(Rc::new(Inner {
335 format: Format::default(),
336 exclude: HashSet::new(),
337 exclude_regex: RegexSet::empty(),
338 log_target: "http_logger".into(),
339 custom_level_func: None,
340 custom_resp_error_level_func: None,
341 }))
342 }
343}
344
345impl<S, B> Transform<S, ServiceRequest> for Logger
346where
347 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
348 B: MessageBody,
349{
350 type Response = ServiceResponse<StreamLog<B>>;
351 type Error = Error;
352 type Transform = LoggerMiddleware<S>;
353 type InitError = ();
354 type Future = Ready<Result<Self::Transform, Self::InitError>>;
355
356 fn new_transform(&self, service: S) -> Self::Future {
357 for unit in &self.0.format.0 {
358 if let FormatText::CustomRequest(label, None) = unit {
359 warn!(
360 "No custom request replacement function was registered for label: {}",
361 label
362 );
363 }
364
365 if let FormatText::CustomResponse(label, None) = unit {
366 warn!(
367 "No custom response replacement function was registered for label: {}",
368 label
369 );
370 }
371 }
372
373 ready(Ok(LoggerMiddleware {
374 service,
375 inner: self.0.clone(),
376 }))
377 }
378}
379
380pub struct LoggerMiddleware<S> {
382 inner: Rc<Inner>,
383 service: S,
384}
385
386impl<S, B> Service<ServiceRequest> for LoggerMiddleware<S>
387where
388 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
389 B: MessageBody,
390{
391 type Response = ServiceResponse<StreamLog<B>>;
392 type Error = Error;
393 type Future = LoggerResponse<S, B>;
394
395 actix_service::forward_ready!(service);
396
397 fn call(&self, req: ServiceRequest) -> Self::Future {
398 let excluded = self.inner.exclude.contains(req.path())
399 || self.inner.exclude_regex.is_match(req.path());
400
401 if excluded {
402 LoggerResponse {
403 fut: self.service.call(req),
404 format: None,
405 time: OffsetDateTime::now_utc(),
406 log_target: Cow::Borrowed(""),
407 _phantom: PhantomData,
408 custom_level_func: self.inner.custom_level_func,
409 custom_resp_error_level_func: self.inner.custom_resp_error_level_func,
410 }
411 } else {
412 let now = OffsetDateTime::now_utc();
413 let mut format = self.inner.format.clone();
414
415 for unit in &mut format.0 {
416 unit.render_request(now, &req);
417 }
418
419 LoggerResponse {
420 fut: self.service.call(req),
421 format: Some(format),
422 time: now,
423 log_target: self.inner.log_target.clone(),
424 _phantom: PhantomData,
425 custom_level_func: self.inner.custom_level_func,
426 custom_resp_error_level_func: self.inner.custom_resp_error_level_func,
427 }
428 }
429 }
430}
431
432pin_project! {
433 pub struct LoggerResponse<S, B>
434 where
435 B: MessageBody,
436 S: Service<ServiceRequest>,
437 {
438 #[pin]
439 fut: S::Future,
440 time: OffsetDateTime,
441 format: Option<Format>,
442 log_target: Cow<'static, str>,
443 _phantom: PhantomData<B>,
444 custom_level_func: Option<fn(StatusCode) -> Level>,
445 custom_resp_error_level_func: Option<fn(StatusCode) -> Level>,
446 }
447}
448
449impl<S, B> Future for LoggerResponse<S, B>
450where
451 B: MessageBody,
452 S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
453{
454 type Output = Result<ServiceResponse<StreamLog<B>>, Error>;
455
456 fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
457 let this = self.project();
458
459 let res = match ready!(this.fut.poll(cx)) {
460 Ok(res) => res,
461 Err(e) => return Poll::Ready(Err(e)),
462 };
463
464 let response = res.response();
465 let status: StatusCode = response.status();
466 let level = match this.custom_resp_error_level_func {
467 Some(f) => f(status),
468 None => {
469 if status.is_server_error() {
470 Level::Error
471 } else {
472 Level::Debug
473 }
474 },
475 };
476 if let Some(error) = response.error() {
477 log::log!(
478 target: this.log_target,
479 level,
480 "Error in \"{}\" response: {:?}", status, error
481 );
482 }
483
484 let res = if let Some(ref mut format) = this.format {
485 let (req, res) = res.into_parts();
489 let (res, body) = res.into_parts();
490
491 let temp_res = ServiceResponse::new(req, res.map_into_boxed_body());
492
493 for unit in &mut format.0 {
494 unit.render_response(&temp_res);
495 }
496
497 let (req, res) = temp_res.into_parts();
499 ServiceResponse::new(req, res.set_body(body))
500 } else {
501 res
502 };
503
504 let time = *this.time;
505 let format = this.format.take();
506 let log_target = this.log_target.clone();
507 let status = res.status();
508 let level = match this.custom_level_func {
509 Some(f) => f(status),
510 None => {
511 if status.is_server_error() {
512 Level::Error
513 } else {
514 Level::Info
515 }
516 }
517 };
518
519 Poll::Ready(Ok(res.map_body(move |_, body| StreamLog {
520 body,
521 time,
522 format,
523 size: 0,
524 log_target,
525 level,
526 })))
527 }
528}
529
530pin_project! {
531 pub struct StreamLog<B> {
532 #[pin]
533 body: B,
534 format: Option<Format>,
535 size: usize,
536 time: OffsetDateTime,
537 log_target: Cow<'static, str>,
538 level: Level,
539 }
540
541 impl<B> PinnedDrop for StreamLog<B> {
542 fn drop(this: Pin<&mut Self>) {
543 if let Some(ref format) = this.format {
544 let render = |fmt: &mut fmt::Formatter<'_>| {
545 for unit in &format.0 {
546 unit.render(fmt, this.size, this.time)?;
547 }
548 Ok(())
549 };
550
551 log::log!(
552 target: this.log_target.as_ref(),
553 this.level,
554 "{}", FormatDisplay(&render)
555 );
556 }
557 }
558 }
559}
560
561impl<B: MessageBody> MessageBody for StreamLog<B> {
562 type Error = B::Error;
563
564 #[inline]
565 fn size(&self) -> BodySize {
566 self.body.size()
567 }
568
569 fn poll_next(
570 self: Pin<&mut Self>,
571 cx: &mut Context<'_>,
572 ) -> Poll<Option<Result<Bytes, Self::Error>>> {
573 let this = self.project();
574
575 match ready!(this.body.poll_next(cx)) {
576 Some(Ok(chunk)) => {
577 *this.size += chunk.len();
578 Poll::Ready(Some(Ok(chunk)))
579 }
580 Some(Err(err)) => Poll::Ready(Some(Err(err))),
581 None => Poll::Ready(None),
582 }
583 }
584}
585
586#[derive(Debug, Clone)]
588struct Format(Vec<FormatText>);
589
590impl Default for Format {
591 fn default() -> Format {
593 Format::new(r#"%a "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T"#)
594 }
595}
596
597impl Format {
598 pub fn new(s: &str) -> Format {
602 log::trace!("Access log format: {}", s);
603 let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|x[io])|[%atPrUsbTD]?)").unwrap();
604
605 let mut idx = 0;
606 let mut results = Vec::new();
607 for cap in fmt.captures_iter(s) {
608 let m = cap.get(0).unwrap();
609 let pos = m.start();
610 if idx != pos {
611 results.push(FormatText::Str(s[idx..pos].to_owned()));
612 }
613 idx = m.end();
614
615 if let Some(key) = cap.get(2) {
616 results.push(match cap.get(3).unwrap().as_str() {
617 "a" => {
618 if key.as_str() == "r" {
619 FormatText::RealIpRemoteAddr
620 } else {
621 unreachable!("regex and code mismatch")
622 }
623 }
624 "i" => {
625 FormatText::RequestHeader(HeaderName::try_from(key.as_str()).unwrap())
626 }
627 "o" => {
628 FormatText::ResponseHeader(HeaderName::try_from(key.as_str()).unwrap())
629 }
630 "e" => FormatText::EnvironHeader(key.as_str().to_owned()),
631 "xi" => FormatText::CustomRequest(key.as_str().to_owned(), None),
632 "xo" => FormatText::CustomResponse(key.as_str().to_owned(), None),
633 _ => unreachable!(),
634 })
635 } else {
636 let m = cap.get(1).unwrap();
637 results.push(match m.as_str() {
638 "%" => FormatText::Percent,
639 "a" => FormatText::RemoteAddr,
640 "t" => FormatText::RequestTime,
641 "r" => FormatText::RequestLine,
642 "s" => FormatText::ResponseStatus,
643 "b" => FormatText::ResponseSize,
644 "U" => FormatText::UrlPath,
645 "T" => FormatText::Time,
646 "D" => FormatText::TimeMillis,
647 _ => FormatText::Str(m.as_str().to_owned()),
648 });
649 }
650 }
651 if idx != s.len() {
652 results.push(FormatText::Str(s[idx..].to_owned()));
653 }
654
655 Format(results)
656 }
657}
658
659#[non_exhaustive]
663#[derive(Debug, Clone)]
664enum FormatText {
665 Str(String),
666 Percent,
667 RequestLine,
668 RequestTime,
669 ResponseStatus,
670 ResponseSize,
671 Time,
672 TimeMillis,
673 RemoteAddr,
674 RealIpRemoteAddr,
675 UrlPath,
676 RequestHeader(HeaderName),
677 ResponseHeader(HeaderName),
678 EnvironHeader(String),
679 CustomRequest(String, Option<CustomRequestFn>),
680 CustomResponse(String, Option<CustomResponseFn>),
681}
682
683#[derive(Clone)]
684struct CustomRequestFn {
685 inner_fn: Rc<dyn Fn(&ServiceRequest) -> String>,
686}
687
688impl CustomRequestFn {
689 fn call(&self, req: &ServiceRequest) -> String {
690 (self.inner_fn)(req)
691 }
692}
693
694impl fmt::Debug for CustomRequestFn {
695 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
696 f.write_str("custom_request_fn")
697 }
698}
699
700#[derive(Clone)]
701struct CustomResponseFn {
702 inner_fn: Rc<dyn Fn(&ServiceResponse) -> String>,
703}
704
705impl CustomResponseFn {
706 fn call(&self, res: &ServiceResponse) -> String {
707 (self.inner_fn)(res)
708 }
709}
710
711impl fmt::Debug for CustomResponseFn {
712 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
713 f.write_str("custom_response_fn")
714 }
715}
716
717impl FormatText {
718 fn render(
719 &self,
720 fmt: &mut fmt::Formatter<'_>,
721 size: usize,
722 entry_time: OffsetDateTime,
723 ) -> Result<(), fmt::Error> {
724 match self {
725 FormatText::Str(ref string) => fmt.write_str(string),
726 FormatText::Percent => "%".fmt(fmt),
727 FormatText::ResponseSize => size.fmt(fmt),
728 FormatText::Time => {
729 let rt = OffsetDateTime::now_utc() - entry_time;
730 let rt = rt.as_seconds_f64();
731 fmt.write_fmt(format_args!("{:.6}", rt))
732 }
733 FormatText::TimeMillis => {
734 let rt = OffsetDateTime::now_utc() - entry_time;
735 let rt = (rt.whole_nanoseconds() as f64) / 1_000_000.0;
736 fmt.write_fmt(format_args!("{:.6}", rt))
737 }
738 FormatText::EnvironHeader(ref name) => {
739 if let Ok(val) = env::var(name) {
740 fmt.write_fmt(format_args!("{}", val))
741 } else {
742 "-".fmt(fmt)
743 }
744 }
745 _ => Ok(()),
746 }
747 }
748
749 fn render_response(&mut self, res: &ServiceResponse) {
750 match self {
751 FormatText::ResponseStatus => {
752 *self = FormatText::Str(format!("{}", res.status().as_u16()))
753 }
754
755 FormatText::ResponseHeader(ref name) => {
756 let s = if let Some(val) = res.headers().get(name) {
757 if let Ok(s) = val.to_str() {
758 s
759 } else {
760 "-"
761 }
762 } else {
763 "-"
764 };
765 *self = FormatText::Str(s.to_string())
766 }
767
768 FormatText::CustomResponse(_, res_fn) => {
769 let text = match res_fn {
770 Some(res_fn) => FormatText::Str(res_fn.call(res)),
771 None => FormatText::Str("-".to_owned()),
772 };
773
774 *self = text;
775 }
776
777 _ => {}
778 }
779 }
780
781 fn render_request(&mut self, now: OffsetDateTime, req: &ServiceRequest) {
782 match self {
783 FormatText::RequestLine => {
784 *self = if req.query_string().is_empty() {
785 FormatText::Str(format!(
786 "{} {} {:?}",
787 req.method(),
788 req.path(),
789 req.version()
790 ))
791 } else {
792 FormatText::Str(format!(
793 "{} {}?{} {:?}",
794 req.method(),
795 req.path(),
796 req.query_string(),
797 req.version()
798 ))
799 };
800 }
801 FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()),
802 FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()),
803 FormatText::RequestHeader(ref name) => {
804 let s = if let Some(val) = req.headers().get(name) {
805 if let Ok(s) = val.to_str() {
806 s
807 } else {
808 "-"
809 }
810 } else {
811 "-"
812 };
813 *self = FormatText::Str(s.to_string());
814 }
815 FormatText::RemoteAddr => {
816 let s = if let Some(peer) = req.connection_info().peer_addr() {
817 FormatText::Str((*peer).to_string())
818 } else {
819 FormatText::Str("-".to_string())
820 };
821 *self = s;
822 }
823 FormatText::RealIpRemoteAddr => {
824 let s = if let Some(remote) = req.connection_info().realip_remote_addr() {
825 FormatText::Str(remote.to_string())
826 } else {
827 FormatText::Str("-".to_string())
828 };
829 *self = s;
830 }
831 FormatText::CustomRequest(_, request_fn) => {
832 let s = match request_fn {
833 Some(f) => FormatText::Str(f.call(req)),
834 None => FormatText::Str("-".to_owned()),
835 };
836
837 *self = s;
838 }
839 _ => {}
840 }
841 }
842}
843
844pub(crate) struct FormatDisplay<'a>(
846 &'a dyn Fn(&mut fmt::Formatter<'_>) -> Result<(), fmt::Error>,
847);
848
849impl<'a> fmt::Display for FormatDisplay<'a> {
850 fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
851 (self.0)(fmt)
852 }
853}
854
855#[cfg(test)]
856mod tests {
857 use actix_service::{IntoService, Service, Transform};
858 use actix_utils::future::ok;
859
860 use super::*;
861 use actix_web::{
862 http::{header, StatusCode},
863 test::{self, TestRequest},
864 HttpResponse,
865 };
866
867 #[actix_rt::test]
868 async fn test_logger() {
869 let srv = |req: ServiceRequest| {
870 ok(req.into_response(
871 HttpResponse::build(StatusCode::OK)
872 .insert_header(("X-Test", "ttt"))
873 .finish(),
874 ))
875 };
876 let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test");
877
878 let srv = logger.new_transform(srv.into_service()).await.unwrap();
879
880 let req = TestRequest::default()
881 .insert_header((
882 header::USER_AGENT,
883 header::HeaderValue::from_static("ACTIX-WEB"),
884 ))
885 .to_srv_request();
886 let _res = srv.call(req).await;
887 }
888
889 #[actix_rt::test]
890 async fn test_logger_exclude_regex() {
891 let srv = |req: ServiceRequest| {
892 ok(req.into_response(
893 HttpResponse::build(StatusCode::OK)
894 .insert_header(("X-Test", "ttt"))
895 .finish(),
896 ))
897 };
898 let logger =
899 Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test").exclude_regex("\\w");
900
901 let srv = logger.new_transform(srv.into_service()).await.unwrap();
902
903 let req = TestRequest::default()
904 .insert_header((
905 header::USER_AGENT,
906 header::HeaderValue::from_static("ACTIX-WEB"),
907 ))
908 .to_srv_request();
909 let _res = srv.call(req).await.unwrap();
910 }
911
912 #[actix_rt::test]
913 async fn test_escape_percent() {
914 let mut format = Format::new("%%{r}a");
915
916 let req = TestRequest::default()
917 .insert_header((
918 header::FORWARDED,
919 header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"),
920 ))
921 .to_srv_request();
922
923 let now = OffsetDateTime::now_utc();
924 for unit in &mut format.0 {
925 unit.render_request(now, &req);
926 }
927
928 let req = TestRequest::default().to_http_request();
929 let res = ServiceResponse::new(req, HttpResponse::Ok().finish());
930 for unit in &mut format.0 {
931 unit.render_response(&res);
932 }
933
934 let entry_time = OffsetDateTime::now_utc();
935 let render = |fmt: &mut fmt::Formatter<'_>| {
936 for unit in &format.0 {
937 unit.render(fmt, 1024, entry_time)?;
938 }
939 Ok(())
940 };
941 let s = format!("{}", FormatDisplay(&render));
942 assert_eq!(s, "%{r}a");
943 }
944
945 #[actix_rt::test]
946 async fn test_url_path() {
947 let mut format = Format::new("%T %U");
948 let req = TestRequest::default()
949 .insert_header((
950 header::USER_AGENT,
951 header::HeaderValue::from_static("ACTIX-WEB"),
952 ))
953 .uri("/test/route/yeah")
954 .to_srv_request();
955
956 let now = OffsetDateTime::now_utc();
957 for unit in &mut format.0 {
958 unit.render_request(now, &req);
959 }
960
961 let req = TestRequest::default().to_http_request();
962 let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish());
963 for unit in &mut format.0 {
964 unit.render_response(&res);
965 }
966
967 let render = |fmt: &mut fmt::Formatter<'_>| {
968 for unit in &format.0 {
969 unit.render(fmt, 1024, now)?;
970 }
971 Ok(())
972 };
973 let s = format!("{}", FormatDisplay(&render));
974 assert!(s.contains("/test/route/yeah"));
975 }
976
977 #[actix_rt::test]
978 async fn test_default_format() {
979 let mut format = Format::default();
980
981 let req = TestRequest::default()
982 .insert_header((
983 header::USER_AGENT,
984 header::HeaderValue::from_static("ACTIX-WEB"),
985 ))
986 .peer_addr("127.0.0.1:8081".parse().unwrap())
987 .to_srv_request();
988
989 let now = OffsetDateTime::now_utc();
990 for unit in &mut format.0 {
991 unit.render_request(now, &req);
992 }
993
994 let req = TestRequest::default().to_http_request();
995 let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish());
996 for unit in &mut format.0 {
997 unit.render_response(&res);
998 }
999
1000 let entry_time = OffsetDateTime::now_utc();
1001 let render = |fmt: &mut fmt::Formatter<'_>| {
1002 for unit in &format.0 {
1003 unit.render(fmt, 1024, entry_time)?;
1004 }
1005 Ok(())
1006 };
1007 let s = format!("{}", FormatDisplay(&render));
1008 assert!(s.contains("GET / HTTP/1.1"));
1009 assert!(s.contains("127.0.0.1"));
1010 assert!(s.contains("200 1024"));
1011 assert!(s.contains("ACTIX-WEB"));
1012 }
1013
1014 #[actix_rt::test]
1015 async fn test_request_time_format() {
1016 let mut format = Format::new("%t");
1017 let req = TestRequest::default().to_srv_request();
1018
1019 let now = OffsetDateTime::now_utc();
1020 for unit in &mut format.0 {
1021 unit.render_request(now, &req);
1022 }
1023
1024 let req = TestRequest::default().to_http_request();
1025 let res = ServiceResponse::new(req, HttpResponse::Ok().force_close().finish());
1026 for unit in &mut format.0 {
1027 unit.render_response(&res);
1028 }
1029
1030 let render = |fmt: &mut fmt::Formatter<'_>| {
1031 for unit in &format.0 {
1032 unit.render(fmt, 1024, now)?;
1033 }
1034 Ok(())
1035 };
1036 let s = format!("{}", FormatDisplay(&render));
1037 assert!(s.contains(&now.format(&Rfc3339).unwrap()));
1038 }
1039
1040 #[actix_rt::test]
1041 async fn test_remote_addr_format() {
1042 let mut format = Format::new("%{r}a");
1043
1044 let req = TestRequest::default()
1045 .insert_header((
1046 header::FORWARDED,
1047 header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"),
1048 ))
1049 .to_srv_request();
1050
1051 let now = OffsetDateTime::now_utc();
1052 for unit in &mut format.0 {
1053 unit.render_request(now, &req);
1054 }
1055
1056 let req = TestRequest::default().to_http_request();
1057 let res = ServiceResponse::new(req, HttpResponse::Ok().finish());
1058 for unit in &mut format.0 {
1059 unit.render_response(&res);
1060 }
1061
1062 let entry_time = OffsetDateTime::now_utc();
1063 let render = |fmt: &mut fmt::Formatter<'_>| {
1064 for unit in &format.0 {
1065 unit.render(fmt, 1024, entry_time)?;
1066 }
1067 Ok(())
1068 };
1069 let s = format!("{}", FormatDisplay(&render));
1070 assert!(s.contains("192.0.2.60"));
1071 }
1072
1073 #[actix_rt::test]
1074 async fn test_custom_closure_req_log() {
1075 let mut logger = Logger::new("test %{CUSTOM}xi")
1076 .custom_request_replace("CUSTOM", |_req: &ServiceRequest| -> String {
1077 String::from("custom_log")
1078 });
1079 let mut unit = Rc::get_mut(&mut logger.0).unwrap().format.0[1].clone();
1080
1081 let label = match &unit {
1082 FormatText::CustomRequest(label, _) => label,
1083 ft => panic!("expected CustomRequest, found {:?}", ft),
1084 };
1085
1086 assert_eq!(label, "CUSTOM");
1087
1088 let req = TestRequest::default().to_srv_request();
1089 let now = OffsetDateTime::now_utc();
1090
1091 unit.render_request(now, &req);
1092
1093 let render = |fmt: &mut fmt::Formatter<'_>| unit.render(fmt, 1024, now);
1094
1095 let log_output = FormatDisplay(&render).to_string();
1096 assert_eq!(log_output, "custom_log");
1097 }
1098
1099 #[actix_rt::test]
1100 async fn test_custom_closure_response_log() {
1101 let mut logger = Logger::new("test %{CUSTOM}xo").custom_response_replace(
1102 "CUSTOM",
1103 |res: &ServiceResponse| -> String {
1104 if res.status().as_u16() == 200 {
1105 String::from("custom_log")
1106 } else {
1107 String::from("-")
1108 }
1109 },
1110 );
1111 let mut unit = Rc::get_mut(&mut logger.0).unwrap().format.0[1].clone();
1112
1113 let label = match &unit {
1114 FormatText::CustomResponse(label, _) => label,
1115 ft => panic!("expected CustomResponse, found {:?}", ft),
1116 };
1117
1118 assert_eq!(label, "CUSTOM");
1119
1120 let req = TestRequest::default().to_http_request();
1121 let resp_ok = ServiceResponse::new(req, HttpResponse::Ok().finish());
1122 let now = OffsetDateTime::now_utc();
1123 unit.render_response(&resp_ok);
1124
1125 let render = |fmt: &mut fmt::Formatter<'_>| unit.render(fmt, 1024, now);
1126
1127 let log_output = FormatDisplay(&render).to_string();
1128 assert_eq!(log_output, "custom_log");
1129 }
1130
1131 #[actix_rt::test]
1132 async fn test_closure_logger_in_middleware() {
1133 let captured = "custom log replacement";
1134
1135 let logger = Logger::new("%{CUSTOM}xi")
1136 .custom_request_replace("CUSTOM", move |_req: &ServiceRequest| -> String {
1137 captured.to_owned()
1138 });
1139
1140 let srv = logger.new_transform(test::ok_service()).await.unwrap();
1141
1142 let req = TestRequest::default().to_srv_request();
1143 srv.call(req).await.unwrap();
1144 }
1145}