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
27pub 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 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 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 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 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 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
198pub 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 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 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 KV(String, Option<String>),
485 Method,
487 Status,
489 Path,
491 Params,
493 Version,
495 Host,
497 RemoteAddr,
499 RealIp,
501 RequestId(HeaderName),
505 #[cfg(feature = "tracing-request-id")]
506 TracingRequestId,
508 RequestHeader(HeaderName),
510 ResponseHeader(HeaderName),
512 Size,
514 Duration,
516 DurationMillis,
518 RequestTime,
520 UserAgent,
522 Referer,
524 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 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 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 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 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 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 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 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 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 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 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 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 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 let req = TestRequest::default().to_http_request();
866 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(req, response.finish());
874
875 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 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 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 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 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 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); } else {
938 panic!("Field should be KV");
939 }
940
941 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); } else {
949 panic!("Field should be KV");
950 }
951
952 unsafe {
954 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 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); }
983}