actix_web_middleware_slogger/
logger.rs

1use std::{
2    borrow::Cow,
3    collections::HashSet,
4    env,
5    future::Future,
6    marker::PhantomData,
7    pin::Pin,
8    rc::Rc,
9    task::{Context, Poll},
10};
11
12use bytes::Bytes;
13use futures_core::ready;
14use pin_project_lite::pin_project;
15use regex::Regex;
16use time::{OffsetDateTime, format_description::well_known::Rfc3339};
17use uuid::Uuid;
18
19use actix_service::{Service, Transform};
20use actix_utils::future::{Ready, ready};
21use actix_web::HttpMessage;
22use actix_web::body::{BodySize, MessageBody};
23use actix_web::dev::{ServiceRequest, ServiceResponse};
24use actix_web::http::header::HeaderName;
25use actix_web::{Error, Result};
26
27/// Middleware for logging requests and responses summaries using slog.
28///
29/// This middleware uses the `slog` crate to output information.
30///
31/// # Default Format
32/// The [`default`](SLogger::default)
33///
34/// # Examples
35/// ```rust
36/// use actix_web::App;
37/// use actix_web_middleware_slogger::SLogger;
38///
39/// let app = App::new()
40///     .wrap(SLogger::default());
41/// ```
42pub struct SLogger(Rc<Inner>);
43
44#[derive(Debug, Clone)]
45struct Inner {
46    fields: ListFields,
47    exclude: HashSet<String>,
48    exclude_regex: Vec<Regex>,
49    log_target: Cow<'static, str>,
50}
51
52impl SLogger {
53    /// Create `SLogger` middleware with the specified `fields`.
54    pub fn new(fields: Fields) -> SLogger {
55        SLogger(Rc::new(Inner {
56            fields: fields.into(),
57            exclude: HashSet::new(),
58            exclude_regex: Vec::new(),
59            log_target: Cow::Borrowed(module_path!()),
60        }))
61    }
62
63    /// Ignore and do not log access info for specified path.
64    pub fn exclude<T: Into<String>>(mut self, path: T) -> Self {
65        Rc::get_mut(&mut self.0)
66            .unwrap()
67            .exclude
68            .insert(path.into());
69        self
70    }
71
72    /// Ignore and do not log access info for paths that match regex.
73    pub fn exclude_regex<T: Into<String>>(mut self, path: T) -> Self {
74        let inner = Rc::get_mut(&mut self.0).unwrap();
75        inner.exclude_regex.push(Regex::new(&path.into()).unwrap());
76        self
77    }
78
79    /// Sets the logging target to `target`.
80    ///
81    /// By default, the log target is `module_path!()` of the log call location. In our case, that
82    /// would be `actix_web_middleware_slogger::logger`.
83    ///
84    /// # Examples
85    /// Using `.log_target("http_slog")` would have this effect on request logs:
86    /// ```diff
87    /// - [2015-10-21T07:28:00Z INFO  actix_web_middleware_slogger::logger] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
88    /// + [2015-10-21T07:28:00Z INFO  http_slog] 127.0.0.1 "GET / HTTP/1.1" 200 88 "-" "dmc/1.0" 0.001985
89    ///                               ^^^^^^^^^
90    /// ```
91    pub fn log_target(mut self, target: impl Into<Cow<'static, str>>) -> Self {
92        let inner = Rc::get_mut(&mut self.0).unwrap();
93        inner.log_target = target.into();
94        self
95    }
96}
97
98impl Default for SLogger {
99    /// Create `SLogger` middleware with format:
100    ///
101    /// Fields:
102    /// - Method
103    /// - Status
104    /// - Path
105    /// - Params
106    /// - Host
107    /// - RemoteAddr
108    /// - Size
109    /// - Duration
110    /// - DateTime
111    /// - UserAgent
112    /// - Referer
113    fn default() -> Self {
114        SLogger(Rc::new(Inner {
115            fields: Fields::default().into(),
116            exclude: HashSet::new(),
117            exclude_regex: Vec::new(),
118            log_target: "actix_web_middleware_slogger::logger".into(),
119        }))
120    }
121}
122
123impl<S, B> Transform<S, ServiceRequest> for SLogger
124where
125    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
126    B: MessageBody,
127{
128    type Response = ServiceResponse<StreamLog<B>>;
129    type Error = Error;
130    type Transform = SLoggerMiddlewareService<S>;
131    type InitError = ();
132    type Future = Ready<Result<Self::Transform, Self::InitError>>;
133
134    fn new_transform(&self, service: S) -> Self::Future {
135        ready(Ok(SLoggerMiddlewareService {
136            service,
137            inner: Rc::clone(&self.0),
138        }))
139    }
140}
141
142pin_project! {
143    pub struct StreamLog<B> {
144        #[pin]
145        body: B,
146        fields: Option<ListFields>,
147        size: usize,
148        time: OffsetDateTime,
149        log_target: Cow<'static, str>,
150    }
151
152    impl<B> PinnedDrop for StreamLog<B> {
153        fn drop(this: Pin<&mut Self>) {
154            let this = this.project();
155            if let Some(fields) = this.fields {
156                for unit in &mut fields.0 {
157                    unit.render(*this.size, *this.time)
158                }
159
160                #[cfg(feature = "log")]
161                crate::wrapper::rust_log::log(
162                    log::Level::Info,
163                    this.log_target.as_ref(),
164                    module_path!(),
165                    std::panic::Location::caller(),
166                    fields.0.clone(),
167                );
168            }
169        }
170    }
171}
172
173impl<B: MessageBody> MessageBody for StreamLog<B> {
174    type Error = B::Error;
175
176    #[inline]
177    fn size(&self) -> BodySize {
178        self.body.size()
179    }
180
181    fn poll_next(
182        self: Pin<&mut Self>,
183        cx: &mut Context<'_>,
184    ) -> Poll<Option<Result<Bytes, Self::Error>>> {
185        let this = self.project();
186
187        match ready!(this.body.poll_next(cx)) {
188            Some(Ok(chunk)) => {
189                *this.size += chunk.len();
190                Poll::Ready(Some(Ok(chunk)))
191            }
192            Some(Err(err)) => Poll::Ready(Some(Err(err))),
193            None => Poll::Ready(None),
194        }
195    }
196}
197
198/// Logger middleware service.
199pub struct SLoggerMiddlewareService<S> {
200    inner: Rc<Inner>,
201    service: S,
202}
203
204impl<S, B> Service<ServiceRequest> for SLoggerMiddlewareService<S>
205where
206    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
207    B: MessageBody,
208{
209    type Response = ServiceResponse<StreamLog<B>>;
210    type Error = Error;
211    type Future = SLoggerResponse<S, B>;
212
213    actix_service::forward_ready!(service);
214
215    fn call(&self, req: ServiceRequest) -> Self::Future {
216        let excluded = self.inner.exclude.contains(req.path())
217            || self
218                .inner
219                .exclude_regex
220                .iter()
221                .any(|r| r.is_match(req.path()));
222
223        if excluded {
224            SLoggerResponse {
225                fut: self.service.call(req),
226                fields: None,
227                time: OffsetDateTime::now_utc(),
228                log_target: Cow::Borrowed(""),
229                _phantom: PhantomData,
230            }
231        } else {
232            let now = OffsetDateTime::now_utc();
233            let mut fields = self.inner.fields.clone();
234
235            for unit in &mut fields.0 {
236                unit.render_request(now, &req);
237            }
238
239            SLoggerResponse {
240                fut: self.service.call(req),
241                fields: Some(fields),
242                time: now,
243                log_target: self.inner.log_target.clone(),
244                _phantom: PhantomData,
245            }
246        }
247    }
248}
249
250pin_project! {
251    pub struct SLoggerResponse<S, B>
252    where
253        B: MessageBody,
254        S: Service<ServiceRequest>,
255    {
256        #[pin]
257        fut: S::Future,
258        time: OffsetDateTime,
259        fields: Option<ListFields>,
260        log_target: Cow<'static, str>,
261        _phantom: PhantomData<B>,
262    }
263}
264
265impl<S, B> Future for SLoggerResponse<S, B>
266where
267    B: MessageBody,
268    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
269{
270    type Output = Result<ServiceResponse<StreamLog<B>>, Error>;
271
272    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
273        let this = self.project();
274
275        let res = match ready!(this.fut.poll(cx)) {
276            Ok(res) => res,
277            Err(err) => return Poll::Ready(Err(err)),
278        };
279
280        if let Some(error) = res.response().error() {
281            log::debug!("Error in response: {:?}", error);
282        }
283
284        let res = if let Some(fields) = this.fields {
285            // to avoid polluting all the Logger types with the body parameter we swap the body
286            // out temporarily since it's not usable in custom response functions anyway
287
288            let (req, res) = res.into_parts();
289            let (res, body) = res.into_parts();
290
291            let temp_res = ServiceResponse::new(req, res.map_into_boxed_body());
292
293            for unit in &mut fields.0 {
294                unit.render_response(&temp_res);
295            }
296
297            // re-construct original service response
298            let (req, res) = temp_res.into_parts();
299            ServiceResponse::new(req, res.set_body(body))
300        } else {
301            res
302        };
303
304        let time = *this.time;
305        let fields = this.fields.take();
306        let log_target = this.log_target.clone();
307
308        Poll::Ready(Ok(res.map_body(move |_, body| StreamLog {
309            body,
310            time,
311            fields,
312            size: 0,
313            log_target,
314        })))
315    }
316}
317
318#[derive(Debug, Clone)]
319struct ListFields(Vec<Field>);
320
321impl From<Fields> for ListFields {
322    fn from(fields: Fields) -> Self {
323        ListFields(fields.0.into_iter().collect())
324    }
325}
326
327#[derive(Debug, Clone)]
328pub struct Fields(HashSet<Field>);
329
330impl Default for Fields {
331    fn default() -> Self {
332        FieldsBuilder::default().build()
333    }
334}
335
336impl Fields {
337    pub fn builder() -> FieldsBuilder {
338        FieldsBuilder::new()
339    }
340
341    pub fn new(fields: HashSet<Field>) -> Self {
342        Fields(fields)
343    }
344}
345
346pub struct FieldsBuilder {
347    fields: HashSet<Field>,
348}
349
350impl FieldsBuilder {
351    pub fn new() -> Self {
352        FieldsBuilder {
353            fields: HashSet::new(),
354        }
355    }
356
357    pub fn build(self) -> Fields {
358        Fields(self.fields)
359    }
360
361    pub fn with_method(mut self) -> Self {
362        self.fields.insert(Field::Method);
363        self
364    }
365
366    pub fn with_status(mut self) -> Self {
367        self.fields.insert(Field::Status);
368        self
369    }
370
371    pub fn with_path(mut self) -> Self {
372        self.fields.insert(Field::Path);
373        self
374    }
375
376    pub fn with_params(mut self) -> Self {
377        self.fields.insert(Field::Params);
378        self
379    }
380
381    pub fn with_version(mut self) -> Self {
382        self.fields.insert(Field::Version);
383        self
384    }
385
386    pub fn with_host(mut self) -> Self {
387        self.fields.insert(Field::Host);
388        self
389    }
390
391    pub fn with_remote_addr(mut self) -> Self {
392        self.fields.insert(Field::RemoteAddr);
393        self
394    }
395
396    pub fn with_real_ip(mut self) -> Self {
397        self.fields.insert(Field::RealIp);
398        self
399    }
400
401    pub fn with_request_id(mut self, header: &str) -> Self {
402        self.fields
403            .insert(Field::RequestId(HeaderName::try_from(header).unwrap()));
404        self
405    }
406
407    #[cfg(feature = "tracing-request-id")]
408    pub fn with_tracing_request_id(mut self) -> Self {
409        self.fields.insert(Field::TracingRequestId);
410        self
411    }
412
413    pub fn with_request_header(mut self, header: &str) -> Self {
414        self.fields
415            .insert(Field::RequestHeader(HeaderName::try_from(header).unwrap()));
416        self
417    }
418
419    pub fn with_response_header(mut self, header: &str) -> Self {
420        self.fields
421            .insert(Field::ResponseHeader(HeaderName::try_from(header).unwrap()));
422        self
423    }
424
425    pub fn with_size(mut self) -> Self {
426        self.fields.insert(Field::Size);
427        self
428    }
429
430    pub fn with_duration(mut self) -> Self {
431        self.fields.insert(Field::Duration);
432        self
433    }
434
435    pub fn with_duration_millis(mut self) -> Self {
436        self.fields.insert(Field::DurationMillis);
437        self
438    }
439
440    pub fn with_date_time(mut self) -> Self {
441        self.fields.insert(Field::RequestTime);
442        self
443    }
444
445    pub fn with_user_agent(mut self) -> Self {
446        self.fields.insert(Field::UserAgent);
447        self
448    }
449
450    pub fn with_referer(mut self) -> Self {
451        self.fields.insert(Field::Referer);
452        self
453    }
454
455    pub fn with_environment(mut self, var: &str) -> Self {
456        self.fields.insert(Field::Environment(var.to_string()));
457        self
458    }
459}
460
461impl Default for FieldsBuilder {
462    fn default() -> Self {
463        FieldsBuilder::new()
464            .with_method()
465            .with_status()
466            .with_path()
467            .with_params()
468            .with_version()
469            .with_host()
470            .with_remote_addr()
471            .with_real_ip()
472            .with_size()
473            .with_duration()
474            .with_date_time()
475            .with_user_agent()
476            .with_referer()
477    }
478}
479
480#[derive(Debug, Clone, PartialEq, Eq, Hash)]
481pub enum Field {
482    /// Key, Value
483    /// Used during result saving
484    KV(String, Option<String>),
485    /// Method. Example: GET
486    Method,
487    /// Status code. Example: 200, 404
488    Status,
489    /// Request path. Example: /index.html
490    Path,
491    /// Query string. Example: ?search=actix
492    Params,
493    /// Version of the HTTP protocol. Example: HTTP/1.1
494    Version,
495    /// Host. Example: localhost
496    Host,
497    /// Remote IP address. Example: 192.168.0.1
498    RemoteAddr,
499    /// Real IP address. Example: 192.168.0.1
500    RealIp,
501    /// Request ID. Example: 7b77f3f1-8e15-4b6a-9b3f-7f4b6f4b6f4b.
502    /// Generated if not provided by the client.
503    /// Used provided string to get the request ID from the request.
504    RequestId(HeaderName),
505    #[cfg(feature = "tracing-request-id")]
506    /// Tracing request ID. Example: 7b77f3f1-8e15-4b6a-9b3f-7f4b6f4b6f4b.
507    TracingRequestId,
508    /// Request headers. Example: Accept: application/json
509    RequestHeader(HeaderName),
510    /// Response headers. Example: Content-Type: application/json
511    ResponseHeader(HeaderName),
512    /// Size of the response body in bytes. Example: 1024
513    Size,
514    /// Duration of the request in seconds. Example: 23
515    Duration,
516    /// Duration of the request in seconds with milliseconds. Example: 23.123
517    DurationMillis,
518    /// Timestamp in RFC3339 format. Example: 2019-05-29T18:51:00.000000Z
519    RequestTime,
520    /// User agent. Example: Mozilla/5.0
521    UserAgent,
522    /// Referer. Example: https://actix.rs
523    Referer,
524    /// Environment variable. Example: USER
525    Environment(String),
526}
527
528#[derive(Clone, Copy, Debug)]
529pub struct RequestId(Uuid);
530
531impl RequestId {
532    pub(crate) fn new() -> Self {
533        #[cfg(not(feature = "uuid_v7"))]
534        {
535            Self(Uuid::new_v4())
536        }
537        #[cfg(feature = "uuid_v7")]
538        {
539            Self(Uuid::now_v7())
540        }
541    }
542}
543
544impl Field {
545    fn render_request(&mut self, now: OffsetDateTime, req: &ServiceRequest) {
546        match self {
547            Field::Method => {
548                *self = Field::KV("method".to_string(), Some(req.method().to_string()));
549            }
550
551            Field::Version => {
552                let version = match req.version() {
553                    actix_http::Version::HTTP_09 => "HTTP/0.9",
554                    actix_http::Version::HTTP_10 => "HTTP/1.0",
555                    actix_http::Version::HTTP_11 => "HTTP/1.1",
556                    actix_http::Version::HTTP_2 => "HTTP/2.0",
557                    actix_http::Version::HTTP_3 => "HTTP/3.0",
558                    _ => "unknown",
559                };
560                *self = Field::KV("version".to_string(), Some(version.to_string()));
561            }
562
563            Field::Path => {
564                *self = Field::KV("path".to_string(), Some(req.path().to_string()));
565            }
566
567            Field::Params => {
568                *self = Field::KV("params".to_string(), Some(req.query_string().to_string()));
569            }
570
571            Field::Host => {
572                *self = Field::KV(
573                    "host".to_string(),
574                    Some(req.connection_info().host().to_string()),
575                );
576            }
577
578            Field::RemoteAddr => {
579                *self = Field::KV(
580                    "remote_addr".to_string(),
581                    req.connection_info()
582                        .peer_addr()
583                        .map(|addr| addr.to_string()),
584                );
585            }
586
587            Field::RealIp => {
588                *self = Field::KV(
589                    "real_ip".to_string(),
590                    req.connection_info()
591                        .realip_remote_addr()
592                        .map(|addr| addr.to_string()),
593                );
594            }
595
596            &mut Field::RequestId(ref header) => match req.headers().get(header) {
597                Some(val) => {
598                    *self = Field::KV(
599                        header.to_string(),
600                        Some(val.to_str().unwrap_or_default().to_string()),
601                    );
602                }
603                None => {
604                    let id = RequestId::new();
605                    req.extensions_mut().insert(id);
606                    *self = Field::KV(header.to_string(), Some(id.0.as_hyphenated().to_string()));
607                }
608            },
609
610            #[cfg(feature = "tracing-request-id")]
611            Field::TracingRequestId => {
612                let ext = req.extensions();
613                match ext.get::<tracing_actix_web::RequestId>() {
614                    Some(id) => {
615                        *self = Field::KV("tracing_request_id".to_string(), Some(id.to_string()));
616                    }
617                    None => {
618                        *self = Field::KV("tracing_request_id".to_string(), None);
619                    }
620                }
621            }
622
623            &mut Field::RequestHeader(ref header) => {
624                *self = match req.headers().get(header) {
625                    Some(val) => Field::KV(
626                        header.to_string(),
627                        Some(val.to_str().unwrap_or_default().to_string()),
628                    ),
629                    None => Field::KV(header.to_string(), None),
630                };
631            }
632
633            Field::RequestTime => {
634                *self = Field::KV("datetime".to_string(), Some(now.format(&Rfc3339).unwrap()));
635            }
636
637            Field::UserAgent => {
638                *self = Field::KV(
639                    "user_agent".to_string(),
640                    req.headers()
641                        .get("user-agent")
642                        .map(|v| v.to_str().unwrap_or_default().to_string()),
643                );
644            }
645
646            Field::Referer => {
647                *self = Field::KV(
648                    "referer".to_string(),
649                    req.headers()
650                        .get("referer")
651                        .map(|v| v.to_str().unwrap_or_default().to_string()),
652                );
653            }
654
655            _ => {}
656        }
657    }
658
659    pub fn render_response(&mut self, res: &ServiceResponse) {
660        match self {
661            Field::Status => {
662                *self = Field::KV("status".to_string(), Some(res.status().to_string()));
663            }
664
665            Field::ResponseHeader(header) => {
666                *self = match res.headers().get(header.as_str()) {
667                    Some(val) => Field::KV(
668                        header.to_string(),
669                        Some(val.to_str().unwrap_or_default().to_string()),
670                    ),
671                    None => Field::KV(header.to_string(), None),
672                };
673            }
674
675            _ => {}
676        }
677    }
678
679    pub fn render(&mut self, size: usize, entry_time: OffsetDateTime) {
680        match self {
681            Field::Duration => {
682                let rt = OffsetDateTime::now_utc() - entry_time;
683                let rt = rt.as_seconds_f64();
684                *self = Field::KV("duration".to_string(), Some(rt.to_string()));
685            }
686
687            Field::DurationMillis => {
688                let rt = OffsetDateTime::now_utc() - entry_time;
689                let rt = (rt.whole_nanoseconds() as f64) / 1_000_000.0;
690                *self = Field::KV("duration".to_string(), Some(rt.to_string()));
691            }
692
693            Field::Size => {
694                *self = Field::KV("size".to_string(), Some(size.to_string()));
695            }
696
697            Field::Environment(name) => {
698                if let Ok(val) = env::var(name.as_str()) {
699                    *self = Field::KV(name.to_string(), Some(val));
700                } else {
701                    *self = Field::KV(name.to_string(), None);
702                }
703            }
704
705            _ => {}
706        }
707    }
708}
709
710#[cfg(test)]
711mod tests {
712    use super::*;
713    use actix_web::{
714        HttpResponse,
715        http::{Method, StatusCode, header},
716        test::TestRequest,
717    };
718
719    #[test]
720    fn test_slogger_builder() {
721        // Test default configuration
722        let logger = SLogger::default();
723        assert_eq!(logger.0.log_target, "actix_web_middleware_slogger::logger");
724        assert!(logger.0.exclude.is_empty());
725        assert!(logger.0.exclude_regex.is_empty());
726
727        // Test custom configuration
728        let logger = SLogger::default()
729            .exclude("/health")
730            .exclude_regex("^/api/v1/.*")
731            .log_target("custom_target");
732
733        assert_eq!(logger.0.log_target, "custom_target");
734        assert!(logger.0.exclude.contains("/health"));
735        assert_eq!(logger.0.exclude_regex.len(), 1);
736        assert!(logger.0.exclude_regex[0].is_match("/api/v1/users"));
737        assert!(!logger.0.exclude_regex[0].is_match("/api/v2/users"));
738    }
739
740    #[test]
741    fn test_fields_builder() {
742        // Test default fields
743        let fields = Fields::default();
744        let field_set = &fields.0;
745
746        assert!(field_set.contains(&Field::Method));
747        assert!(field_set.contains(&Field::Status));
748        assert!(field_set.contains(&Field::Path));
749        assert!(field_set.contains(&Field::RemoteAddr));
750        assert!(field_set.contains(&Field::Duration));
751
752        // Test custom fields
753        let custom_fields = Fields::builder()
754            .with_method()
755            .with_status()
756            .with_request_header("content-type")
757            .with_response_header("x-request-id")
758            .with_environment("APP_ENV")
759            .build();
760
761        assert!(custom_fields.0.contains(&Field::Method));
762        assert!(custom_fields.0.contains(&Field::Status));
763        assert!(custom_fields.0.contains(&Field::RequestHeader(
764            HeaderName::try_from("content-type").unwrap()
765        )));
766        assert!(custom_fields.0.contains(&Field::ResponseHeader(
767            HeaderName::try_from("x-request-id").unwrap()
768        )));
769        assert!(
770            custom_fields
771                .0
772                .contains(&Field::Environment("APP_ENV".to_string()))
773        );
774        assert!(!custom_fields.0.contains(&Field::Path));
775    }
776
777    #[test]
778    fn test_field_render_request() {
779        // Create test request
780        let req = TestRequest::default()
781            .method(Method::GET)
782            .uri("/test?param=value")
783            .insert_header(("user-agent", "test-agent"))
784            .insert_header(("referer", "https://example.com"))
785            .insert_header(("x-request-id", "test-id"))
786            .to_http_request();
787
788        let service_req = ServiceRequest::from_request(req);
789
790        // Test Method field
791        let mut field = Field::Method;
792        field.render_request(OffsetDateTime::now_utc(), &service_req);
793        if let Field::KV(key, value) = field {
794            assert_eq!(key, "method");
795            assert_eq!(value, Some("GET".to_string()));
796        } else {
797            panic!("Field should be KV");
798        }
799
800        // Test Path field
801        let mut field = Field::Path;
802        field.render_request(OffsetDateTime::now_utc(), &service_req);
803        if let Field::KV(key, value) = field {
804            assert_eq!(key, "path");
805            assert_eq!(value, Some("/test".to_string()));
806        } else {
807            panic!("Field should be KV");
808        }
809
810        // Test Params field
811        let mut field = Field::Params;
812        field.render_request(OffsetDateTime::now_utc(), &service_req);
813        if let Field::KV(key, value) = field {
814            assert_eq!(key, "params");
815            assert_eq!(value, Some("param=value".to_string()));
816        } else {
817            panic!("Field should be KV");
818        }
819
820        // Test UserAgent field
821        let mut field = Field::UserAgent;
822        field.render_request(OffsetDateTime::now_utc(), &service_req);
823        if let Field::KV(key, value) = field {
824            assert_eq!(key, "user_agent");
825            assert_eq!(value, Some("test-agent".to_string()));
826        } else {
827            panic!("Field should be KV");
828        }
829
830        // Test Referer field
831        let mut field = Field::Referer;
832        field.render_request(OffsetDateTime::now_utc(), &service_req);
833        if let Field::KV(key, value) = field {
834            assert_eq!(key, "referer");
835            assert_eq!(value, Some("https://example.com".to_string()));
836        } else {
837            panic!("Field should be KV");
838        }
839
840        // Test RequestHeader field
841        let mut field = Field::RequestHeader(HeaderName::from_static("x-request-id"));
842        field.render_request(OffsetDateTime::now_utc(), &service_req);
843        if let Field::KV(key, value) = field {
844            assert_eq!(key, "x-request-id");
845            assert_eq!(value, Some("test-id".to_string()));
846        } else {
847            panic!("Field should be KV");
848        }
849
850        // Test RequestTime field
851        let now = OffsetDateTime::now_utc();
852        let mut field = Field::RequestTime;
853        field.render_request(now, &service_req);
854        if let Field::KV(key, value) = field {
855            assert_eq!(key, "datetime");
856            assert_eq!(value, Some(now.format(&Rfc3339).unwrap()));
857        } else {
858            panic!("Field should be KV");
859        }
860    }
861
862    #[test]
863    fn test_field_render_response() {
864        // Create test request and response
865        let req = TestRequest::default().to_http_request();
866        // let service_req = ServiceRequest::from_request(req);
867
868        let mut response = HttpResponse::build(StatusCode::OK);
869        response.append_header((header::CONTENT_TYPE, "application/json"));
870        response.append_header(("x-custom-header", "test-value"));
871
872        // let service_resp = ServiceResponse::new(service_req, response.finish());
873        let service_resp = ServiceResponse::new(req, response.finish());
874
875        // Test Status field
876        let mut field = Field::Status;
877        field.render_response(&service_resp);
878        if let Field::KV(key, value) = field {
879            assert_eq!(key, "status");
880            assert_eq!(value, Some("200 OK".to_string()));
881        } else {
882            panic!("Field should be KV");
883        }
884
885        // Test ResponseHeader field
886        let mut field = Field::ResponseHeader(HeaderName::from_static("content-type"));
887        field.render_response(&service_resp);
888        if let Field::KV(key, value) = field {
889            assert_eq!(key, "content-type");
890            assert_eq!(value, Some("application/json".to_string()));
891        } else {
892            panic!("Field should be KV");
893        }
894
895        // Test custom ResponseHeader field
896        let mut field = Field::ResponseHeader(HeaderName::from_static("x-custom-header"));
897        field.render_response(&service_resp);
898        if let Field::KV(key, value) = field {
899            assert_eq!(key, "x-custom-header");
900            assert_eq!(value, Some("test-value".to_string()));
901        } else {
902            panic!("Field should be KV");
903        }
904
905        // Test missing ResponseHeader field
906        let mut field = Field::ResponseHeader(HeaderName::from_static("x-missing-header"));
907        field.render_response(&service_resp);
908        if let Field::KV(key, value) = field {
909            assert_eq!(key, "x-missing-header");
910            assert_eq!(value, None);
911        } else {
912            panic!("Field should be KV");
913        }
914    }
915
916    #[test]
917    fn test_field_render() {
918        let entry_time = OffsetDateTime::now_utc() - time::Duration::seconds(2);
919
920        // Test Size field
921        let mut field = Field::Size;
922        field.render(1024, entry_time);
923        if let Field::KV(key, value) = field {
924            assert_eq!(key, "size");
925            assert_eq!(value, Some("1024".to_string()));
926        } else {
927            panic!("Field should be KV");
928        }
929
930        // Test Duration field
931        let mut field = Field::Duration;
932        field.render(0, entry_time);
933        if let Field::KV(key, value) = field {
934            assert_eq!(key, "duration");
935            let duration: f64 = value.unwrap().parse().unwrap();
936            assert!(duration >= 1.9 && duration <= 3.0); // Allow some margin for test execution time
937        } else {
938            panic!("Field should be KV");
939        }
940
941        // Test DurationMillis field
942        let mut field = Field::DurationMillis;
943        field.render(0, entry_time);
944        if let Field::KV(key, value) = field {
945            assert_eq!(key, "duration");
946            let duration: f64 = value.unwrap().parse().unwrap();
947            assert!(duration >= 1900.0 && duration <= 3000.0); // Allow some margin for test execution time
948        } else {
949            panic!("Field should be KV");
950        }
951
952        // Test Environment field (with env var set)
953        unsafe {
954            // Set the environment variable for testing
955            std::env::set_var("TEST_ENV_VAR", "test_value");
956        }
957        let mut field = Field::Environment("TEST_ENV_VAR".to_string());
958        field.render(0, entry_time);
959        if let Field::KV(key, value) = field {
960            assert_eq!(key, "TEST_ENV_VAR");
961            assert_eq!(value, Some("test_value".to_string()));
962        } else {
963            panic!("Field should be KV");
964        }
965
966        // Test Environment field (with env var not set)
967        let mut field = Field::Environment("MISSING_ENV_VAR".to_string());
968        field.render(0, entry_time);
969        if let Field::KV(key, value) = field {
970            assert_eq!(key, "MISSING_ENV_VAR");
971            assert_eq!(value, None);
972        } else {
973            panic!("Field should be KV");
974        }
975    }
976
977    #[test]
978    fn test_request_id_generation() {
979        let id1 = RequestId::new();
980        let id2 = RequestId::new();
981        assert_ne!(id1.0, id2.0); // IDs should be unique
982    }
983}