1use parking_lot::RwLock;
27use std::collections::HashMap;
28use std::sync::OnceLock;
29use sz_rust_infra_facade::config::{LogChannel, LogSection};
30
31pub use sz_rust_orm_facade::logger::{LogEntry, LogLevel, Logger, LoggerFactory, StructuredLogger};
33
34static LOG_FACADE: OnceLock<LogFacade> = OnceLock::new();
36
37pub struct LogFacade {
41 default_channel: String,
43 logger: StructuredLogger,
45 channels: RwLock<HashMap<String, StructuredLogger>>,
47}
48
49impl LogFacade {
50 pub fn new(section: &LogSection) -> Self {
52 let default_channel = section.default.clone();
53 let default_log_level = section
54 .channels
55 .get(&default_channel)
56 .map(|c| parse_level(&c.level))
57 .unwrap_or(LogLevel::Info);
58 let logger = StructuredLogger::with_level(default_log_level);
59
60 let mut channels = HashMap::new();
61 for (name, channel_cfg) in §ion.channels {
62 channels.insert(name.clone(), channel_to_logger(channel_cfg));
63 }
64
65 LogFacade {
66 default_channel,
67 logger,
68 channels: RwLock::new(channels),
69 }
70 }
71
72 pub fn init(section: &LogSection) -> &'static LogFacade {
76 LOG_FACADE.get_or_init(|| LogFacade::new(section))
77 }
78
79 pub fn instance() -> Option<&'static LogFacade> {
83 LOG_FACADE.get()
84 }
85
86 pub fn default_channel(&self) -> &str {
88 &self.default_channel
89 }
90
91 pub fn logger(&self) -> &StructuredLogger {
93 &self.logger
94 }
95
96 pub fn channel(&self, name: &str) -> Option<ChannelRef<'_>> {
100 if self.channels.read().contains_key(name) {
101 Some(ChannelRef {
102 facade: self,
103 name: name.to_string(),
104 })
105 } else {
106 None
107 }
108 }
109
110 pub fn channel_names(&self) -> Vec<String> {
112 self.channels.read().keys().cloned().collect()
113 }
114
115 pub fn log(&self, level: LogLevel, msg: &str) {
117 self.logger.log(level, msg);
118 match level {
119 LogLevel::Trace => tracing::trace!("{}", msg),
120 LogLevel::Debug => tracing::debug!("{}", msg),
121 LogLevel::Info => tracing::info!("{}", msg),
122 LogLevel::Warn => tracing::warn!("{}", msg),
123 LogLevel::Error => tracing::error!("{}", msg),
124 }
125 }
126
127 pub fn debug(&self, msg: &str) {
129 self.log(LogLevel::Debug, msg);
130 }
131
132 pub fn info(&self, msg: &str) {
134 self.log(LogLevel::Info, msg);
135 }
136
137 pub fn warn(&self, msg: &str) {
139 self.log(LogLevel::Warn, msg);
140 }
141
142 pub fn error(&self, msg: &str) {
144 self.log(LogLevel::Error, msg);
145 }
146}
147
148impl std::fmt::Debug for LogFacade {
149 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150 f.debug_struct("LogFacade")
151 .field("default_channel", &self.default_channel)
152 .field("channels", &self.channels.read().keys().collect::<Vec<_>>())
153 .finish()
154 }
155}
156
157pub struct ChannelRef<'a> {
161 facade: &'a LogFacade,
162 name: String,
163}
164
165impl<'a> ChannelRef<'a> {
166 pub fn name(&self) -> &str {
168 &self.name
169 }
170
171 pub fn log(&self, level: LogLevel, msg: &str) {
173 let guard = self.facade.channels.read();
174 if let Some(logger) = guard.get(&self.name) {
175 logger.log(level, msg);
176 }
177 match level {
178 LogLevel::Trace => tracing::trace!("[{}] {}", self.name, msg),
179 LogLevel::Debug => tracing::debug!("[{}] {}", self.name, msg),
180 LogLevel::Info => tracing::info!("[{}] {}", self.name, msg),
181 LogLevel::Warn => tracing::warn!("[{}] {}", self.name, msg),
182 LogLevel::Error => tracing::error!("[{}] {}", self.name, msg),
183 }
184 }
185
186 pub fn debug(&self, msg: &str) {
188 self.log(LogLevel::Debug, msg);
189 }
190
191 pub fn info(&self, msg: &str) {
193 self.log(LogLevel::Info, msg);
194 }
195
196 pub fn warn(&self, msg: &str) {
198 self.log(LogLevel::Warn, msg);
199 }
200
201 pub fn error(&self, msg: &str) {
203 self.log(LogLevel::Error, msg);
204 }
205}
206
207pub fn parse_level(s: &str) -> LogLevel {
212 match s.to_lowercase().as_str() {
213 "trace" => LogLevel::Trace,
214 "debug" => LogLevel::Debug,
215 "info" => LogLevel::Info,
216 "warn" | "warning" => LogLevel::Warn,
217 "error" => LogLevel::Error,
218 _ => LogLevel::Info,
219 }
220}
221
222fn channel_to_logger(channel: &LogChannel) -> StructuredLogger {
224 StructuredLogger::with_level(parse_level(&channel.level))
225}
226
227#[cfg(test)]
232mod tests {
233 use super::*;
234 use sz_rust_infra_facade::config::{LogChannel, LogSection};
235
236 fn make_log_section() -> LogSection {
238 let mut channels = HashMap::new();
239 channels.insert(
240 "file".to_string(),
241 LogChannel {
242 r#type: "file".to_string(),
243 path: "runtime/logs".to_string(),
244 level: "info".to_string(),
245 max_files: 30,
246 format: "%{time} [%{level}] %{message}".to_string(),
247 },
248 );
249 channels.insert(
250 "console".to_string(),
251 LogChannel {
252 r#type: "console".to_string(),
253 path: String::new(),
254 level: "debug".to_string(),
255 max_files: 0,
256 format: "%{time} [%{level}] %{message}".to_string(),
257 },
258 );
259 LogSection {
260 default: "file".to_string(),
261 channels,
262 }
263 }
264
265 #[test]
267 fn test_parse_level() {
268 assert_eq!(parse_level("debug"), LogLevel::Debug);
269 assert_eq!(parse_level("DEBUG"), LogLevel::Debug);
270 assert_eq!(parse_level("Debug"), LogLevel::Debug);
271 assert_eq!(parse_level("info"), LogLevel::Info);
272 assert_eq!(parse_level("INFO"), LogLevel::Info);
273 assert_eq!(parse_level("warn"), LogLevel::Warn);
274 assert_eq!(parse_level("warning"), LogLevel::Warn);
275 assert_eq!(parse_level("WARN"), LogLevel::Warn);
276 assert_eq!(parse_level("error"), LogLevel::Error);
277 assert_eq!(parse_level("ERROR"), LogLevel::Error);
278 assert_eq!(parse_level("unknown"), LogLevel::Info);
280 assert_eq!(parse_level(""), LogLevel::Info);
281 }
282
283 #[test]
285 fn test_log_facade_new() {
286 let section = make_log_section();
287 let facade = LogFacade::new(§ion);
288
289 assert_eq!(facade.default_channel(), "file");
290 let names = facade.channel_names();
291 assert_eq!(names.len(), 2);
292 assert!(names.contains(&"file".to_string()));
293 assert!(names.contains(&"console".to_string()));
294 }
295
296 #[test]
298 fn test_default_logger_level() {
299 let section = make_log_section();
300 let facade = LogFacade::new(§ion);
301
302 assert_eq!(facade.logger().level(), LogLevel::Info);
304
305 facade.debug("debug msg - should be filtered");
307 let entries = facade.logger().entries();
308 assert!(entries.iter().all(|e| e.level != LogLevel::Debug));
309 }
310
311 #[test]
313 fn test_log_to_default_logger() {
314 let section = make_log_section();
315 let facade = LogFacade::new(§ion);
316
317 facade.info("test info message");
318 facade.warn("test warn message");
319 facade.error("test error message");
320
321 let entries = facade.logger().entries();
322 assert!(entries.iter().any(|e| e.message == "test info message"));
323 assert!(entries.iter().any(|e| e.message == "test warn message"));
324 assert!(entries.iter().any(|e| e.message == "test error message"));
325 }
326
327 #[test]
329 fn test_channel_access() {
330 let section = make_log_section();
331 let facade = LogFacade::new(§ion);
332
333 let file_channel = facade.channel("file");
335 assert!(file_channel.is_some());
336 let file_channel = file_channel.unwrap();
337 assert_eq!(file_channel.name(), "file");
338
339 let console_channel = facade.channel("console");
341 assert!(console_channel.is_some());
342
343 assert!(facade.channel("nonexistent").is_none());
345 }
346
347 #[test]
349 fn test_console_channel_debug_level() {
350 let section = make_log_section();
351 let facade = LogFacade::new(§ion);
352
353 let console = facade.channel("console").unwrap();
354 console.debug("debug msg");
355 console.info("info msg");
356 console.warn("warn msg");
357 console.error("error msg");
358
359 let guard = facade.channels.read();
361 let console_logger = guard.get("console").unwrap();
362 let entries = console_logger.entries();
363 assert_eq!(entries.len(), 4);
364 }
365
366 #[test]
368 fn test_log_facade_init_singleton() {
369 let section = make_log_section();
370 let facade = LogFacade::init(§ion);
371
372 let facade2 = LogFacade::instance();
374 assert!(facade2.is_some());
375 assert!(std::ptr::eq(facade, facade2.unwrap()));
376
377 let section2 = make_log_section();
379 let facade3 = LogFacade::init(§ion2);
380 assert!(std::ptr::eq(facade, facade3));
381 }
382
383 #[test]
385 fn test_load_from_config_file() {
386 let config_dir = std::env::current_dir().ok().and_then(|d| {
388 let mut current = d.clone();
389 for _ in 0..5 {
390 if current.join("config").exists() {
391 return Some(current.join("config"));
392 }
393 if let Some(parent) = current.parent() {
394 current = parent.to_path_buf();
395 } else {
396 break;
397 }
398 }
399 None
400 });
401
402 let Some(config_dir) = config_dir else {
403 eprintln!("跳过:未找到 config 目录");
404 return;
405 };
406
407 let log_path = config_dir.join("log.yml");
408 if !log_path.exists() {
409 eprintln!("跳过:未找到 log.yml");
410 return;
411 }
412
413 let content = std::fs::read_to_string(&log_path).unwrap();
414 let section: LogSection = serde_yaml::from_str(&content).unwrap();
415
416 assert_eq!(section.default, "file");
418
419 assert!(section.channels.contains_key("file"));
421 assert!(section.channels.contains_key("console"));
422
423 let file_channel = section.channels.get("file").unwrap();
425 assert_eq!(file_channel.r#type, "file");
426 assert_eq!(file_channel.level, "info");
427 assert_eq!(file_channel.max_files, 30);
428
429 let console_channel = section.channels.get("console").unwrap();
431 assert_eq!(console_channel.r#type, "console");
432 assert_eq!(console_channel.level, "debug");
433 }
434
435 #[test]
437 fn test_log_facade_with_empty_channels() {
438 let section = LogSection::default();
439 let facade = LogFacade::new(§ion);
440
441 assert_eq!(facade.logger().level(), LogLevel::Info);
443 assert_eq!(facade.default_channel(), "");
444 }
445
446 #[test]
448 fn test_log_facade_debug_format() {
449 let section = make_log_section();
450 let facade = LogFacade::new(§ion);
451
452 let debug_str = format!("{:?}", facade);
453 assert!(debug_str.contains("LogFacade"));
454 assert!(debug_str.contains("file"));
455 }
456}
457use axum::extract::Request;
506use axum::middleware::Next;
507use axum::response::Response;
508use std::sync::atomic::{AtomicU64, Ordering};
509use std::time::Instant;
510
511#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
516pub struct RequestId {
517 timestamp_secs: u64,
519 counter: u64,
521}
522
523impl RequestId {
524 pub fn to_hex(&self) -> String {
528 format!("{:08x}{:08x}", self.timestamp_secs, self.counter)
529 }
530
531 pub fn timestamp_secs(&self) -> u64 {
533 self.timestamp_secs
534 }
535
536 pub fn counter(&self) -> u64 {
538 self.counter
539 }
540}
541
542impl std::fmt::Display for RequestId {
543 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
544 f.write_str(&self.to_hex())
545 }
546}
547
548static REQUEST_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
550
551pub fn generate_request_id() -> RequestId {
556 let counter = REQUEST_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
557 let timestamp_secs = std::time::SystemTime::now()
558 .duration_since(std::time::UNIX_EPOCH)
559 .map(|d| d.as_secs())
560 .unwrap_or(0);
561 RequestId {
562 timestamp_secs,
563 counter,
564 }
565}
566
567#[derive(Debug, Clone, Default)]
569pub struct LogConfig {
570 pub exclude_paths: Vec<String>,
574}
575
576impl LogConfig {
577 pub fn with_exclude_paths(mut self, paths: Vec<String>) -> Self {
579 self.exclude_paths = paths;
580 self
581 }
582
583 pub fn is_excluded(&self, path: &str) -> bool {
587 crate::auth::is_route_allowed(path, &self.exclude_paths)
588 }
589}
590
591pub fn log_level_for_status(status: u16) -> LogLevel {
600 match status {
601 400..=499 => LogLevel::Warn,
602 500..=599 => LogLevel::Error,
603 _ => LogLevel::Info,
604 }
605}
606
607pub fn format_request_log(
613 method: &str,
614 uri: &str,
615 status: u16,
616 duration_ms: u64,
617 request_id: &RequestId,
618) -> String {
619 format!(
620 "request_id={} method={} uri={} status={} duration_ms={}",
621 request_id.to_hex(),
622 method,
623 uri,
624 status,
625 duration_ms
626 )
627}
628
629pub async fn log_middleware(req: Request, next: Next) -> Response {
657 log_middleware_inner(req, next, &LogConfig::default()).await
658}
659
660pub async fn log_middleware_with_config(
662 axum::extract::State(config): axum::extract::State<LogConfig>,
663 req: Request,
664 next: Next,
665) -> Response {
666 log_middleware_inner(req, next, &config).await
667}
668
669async fn log_middleware_inner(req: Request, next: Next, config: &LogConfig) -> Response {
670 let method = req.method().clone();
672 let uri = req.uri().path().to_string();
673
674 let request_id = req
676 .extensions()
677 .get::<RequestId>()
678 .copied()
679 .unwrap_or_else(generate_request_id);
680
681 let start = Instant::now();
683
684 let mut req = req;
686 req.extensions_mut().insert(request_id);
687
688 let response = next.run(req).await;
690
691 let duration_ms = start.elapsed().as_millis() as u64;
693
694 if !config.is_excluded(&uri) {
696 let status = response.status().as_u16();
697 let level = log_level_for_status(status);
698 let request_id_hex = request_id.to_hex();
699
700 match level {
701 LogLevel::Trace => tracing::trace!(
702 request_id = %request_id_hex,
703 method = %method,
704 uri = %uri,
705 status = status,
706 duration_ms = duration_ms,
707 "request completed"
708 ),
709 LogLevel::Debug => tracing::debug!(
710 request_id = %request_id_hex,
711 method = %method,
712 uri = %uri,
713 status = status,
714 duration_ms = duration_ms,
715 "request completed"
716 ),
717 LogLevel::Info => tracing::info!(
718 request_id = %request_id_hex,
719 method = %method,
720 uri = %uri,
721 status = status,
722 duration_ms = duration_ms,
723 "request completed"
724 ),
725 LogLevel::Warn => tracing::warn!(
726 request_id = %request_id_hex,
727 method = %method,
728 uri = %uri,
729 status = status,
730 duration_ms = duration_ms,
731 "request completed"
732 ),
733 LogLevel::Error => tracing::error!(
734 request_id = %request_id_hex,
735 method = %method,
736 uri = %uri,
737 status = status,
738 duration_ms = duration_ms,
739 "request completed"
740 ),
741 }
742 }
743
744 response
745}
746
747#[cfg(test)]
748mod middleware_tests {
749 use super::*;
750 use axum::body::Body;
751 use axum::http::StatusCode;
752 use axum::Router;
753 use http_body_util::BodyExt;
754 use tower::ServiceExt;
755
756 async fn read_body(resp: Response) -> String {
761 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
762 String::from_utf8(bytes.to_vec()).unwrap()
763 }
764
765 fn make_request(method: &str, uri: &str) -> Request {
766 Request::builder()
767 .method(method)
768 .uri(uri)
769 .body(Body::empty())
770 .unwrap()
771 }
772
773 fn build_app() -> Router {
775 Router::new()
776 .route(
777 "/ok",
778 axum::routing::get(|| async { axum::http::StatusCode::OK }),
779 )
780 .route(
781 "/notfound",
782 axum::routing::get(|| async { axum::http::StatusCode::NOT_FOUND }),
783 )
784 .route(
785 "/error",
786 axum::routing::get(|| async { axum::http::StatusCode::INTERNAL_SERVER_ERROR }),
787 )
788 .route("/body", axum::routing::get(|| async { "hello" }))
789 .layer(axum::middleware::from_fn(log_middleware))
790 }
791
792 #[test]
797 fn test_request_id_to_hex_is_16_chars() {
798 let id = RequestId {
799 timestamp_secs: 0x12345678,
800 counter: 0x9ABCDEF0,
801 };
802 let hex = id.to_hex();
803 assert_eq!(hex.len(), 16);
804 assert_eq!(hex, "123456789abcdef0");
805 }
806
807 #[test]
808 fn test_request_id_to_hex_zero() {
809 let id = RequestId {
810 timestamp_secs: 0,
811 counter: 0,
812 };
813 assert_eq!(id.to_hex(), "0000000000000000");
814 }
815
816 #[test]
817 fn test_request_id_to_hex_max() {
818 let id = RequestId {
819 timestamp_secs: u64::MAX,
820 counter: u64::MAX,
821 };
822 let hex = id.to_hex();
824 assert_eq!(hex.len(), 32); }
826
827 #[test]
828 fn test_request_id_display_matches_to_hex() {
829 let id = RequestId {
830 timestamp_secs: 0x12345678,
831 counter: 0x9ABCDEF0,
832 };
833 assert_eq!(format!("{}", id), id.to_hex());
834 }
835
836 #[test]
837 fn test_request_id_accessors() {
838 let id = RequestId {
839 timestamp_secs: 100,
840 counter: 200,
841 };
842 assert_eq!(id.timestamp_secs(), 100);
843 assert_eq!(id.counter(), 200);
844 }
845
846 #[test]
847 fn test_request_id_equality() {
848 let id1 = RequestId {
849 timestamp_secs: 1,
850 counter: 2,
851 };
852 let id2 = RequestId {
853 timestamp_secs: 1,
854 counter: 2,
855 };
856 let id3 = RequestId {
857 timestamp_secs: 1,
858 counter: 3,
859 };
860 assert_eq!(id1, id2);
861 assert_ne!(id1, id3);
862 }
863
864 #[test]
869 fn test_generate_request_id_returns_unique() {
870 let id1 = generate_request_id();
871 let id2 = generate_request_id();
872 assert_ne!(id1.counter(), id2.counter());
874 assert_eq!(id2.counter(), id1.counter() + 1);
875 }
876
877 #[test]
878 fn test_generate_request_id_hex_is_16_chars() {
879 let id = generate_request_id();
880 let hex = id.to_hex();
881 assert!(hex.len() >= 16);
884 }
885
886 #[test]
891 fn test_log_level_for_2xx_returns_info() {
892 assert_eq!(log_level_for_status(200), LogLevel::Info);
893 assert_eq!(log_level_for_status(201), LogLevel::Info);
894 assert_eq!(log_level_for_status(204), LogLevel::Info);
895 }
896
897 #[test]
898 fn test_log_level_for_3xx_returns_info() {
899 assert_eq!(log_level_for_status(301), LogLevel::Info);
900 assert_eq!(log_level_for_status(302), LogLevel::Info);
901 assert_eq!(log_level_for_status(304), LogLevel::Info);
902 }
903
904 #[test]
905 fn test_log_level_for_4xx_returns_warn() {
906 assert_eq!(log_level_for_status(400), LogLevel::Warn);
907 assert_eq!(log_level_for_status(401), LogLevel::Warn);
908 assert_eq!(log_level_for_status(403), LogLevel::Warn);
909 assert_eq!(log_level_for_status(404), LogLevel::Warn);
910 assert_eq!(log_level_for_status(422), LogLevel::Warn);
911 assert_eq!(log_level_for_status(499), LogLevel::Warn);
912 }
913
914 #[test]
915 fn test_log_level_for_5xx_returns_error() {
916 assert_eq!(log_level_for_status(500), LogLevel::Error);
917 assert_eq!(log_level_for_status(501), LogLevel::Error);
918 assert_eq!(log_level_for_status(502), LogLevel::Error);
919 assert_eq!(log_level_for_status(503), LogLevel::Error);
920 assert_eq!(log_level_for_status(599), LogLevel::Error);
921 }
922
923 #[test]
924 fn test_log_level_for_1xx_returns_info() {
925 assert_eq!(log_level_for_status(100), LogLevel::Info);
927 assert_eq!(log_level_for_status(101), LogLevel::Info);
928 }
929
930 #[test]
931 fn test_log_level_for_boundary() {
932 assert_eq!(log_level_for_status(399), LogLevel::Info);
934 assert_eq!(log_level_for_status(400), LogLevel::Warn);
935 assert_eq!(log_level_for_status(499), LogLevel::Warn);
936 assert_eq!(log_level_for_status(500), LogLevel::Error);
937 assert_eq!(log_level_for_status(599), LogLevel::Error);
938 assert_eq!(log_level_for_status(600), LogLevel::Info);
939 }
940
941 #[test]
946 fn test_format_request_log_basic() {
947 let request_id = RequestId {
948 timestamp_secs: 0x12345678,
949 counter: 0x9ABCDEF0,
950 };
951 let msg = format_request_log("GET", "/api/users", 200, 15, &request_id);
952 assert_eq!(
953 msg,
954 "request_id=123456789abcdef0 method=GET uri=/api/users status=200 duration_ms=15"
955 );
956 }
957
958 #[test]
959 fn test_format_request_log_post_method() {
960 let request_id = RequestId {
961 timestamp_secs: 0,
962 counter: 1,
963 };
964 let msg = format_request_log("POST", "/api/orders", 201, 42, &request_id);
965 assert_eq!(
966 msg,
967 "request_id=0000000000000001 method=POST uri=/api/orders status=201 duration_ms=42"
968 );
969 }
970
971 #[test]
972 fn test_format_request_log_error_status() {
973 let request_id = RequestId {
974 timestamp_secs: 0,
975 counter: 0,
976 };
977 let msg = format_request_log("GET", "/missing", 404, 5, &request_id);
978 assert_eq!(
979 msg,
980 "request_id=0000000000000000 method=GET uri=/missing status=404 duration_ms=5"
981 );
982 }
983
984 #[test]
985 fn test_format_request_log_with_query_string_in_uri() {
986 let request_id = RequestId {
988 timestamp_secs: 0,
989 counter: 0,
990 };
991 let msg = format_request_log("GET", "/api?foo=bar", 200, 1, &request_id);
992 assert!(msg.contains("uri=/api?foo=bar"));
993 }
994
995 #[test]
1000 fn test_log_config_default_empty_exclude_paths() {
1001 let config = LogConfig::default();
1002 assert!(config.exclude_paths.is_empty());
1003 }
1004
1005 #[test]
1006 fn test_log_config_with_exclude_paths() {
1007 let config = LogConfig::default().with_exclude_paths(vec!["/health".to_string()]);
1008 assert_eq!(config.exclude_paths, vec!["/health".to_string()]);
1009 }
1010
1011 #[test]
1012 fn test_log_config_is_excluded_exact_match() {
1013 let config = LogConfig::default().with_exclude_paths(vec!["/health".to_string()]);
1014 assert!(config.is_excluded("/health"));
1015 assert!(!config.is_excluded("/health/detail"));
1016 assert!(!config.is_excluded("/api"));
1017 }
1018
1019 #[test]
1020 fn test_log_config_is_excluded_wildcard_match() {
1021 let config = LogConfig::default().with_exclude_paths(vec!["/health/*".to_string()]);
1022 assert!(config.is_excluded("/health/check"));
1023 assert!(config.is_excluded("/health/deep/nested"));
1024 assert!(!config.is_excluded("/health"));
1025 assert!(!config.is_excluded("/api"));
1026 }
1027
1028 #[test]
1029 fn test_log_config_is_excluded_empty_list() {
1030 let config = LogConfig::default();
1031 assert!(!config.is_excluded("/any"));
1032 }
1033
1034 #[test]
1035 fn test_log_config_is_excluded_multiple_entries() {
1036 let config = LogConfig::default()
1037 .with_exclude_paths(vec!["/health".to_string(), "/metrics/*".to_string()]);
1038 assert!(config.is_excluded("/health"));
1039 assert!(config.is_excluded("/metrics/prometheus"));
1040 assert!(!config.is_excluded("/api"));
1041 }
1042
1043 #[tokio::test]
1048 async fn test_log_middleware_returns_response_unchanged() {
1049 let app = build_app();
1051 let resp = app.oneshot(make_request("GET", "/body")).await.unwrap();
1052 let body = read_body(resp).await;
1053 assert_eq!(body, "hello");
1054 }
1055
1056 #[tokio::test]
1057 async fn test_log_middleware_returns_correct_status() {
1058 let app = build_app();
1059 let resp = app.oneshot(make_request("GET", "/ok")).await.unwrap();
1060 assert_eq!(resp.status(), StatusCode::OK);
1061 }
1062
1063 #[tokio::test]
1064 async fn test_log_middleware_injects_request_id() {
1065 let app = Router::new()
1067 .route(
1068 "/",
1069 axum::routing::get(|req: Request| async move {
1070 let request_id = req.extensions().get::<RequestId>().unwrap();
1071 format!("request_id:{}", request_id.to_hex())
1072 }),
1073 )
1074 .layer(axum::middleware::from_fn(log_middleware));
1075
1076 let resp = app.oneshot(make_request("GET", "/")).await.unwrap();
1077 assert_eq!(resp.status(), StatusCode::OK);
1078 let body = read_body(resp).await;
1079 assert!(body.starts_with("request_id:"));
1080 let hex = body.strip_prefix("request_id:").unwrap();
1082 assert!(hex.len() >= 16);
1083 }
1084
1085 #[tokio::test]
1086 async fn test_log_middleware_generates_unique_request_ids() {
1087 let app = Router::new()
1089 .route(
1090 "/",
1091 axum::routing::get(|req: Request| async move {
1092 let request_id = req.extensions().get::<RequestId>().unwrap();
1093 request_id.to_hex()
1094 }),
1095 )
1096 .layer(axum::middleware::from_fn(log_middleware));
1097
1098 let resp1 = app.clone().oneshot(make_request("GET", "/")).await.unwrap();
1099 let hex1 = read_body(resp1).await;
1100
1101 let resp2 = app.oneshot(make_request("GET", "/")).await.unwrap();
1102 let hex2 = read_body(resp2).await;
1103
1104 assert_ne!(hex1, hex2);
1105 }
1106
1107 #[tokio::test]
1108 async fn test_log_middleware_preserves_existing_request_id() {
1109 let existing_id = RequestId {
1111 timestamp_secs: 0xDEADBEEF,
1112 counter: 0x12345678,
1113 };
1114 let app = Router::new()
1115 .route(
1116 "/",
1117 axum::routing::get(|req: Request| async move {
1118 let request_id = req.extensions().get::<RequestId>().unwrap();
1119 request_id.to_hex()
1120 }),
1121 )
1122 .layer(axum::middleware::from_fn(log_middleware))
1123 .layer(
1124 tower::ServiceBuilder::new().layer(tower::layer::layer_fn(move |service| {
1125 tower::util::MapRequest::new(service, move |mut req: Request| {
1126 req.extensions_mut().insert(existing_id);
1127 req
1128 })
1129 })),
1130 );
1131
1132 let resp = app.oneshot(make_request("GET", "/")).await.unwrap();
1133 let body = read_body(resp).await;
1134 assert_eq!(body, "deadbeef12345678");
1135 }
1136
1137 #[tokio::test]
1138 async fn test_log_middleware_records_2xx_status() {
1139 let app = build_app();
1141 let resp = app.oneshot(make_request("GET", "/ok")).await.unwrap();
1142 assert_eq!(resp.status(), StatusCode::OK);
1143 }
1144
1145 #[tokio::test]
1146 async fn test_log_middleware_records_4xx_status() {
1147 let app = build_app();
1148 let resp = app.oneshot(make_request("GET", "/notfound")).await.unwrap();
1149 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1150 }
1151
1152 #[tokio::test]
1153 async fn test_log_middleware_records_5xx_status() {
1154 let app = build_app();
1155 let resp = app.oneshot(make_request("GET", "/error")).await.unwrap();
1156 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1157 }
1158
1159 #[tokio::test]
1160 async fn test_log_middleware_with_config_excludes_path() {
1161 let config = LogConfig::default().with_exclude_paths(vec!["/health".to_string()]);
1163 let app = Router::new()
1164 .route("/health", axum::routing::get(|| async { "healthy" }))
1165 .layer(axum::middleware::from_fn_with_state(
1166 config,
1167 log_middleware_with_config,
1168 ));
1169
1170 let resp = app.oneshot(make_request("GET", "/health")).await.unwrap();
1171 assert_eq!(resp.status(), StatusCode::OK);
1172 let body = read_body(resp).await;
1173 assert_eq!(body, "healthy");
1174 }
1175
1176 #[tokio::test]
1177 async fn test_log_middleware_with_config_wildcard_exclude() {
1178 let config = LogConfig::default().with_exclude_paths(vec!["/metrics/*".to_string()]);
1180 let app = Router::new()
1181 .route(
1182 "/metrics/prometheus",
1183 axum::routing::get(|| async { "metrics" }),
1184 )
1185 .layer(axum::middleware::from_fn_with_state(
1186 config,
1187 log_middleware_with_config,
1188 ));
1189
1190 let resp = app
1191 .oneshot(make_request("GET", "/metrics/prometheus"))
1192 .await
1193 .unwrap();
1194 assert_eq!(resp.status(), StatusCode::OK);
1195 }
1196
1197 #[tokio::test]
1198 async fn test_log_middleware_preserves_method_and_uri() {
1199 let app = build_app();
1202 let resp = app.oneshot(make_request("GET", "/ok")).await.unwrap();
1203 assert_eq!(resp.status(), StatusCode::OK);
1204 }
1205
1206 #[tokio::test]
1207 async fn test_log_middleware_duration_is_non_negative() {
1208 let app = build_app();
1210 let start = std::time::Instant::now();
1211 let resp = app.oneshot(make_request("GET", "/ok")).await.unwrap();
1212 let elapsed = start.elapsed();
1213 assert!(resp.status().is_success());
1214 assert!(elapsed.as_millis() < 5000); }
1217
1218 #[tokio::test]
1219 async fn test_log_middleware_handles_post_request() {
1220 let app = Router::new()
1221 .route(
1222 "/submit",
1223 axum::routing::post(|| async { axum::http::StatusCode::CREATED }),
1224 )
1225 .layer(axum::middleware::from_fn(log_middleware));
1226
1227 let req = Request::builder()
1228 .method("POST")
1229 .uri("/submit")
1230 .body(Body::empty())
1231 .unwrap();
1232 let resp = app.oneshot(req).await.unwrap();
1233 assert_eq!(resp.status(), StatusCode::CREATED);
1234 }
1235
1236 #[tokio::test]
1237 async fn test_log_middleware_chains_with_other_middleware() {
1238 async fn add_header_middleware(req: Request, next: Next) -> Response {
1240 let mut resp = next.run(req).await;
1241 resp.headers_mut()
1242 .insert("X-Custom", "value".parse().unwrap());
1243 resp
1244 }
1245
1246 let app = Router::new()
1247 .route("/", axum::routing::get(|| async { "ok" }))
1248 .layer(axum::middleware::from_fn(add_header_middleware))
1249 .layer(axum::middleware::from_fn(log_middleware));
1250
1251 let resp = app.oneshot(make_request("GET", "/")).await.unwrap();
1252 assert_eq!(resp.status(), StatusCode::OK);
1253 assert_eq!(resp.headers().get("X-Custom").unwrap(), "value");
1254 }
1255
1256 #[test]
1261 fn test_php_apart_level_alignment() {
1262 assert_eq!(log_level_for_status(200), LogLevel::Info);
1266 assert_eq!(log_level_for_status(404), LogLevel::Warn);
1267 assert_eq!(log_level_for_status(500), LogLevel::Error);
1268 }
1269
1270 #[test]
1271 fn test_php_think_logger_level_alignment() {
1272 let levels = [
1275 LogLevel::Debug,
1276 LogLevel::Info,
1277 LogLevel::Warn,
1278 LogLevel::Error,
1279 ];
1280 assert_eq!(levels.len(), 4);
1281 }
1282
1283 #[test]
1284 fn test_request_id_format_aligns_with_w3c_span_id_length() {
1285 let id = RequestId {
1288 timestamp_secs: 0x12345678,
1289 counter: 0x9ABCDEF0,
1290 };
1291 assert_eq!(id.to_hex().len(), 16);
1292 }
1293}