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::Debug => tracing::debug!("{}", msg),
120 LogLevel::Info => tracing::info!("{}", msg),
121 LogLevel::Warn => tracing::warn!("{}", msg),
122 LogLevel::Error => tracing::error!("{}", msg),
123 }
124 }
125
126 pub fn debug(&self, msg: &str) {
128 self.log(LogLevel::Debug, msg);
129 }
130
131 pub fn info(&self, msg: &str) {
133 self.log(LogLevel::Info, msg);
134 }
135
136 pub fn warn(&self, msg: &str) {
138 self.log(LogLevel::Warn, msg);
139 }
140
141 pub fn error(&self, msg: &str) {
143 self.log(LogLevel::Error, msg);
144 }
145}
146
147impl std::fmt::Debug for LogFacade {
148 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
149 f.debug_struct("LogFacade")
150 .field("default_channel", &self.default_channel)
151 .field("channels", &self.channels.read().keys().collect::<Vec<_>>())
152 .finish()
153 }
154}
155
156pub struct ChannelRef<'a> {
160 facade: &'a LogFacade,
161 name: String,
162}
163
164impl<'a> ChannelRef<'a> {
165 pub fn name(&self) -> &str {
167 &self.name
168 }
169
170 pub fn log(&self, level: LogLevel, msg: &str) {
172 let guard = self.facade.channels.read();
173 if let Some(logger) = guard.get(&self.name) {
174 logger.log(level, msg);
175 }
176 match level {
177 LogLevel::Debug => tracing::debug!("[{}] {}", self.name, msg),
178 LogLevel::Info => tracing::info!("[{}] {}", self.name, msg),
179 LogLevel::Warn => tracing::warn!("[{}] {}", self.name, msg),
180 LogLevel::Error => tracing::error!("[{}] {}", self.name, msg),
181 }
182 }
183
184 pub fn debug(&self, msg: &str) {
186 self.log(LogLevel::Debug, msg);
187 }
188
189 pub fn info(&self, msg: &str) {
191 self.log(LogLevel::Info, msg);
192 }
193
194 pub fn warn(&self, msg: &str) {
196 self.log(LogLevel::Warn, msg);
197 }
198
199 pub fn error(&self, msg: &str) {
201 self.log(LogLevel::Error, msg);
202 }
203}
204
205pub fn parse_level(s: &str) -> LogLevel {
210 match s.to_lowercase().as_str() {
211 "debug" => LogLevel::Debug,
212 "info" => LogLevel::Info,
213 "warn" | "warning" => LogLevel::Warn,
214 "error" => LogLevel::Error,
215 _ => LogLevel::Info,
216 }
217}
218
219fn channel_to_logger(channel: &LogChannel) -> StructuredLogger {
221 StructuredLogger::with_level(parse_level(&channel.level))
222}
223
224#[cfg(test)]
229mod tests {
230 use super::*;
231 use sz_rust_infra_facade::config::{LogChannel, LogSection};
232
233 fn make_log_section() -> LogSection {
235 let mut channels = HashMap::new();
236 channels.insert(
237 "file".to_string(),
238 LogChannel {
239 r#type: "file".to_string(),
240 path: "runtime/logs".to_string(),
241 level: "info".to_string(),
242 max_files: 30,
243 format: "%{time} [%{level}] %{message}".to_string(),
244 },
245 );
246 channels.insert(
247 "console".to_string(),
248 LogChannel {
249 r#type: "console".to_string(),
250 path: String::new(),
251 level: "debug".to_string(),
252 max_files: 0,
253 format: "%{time} [%{level}] %{message}".to_string(),
254 },
255 );
256 LogSection {
257 default: "file".to_string(),
258 channels,
259 }
260 }
261
262 #[test]
264 fn test_parse_level() {
265 assert_eq!(parse_level("debug"), LogLevel::Debug);
266 assert_eq!(parse_level("DEBUG"), LogLevel::Debug);
267 assert_eq!(parse_level("Debug"), LogLevel::Debug);
268 assert_eq!(parse_level("info"), LogLevel::Info);
269 assert_eq!(parse_level("INFO"), LogLevel::Info);
270 assert_eq!(parse_level("warn"), LogLevel::Warn);
271 assert_eq!(parse_level("warning"), LogLevel::Warn);
272 assert_eq!(parse_level("WARN"), LogLevel::Warn);
273 assert_eq!(parse_level("error"), LogLevel::Error);
274 assert_eq!(parse_level("ERROR"), LogLevel::Error);
275 assert_eq!(parse_level("unknown"), LogLevel::Info);
277 assert_eq!(parse_level(""), LogLevel::Info);
278 }
279
280 #[test]
282 fn test_log_facade_new() {
283 let section = make_log_section();
284 let facade = LogFacade::new(§ion);
285
286 assert_eq!(facade.default_channel(), "file");
287 let names = facade.channel_names();
288 assert_eq!(names.len(), 2);
289 assert!(names.contains(&"file".to_string()));
290 assert!(names.contains(&"console".to_string()));
291 }
292
293 #[test]
295 fn test_default_logger_level() {
296 let section = make_log_section();
297 let facade = LogFacade::new(§ion);
298
299 assert_eq!(facade.logger().level(), LogLevel::Info);
301
302 facade.debug("debug msg - should be filtered");
304 let entries = facade.logger().entries();
305 assert!(entries.iter().all(|e| e.level != LogLevel::Debug));
306 }
307
308 #[test]
310 fn test_log_to_default_logger() {
311 let section = make_log_section();
312 let facade = LogFacade::new(§ion);
313
314 facade.info("test info message");
315 facade.warn("test warn message");
316 facade.error("test error message");
317
318 let entries = facade.logger().entries();
319 assert!(entries.iter().any(|e| e.message == "test info message"));
320 assert!(entries.iter().any(|e| e.message == "test warn message"));
321 assert!(entries.iter().any(|e| e.message == "test error message"));
322 }
323
324 #[test]
326 fn test_channel_access() {
327 let section = make_log_section();
328 let facade = LogFacade::new(§ion);
329
330 let file_channel = facade.channel("file");
332 assert!(file_channel.is_some());
333 let file_channel = file_channel.unwrap();
334 assert_eq!(file_channel.name(), "file");
335
336 let console_channel = facade.channel("console");
338 assert!(console_channel.is_some());
339
340 assert!(facade.channel("nonexistent").is_none());
342 }
343
344 #[test]
346 fn test_console_channel_debug_level() {
347 let section = make_log_section();
348 let facade = LogFacade::new(§ion);
349
350 let console = facade.channel("console").unwrap();
351 console.debug("debug msg");
352 console.info("info msg");
353 console.warn("warn msg");
354 console.error("error msg");
355
356 let guard = facade.channels.read();
358 let console_logger = guard.get("console").unwrap();
359 let entries = console_logger.entries();
360 assert_eq!(entries.len(), 4);
361 }
362
363 #[test]
365 fn test_log_facade_init_singleton() {
366 let section = make_log_section();
367 let facade = LogFacade::init(§ion);
368
369 let facade2 = LogFacade::instance();
371 assert!(facade2.is_some());
372 assert!(std::ptr::eq(facade, facade2.unwrap()));
373
374 let section2 = make_log_section();
376 let facade3 = LogFacade::init(§ion2);
377 assert!(std::ptr::eq(facade, facade3));
378 }
379
380 #[test]
382 fn test_load_from_config_file() {
383 let config_dir = std::env::current_dir().ok().and_then(|d| {
385 let mut current = d.clone();
386 for _ in 0..5 {
387 if current.join("config").exists() {
388 return Some(current.join("config"));
389 }
390 if let Some(parent) = current.parent() {
391 current = parent.to_path_buf();
392 } else {
393 break;
394 }
395 }
396 None
397 });
398
399 let Some(config_dir) = config_dir else {
400 eprintln!("跳过:未找到 config 目录");
401 return;
402 };
403
404 let log_path = config_dir.join("log.yml");
405 if !log_path.exists() {
406 eprintln!("跳过:未找到 log.yml");
407 return;
408 }
409
410 let content = std::fs::read_to_string(&log_path).unwrap();
411 let section: LogSection = serde_yaml::from_str(&content).unwrap();
412
413 assert_eq!(section.default, "file");
415
416 assert!(section.channels.contains_key("file"));
418 assert!(section.channels.contains_key("console"));
419
420 let file_channel = section.channels.get("file").unwrap();
422 assert_eq!(file_channel.r#type, "file");
423 assert_eq!(file_channel.level, "info");
424 assert_eq!(file_channel.max_files, 30);
425
426 let console_channel = section.channels.get("console").unwrap();
428 assert_eq!(console_channel.r#type, "console");
429 assert_eq!(console_channel.level, "debug");
430 }
431
432 #[test]
434 fn test_log_facade_with_empty_channels() {
435 let section = LogSection::default();
436 let facade = LogFacade::new(§ion);
437
438 assert_eq!(facade.logger().level(), LogLevel::Info);
440 assert_eq!(facade.default_channel(), "");
441 }
442
443 #[test]
445 fn test_log_facade_debug_format() {
446 let section = make_log_section();
447 let facade = LogFacade::new(§ion);
448
449 let debug_str = format!("{:?}", facade);
450 assert!(debug_str.contains("LogFacade"));
451 assert!(debug_str.contains("file"));
452 }
453}
454use axum::extract::Request;
503use axum::middleware::Next;
504use axum::response::Response;
505use std::sync::atomic::{AtomicU64, Ordering};
506use std::time::Instant;
507
508#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
513pub struct RequestId {
514 timestamp_secs: u64,
516 counter: u64,
518}
519
520impl RequestId {
521 pub fn to_hex(&self) -> String {
525 format!("{:08x}{:08x}", self.timestamp_secs, self.counter)
526 }
527
528 pub fn timestamp_secs(&self) -> u64 {
530 self.timestamp_secs
531 }
532
533 pub fn counter(&self) -> u64 {
535 self.counter
536 }
537}
538
539impl std::fmt::Display for RequestId {
540 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
541 f.write_str(&self.to_hex())
542 }
543}
544
545static REQUEST_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
547
548pub fn generate_request_id() -> RequestId {
553 let counter = REQUEST_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
554 let timestamp_secs = std::time::SystemTime::now()
555 .duration_since(std::time::UNIX_EPOCH)
556 .map(|d| d.as_secs())
557 .unwrap_or(0);
558 RequestId {
559 timestamp_secs,
560 counter,
561 }
562}
563
564#[derive(Debug, Clone, Default)]
566pub struct LogConfig {
567 pub exclude_paths: Vec<String>,
571}
572
573impl LogConfig {
574 pub fn with_exclude_paths(mut self, paths: Vec<String>) -> Self {
576 self.exclude_paths = paths;
577 self
578 }
579
580 pub fn is_excluded(&self, path: &str) -> bool {
584 crate::auth::is_route_allowed(path, &self.exclude_paths)
585 }
586}
587
588pub fn log_level_for_status(status: u16) -> LogLevel {
597 match status {
598 400..=499 => LogLevel::Warn,
599 500..=599 => LogLevel::Error,
600 _ => LogLevel::Info,
601 }
602}
603
604pub fn format_request_log(
610 method: &str,
611 uri: &str,
612 status: u16,
613 duration_ms: u64,
614 request_id: &RequestId,
615) -> String {
616 format!(
617 "request_id={} method={} uri={} status={} duration_ms={}",
618 request_id.to_hex(),
619 method,
620 uri,
621 status,
622 duration_ms
623 )
624}
625
626pub async fn log_middleware(req: Request, next: Next) -> Response {
654 log_middleware_inner(req, next, &LogConfig::default()).await
655}
656
657pub async fn log_middleware_with_config(
659 axum::extract::State(config): axum::extract::State<LogConfig>,
660 req: Request,
661 next: Next,
662) -> Response {
663 log_middleware_inner(req, next, &config).await
664}
665
666async fn log_middleware_inner(req: Request, next: Next, config: &LogConfig) -> Response {
667 let method = req.method().clone();
669 let uri = req.uri().path().to_string();
670
671 let request_id = req
673 .extensions()
674 .get::<RequestId>()
675 .copied()
676 .unwrap_or_else(generate_request_id);
677
678 let start = Instant::now();
680
681 let mut req = req;
683 req.extensions_mut().insert(request_id);
684
685 let response = next.run(req).await;
687
688 let duration_ms = start.elapsed().as_millis() as u64;
690
691 if !config.is_excluded(&uri) {
693 let status = response.status().as_u16();
694 let level = log_level_for_status(status);
695 let request_id_hex = request_id.to_hex();
696
697 match level {
698 LogLevel::Debug => tracing::debug!(
699 request_id = %request_id_hex,
700 method = %method,
701 uri = %uri,
702 status = status,
703 duration_ms = duration_ms,
704 "request completed"
705 ),
706 LogLevel::Info => tracing::info!(
707 request_id = %request_id_hex,
708 method = %method,
709 uri = %uri,
710 status = status,
711 duration_ms = duration_ms,
712 "request completed"
713 ),
714 LogLevel::Warn => tracing::warn!(
715 request_id = %request_id_hex,
716 method = %method,
717 uri = %uri,
718 status = status,
719 duration_ms = duration_ms,
720 "request completed"
721 ),
722 LogLevel::Error => tracing::error!(
723 request_id = %request_id_hex,
724 method = %method,
725 uri = %uri,
726 status = status,
727 duration_ms = duration_ms,
728 "request completed"
729 ),
730 }
731 }
732
733 response
734}
735
736#[cfg(test)]
737mod middleware_tests {
738 use super::*;
739 use axum::body::Body;
740 use axum::http::StatusCode;
741 use axum::Router;
742 use http_body_util::BodyExt;
743 use tower::ServiceExt;
744
745 async fn read_body(resp: Response) -> String {
750 let bytes = resp.into_body().collect().await.unwrap().to_bytes();
751 String::from_utf8(bytes.to_vec()).unwrap()
752 }
753
754 fn make_request(method: &str, uri: &str) -> Request {
755 Request::builder()
756 .method(method)
757 .uri(uri)
758 .body(Body::empty())
759 .unwrap()
760 }
761
762 fn build_app() -> Router {
764 Router::new()
765 .route(
766 "/ok",
767 axum::routing::get(|| async { axum::http::StatusCode::OK }),
768 )
769 .route(
770 "/notfound",
771 axum::routing::get(|| async { axum::http::StatusCode::NOT_FOUND }),
772 )
773 .route(
774 "/error",
775 axum::routing::get(|| async { axum::http::StatusCode::INTERNAL_SERVER_ERROR }),
776 )
777 .route("/body", axum::routing::get(|| async { "hello" }))
778 .layer(axum::middleware::from_fn(log_middleware))
779 }
780
781 #[test]
786 fn test_request_id_to_hex_is_16_chars() {
787 let id = RequestId {
788 timestamp_secs: 0x12345678,
789 counter: 0x9ABCDEF0,
790 };
791 let hex = id.to_hex();
792 assert_eq!(hex.len(), 16);
793 assert_eq!(hex, "123456789abcdef0");
794 }
795
796 #[test]
797 fn test_request_id_to_hex_zero() {
798 let id = RequestId {
799 timestamp_secs: 0,
800 counter: 0,
801 };
802 assert_eq!(id.to_hex(), "0000000000000000");
803 }
804
805 #[test]
806 fn test_request_id_to_hex_max() {
807 let id = RequestId {
808 timestamp_secs: u64::MAX,
809 counter: u64::MAX,
810 };
811 let hex = id.to_hex();
813 assert_eq!(hex.len(), 32); }
815
816 #[test]
817 fn test_request_id_display_matches_to_hex() {
818 let id = RequestId {
819 timestamp_secs: 0x12345678,
820 counter: 0x9ABCDEF0,
821 };
822 assert_eq!(format!("{}", id), id.to_hex());
823 }
824
825 #[test]
826 fn test_request_id_accessors() {
827 let id = RequestId {
828 timestamp_secs: 100,
829 counter: 200,
830 };
831 assert_eq!(id.timestamp_secs(), 100);
832 assert_eq!(id.counter(), 200);
833 }
834
835 #[test]
836 fn test_request_id_equality() {
837 let id1 = RequestId {
838 timestamp_secs: 1,
839 counter: 2,
840 };
841 let id2 = RequestId {
842 timestamp_secs: 1,
843 counter: 2,
844 };
845 let id3 = RequestId {
846 timestamp_secs: 1,
847 counter: 3,
848 };
849 assert_eq!(id1, id2);
850 assert_ne!(id1, id3);
851 }
852
853 #[test]
858 fn test_generate_request_id_returns_unique() {
859 let id1 = generate_request_id();
860 let id2 = generate_request_id();
861 assert_ne!(id1.counter(), id2.counter());
863 assert_eq!(id2.counter(), id1.counter() + 1);
864 }
865
866 #[test]
867 fn test_generate_request_id_hex_is_16_chars() {
868 let id = generate_request_id();
869 let hex = id.to_hex();
870 assert!(hex.len() >= 16);
873 }
874
875 #[test]
880 fn test_log_level_for_2xx_returns_info() {
881 assert_eq!(log_level_for_status(200), LogLevel::Info);
882 assert_eq!(log_level_for_status(201), LogLevel::Info);
883 assert_eq!(log_level_for_status(204), LogLevel::Info);
884 }
885
886 #[test]
887 fn test_log_level_for_3xx_returns_info() {
888 assert_eq!(log_level_for_status(301), LogLevel::Info);
889 assert_eq!(log_level_for_status(302), LogLevel::Info);
890 assert_eq!(log_level_for_status(304), LogLevel::Info);
891 }
892
893 #[test]
894 fn test_log_level_for_4xx_returns_warn() {
895 assert_eq!(log_level_for_status(400), LogLevel::Warn);
896 assert_eq!(log_level_for_status(401), LogLevel::Warn);
897 assert_eq!(log_level_for_status(403), LogLevel::Warn);
898 assert_eq!(log_level_for_status(404), LogLevel::Warn);
899 assert_eq!(log_level_for_status(422), LogLevel::Warn);
900 assert_eq!(log_level_for_status(499), LogLevel::Warn);
901 }
902
903 #[test]
904 fn test_log_level_for_5xx_returns_error() {
905 assert_eq!(log_level_for_status(500), LogLevel::Error);
906 assert_eq!(log_level_for_status(501), LogLevel::Error);
907 assert_eq!(log_level_for_status(502), LogLevel::Error);
908 assert_eq!(log_level_for_status(503), LogLevel::Error);
909 assert_eq!(log_level_for_status(599), LogLevel::Error);
910 }
911
912 #[test]
913 fn test_log_level_for_1xx_returns_info() {
914 assert_eq!(log_level_for_status(100), LogLevel::Info);
916 assert_eq!(log_level_for_status(101), LogLevel::Info);
917 }
918
919 #[test]
920 fn test_log_level_for_boundary() {
921 assert_eq!(log_level_for_status(399), LogLevel::Info);
923 assert_eq!(log_level_for_status(400), LogLevel::Warn);
924 assert_eq!(log_level_for_status(499), LogLevel::Warn);
925 assert_eq!(log_level_for_status(500), LogLevel::Error);
926 assert_eq!(log_level_for_status(599), LogLevel::Error);
927 assert_eq!(log_level_for_status(600), LogLevel::Info);
928 }
929
930 #[test]
935 fn test_format_request_log_basic() {
936 let request_id = RequestId {
937 timestamp_secs: 0x12345678,
938 counter: 0x9ABCDEF0,
939 };
940 let msg = format_request_log("GET", "/api/users", 200, 15, &request_id);
941 assert_eq!(
942 msg,
943 "request_id=123456789abcdef0 method=GET uri=/api/users status=200 duration_ms=15"
944 );
945 }
946
947 #[test]
948 fn test_format_request_log_post_method() {
949 let request_id = RequestId {
950 timestamp_secs: 0,
951 counter: 1,
952 };
953 let msg = format_request_log("POST", "/api/orders", 201, 42, &request_id);
954 assert_eq!(
955 msg,
956 "request_id=0000000000000001 method=POST uri=/api/orders status=201 duration_ms=42"
957 );
958 }
959
960 #[test]
961 fn test_format_request_log_error_status() {
962 let request_id = RequestId {
963 timestamp_secs: 0,
964 counter: 0,
965 };
966 let msg = format_request_log("GET", "/missing", 404, 5, &request_id);
967 assert_eq!(
968 msg,
969 "request_id=0000000000000000 method=GET uri=/missing status=404 duration_ms=5"
970 );
971 }
972
973 #[test]
974 fn test_format_request_log_with_query_string_in_uri() {
975 let request_id = RequestId {
977 timestamp_secs: 0,
978 counter: 0,
979 };
980 let msg = format_request_log("GET", "/api?foo=bar", 200, 1, &request_id);
981 assert!(msg.contains("uri=/api?foo=bar"));
982 }
983
984 #[test]
989 fn test_log_config_default_empty_exclude_paths() {
990 let config = LogConfig::default();
991 assert!(config.exclude_paths.is_empty());
992 }
993
994 #[test]
995 fn test_log_config_with_exclude_paths() {
996 let config = LogConfig::default().with_exclude_paths(vec!["/health".to_string()]);
997 assert_eq!(config.exclude_paths, vec!["/health".to_string()]);
998 }
999
1000 #[test]
1001 fn test_log_config_is_excluded_exact_match() {
1002 let config = LogConfig::default().with_exclude_paths(vec!["/health".to_string()]);
1003 assert!(config.is_excluded("/health"));
1004 assert!(!config.is_excluded("/health/detail"));
1005 assert!(!config.is_excluded("/api"));
1006 }
1007
1008 #[test]
1009 fn test_log_config_is_excluded_wildcard_match() {
1010 let config = LogConfig::default().with_exclude_paths(vec!["/health/*".to_string()]);
1011 assert!(config.is_excluded("/health/check"));
1012 assert!(config.is_excluded("/health/deep/nested"));
1013 assert!(!config.is_excluded("/health"));
1014 assert!(!config.is_excluded("/api"));
1015 }
1016
1017 #[test]
1018 fn test_log_config_is_excluded_empty_list() {
1019 let config = LogConfig::default();
1020 assert!(!config.is_excluded("/any"));
1021 }
1022
1023 #[test]
1024 fn test_log_config_is_excluded_multiple_entries() {
1025 let config = LogConfig::default()
1026 .with_exclude_paths(vec!["/health".to_string(), "/metrics/*".to_string()]);
1027 assert!(config.is_excluded("/health"));
1028 assert!(config.is_excluded("/metrics/prometheus"));
1029 assert!(!config.is_excluded("/api"));
1030 }
1031
1032 #[tokio::test]
1037 async fn test_log_middleware_returns_response_unchanged() {
1038 let app = build_app();
1040 let resp = app.oneshot(make_request("GET", "/body")).await.unwrap();
1041 let body = read_body(resp).await;
1042 assert_eq!(body, "hello");
1043 }
1044
1045 #[tokio::test]
1046 async fn test_log_middleware_returns_correct_status() {
1047 let app = build_app();
1048 let resp = app.oneshot(make_request("GET", "/ok")).await.unwrap();
1049 assert_eq!(resp.status(), StatusCode::OK);
1050 }
1051
1052 #[tokio::test]
1053 async fn test_log_middleware_injects_request_id() {
1054 let app = Router::new()
1056 .route(
1057 "/",
1058 axum::routing::get(|req: Request| async move {
1059 let request_id = req.extensions().get::<RequestId>().unwrap();
1060 format!("request_id:{}", request_id.to_hex())
1061 }),
1062 )
1063 .layer(axum::middleware::from_fn(log_middleware));
1064
1065 let resp = app.oneshot(make_request("GET", "/")).await.unwrap();
1066 assert_eq!(resp.status(), StatusCode::OK);
1067 let body = read_body(resp).await;
1068 assert!(body.starts_with("request_id:"));
1069 let hex = body.strip_prefix("request_id:").unwrap();
1071 assert!(hex.len() >= 16);
1072 }
1073
1074 #[tokio::test]
1075 async fn test_log_middleware_generates_unique_request_ids() {
1076 let app = Router::new()
1078 .route(
1079 "/",
1080 axum::routing::get(|req: Request| async move {
1081 let request_id = req.extensions().get::<RequestId>().unwrap();
1082 request_id.to_hex()
1083 }),
1084 )
1085 .layer(axum::middleware::from_fn(log_middleware));
1086
1087 let resp1 = app.clone().oneshot(make_request("GET", "/")).await.unwrap();
1088 let hex1 = read_body(resp1).await;
1089
1090 let resp2 = app.oneshot(make_request("GET", "/")).await.unwrap();
1091 let hex2 = read_body(resp2).await;
1092
1093 assert_ne!(hex1, hex2);
1094 }
1095
1096 #[tokio::test]
1097 async fn test_log_middleware_preserves_existing_request_id() {
1098 let existing_id = RequestId {
1100 timestamp_secs: 0xDEADBEEF,
1101 counter: 0x12345678,
1102 };
1103 let app = Router::new()
1104 .route(
1105 "/",
1106 axum::routing::get(|req: Request| async move {
1107 let request_id = req.extensions().get::<RequestId>().unwrap();
1108 request_id.to_hex()
1109 }),
1110 )
1111 .layer(axum::middleware::from_fn(log_middleware))
1112 .layer(
1113 tower::ServiceBuilder::new().layer(tower::layer::layer_fn(move |service| {
1114 tower::util::MapRequest::new(service, move |mut req: Request| {
1115 req.extensions_mut().insert(existing_id);
1116 req
1117 })
1118 })),
1119 );
1120
1121 let resp = app.oneshot(make_request("GET", "/")).await.unwrap();
1122 let body = read_body(resp).await;
1123 assert_eq!(body, "deadbeef12345678");
1124 }
1125
1126 #[tokio::test]
1127 async fn test_log_middleware_records_2xx_status() {
1128 let app = build_app();
1130 let resp = app.oneshot(make_request("GET", "/ok")).await.unwrap();
1131 assert_eq!(resp.status(), StatusCode::OK);
1132 }
1133
1134 #[tokio::test]
1135 async fn test_log_middleware_records_4xx_status() {
1136 let app = build_app();
1137 let resp = app.oneshot(make_request("GET", "/notfound")).await.unwrap();
1138 assert_eq!(resp.status(), StatusCode::NOT_FOUND);
1139 }
1140
1141 #[tokio::test]
1142 async fn test_log_middleware_records_5xx_status() {
1143 let app = build_app();
1144 let resp = app.oneshot(make_request("GET", "/error")).await.unwrap();
1145 assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
1146 }
1147
1148 #[tokio::test]
1149 async fn test_log_middleware_with_config_excludes_path() {
1150 let config = LogConfig::default().with_exclude_paths(vec!["/health".to_string()]);
1152 let app = Router::new()
1153 .route("/health", axum::routing::get(|| async { "healthy" }))
1154 .layer(axum::middleware::from_fn_with_state(
1155 config,
1156 log_middleware_with_config,
1157 ));
1158
1159 let resp = app.oneshot(make_request("GET", "/health")).await.unwrap();
1160 assert_eq!(resp.status(), StatusCode::OK);
1161 let body = read_body(resp).await;
1162 assert_eq!(body, "healthy");
1163 }
1164
1165 #[tokio::test]
1166 async fn test_log_middleware_with_config_wildcard_exclude() {
1167 let config = LogConfig::default().with_exclude_paths(vec!["/metrics/*".to_string()]);
1169 let app = Router::new()
1170 .route(
1171 "/metrics/prometheus",
1172 axum::routing::get(|| async { "metrics" }),
1173 )
1174 .layer(axum::middleware::from_fn_with_state(
1175 config,
1176 log_middleware_with_config,
1177 ));
1178
1179 let resp = app
1180 .oneshot(make_request("GET", "/metrics/prometheus"))
1181 .await
1182 .unwrap();
1183 assert_eq!(resp.status(), StatusCode::OK);
1184 }
1185
1186 #[tokio::test]
1187 async fn test_log_middleware_preserves_method_and_uri() {
1188 let app = build_app();
1191 let resp = app.oneshot(make_request("GET", "/ok")).await.unwrap();
1192 assert_eq!(resp.status(), StatusCode::OK);
1193 }
1194
1195 #[tokio::test]
1196 async fn test_log_middleware_duration_is_non_negative() {
1197 let app = build_app();
1199 let start = std::time::Instant::now();
1200 let resp = app.oneshot(make_request("GET", "/ok")).await.unwrap();
1201 let elapsed = start.elapsed();
1202 assert!(resp.status().is_success());
1203 assert!(elapsed.as_millis() < 5000); }
1206
1207 #[tokio::test]
1208 async fn test_log_middleware_handles_post_request() {
1209 let app = Router::new()
1210 .route(
1211 "/submit",
1212 axum::routing::post(|| async { axum::http::StatusCode::CREATED }),
1213 )
1214 .layer(axum::middleware::from_fn(log_middleware));
1215
1216 let req = Request::builder()
1217 .method("POST")
1218 .uri("/submit")
1219 .body(Body::empty())
1220 .unwrap();
1221 let resp = app.oneshot(req).await.unwrap();
1222 assert_eq!(resp.status(), StatusCode::CREATED);
1223 }
1224
1225 #[tokio::test]
1226 async fn test_log_middleware_chains_with_other_middleware() {
1227 async fn add_header_middleware(req: Request, next: Next) -> Response {
1229 let mut resp = next.run(req).await;
1230 resp.headers_mut()
1231 .insert("X-Custom", "value".parse().unwrap());
1232 resp
1233 }
1234
1235 let app = Router::new()
1236 .route("/", axum::routing::get(|| async { "ok" }))
1237 .layer(axum::middleware::from_fn(add_header_middleware))
1238 .layer(axum::middleware::from_fn(log_middleware));
1239
1240 let resp = app.oneshot(make_request("GET", "/")).await.unwrap();
1241 assert_eq!(resp.status(), StatusCode::OK);
1242 assert_eq!(resp.headers().get("X-Custom").unwrap(), "value");
1243 }
1244
1245 #[test]
1250 fn test_php_apart_level_alignment() {
1251 assert_eq!(log_level_for_status(200), LogLevel::Info);
1255 assert_eq!(log_level_for_status(404), LogLevel::Warn);
1256 assert_eq!(log_level_for_status(500), LogLevel::Error);
1257 }
1258
1259 #[test]
1260 fn test_php_think_logger_level_alignment() {
1261 let levels = [
1264 LogLevel::Debug,
1265 LogLevel::Info,
1266 LogLevel::Warn,
1267 LogLevel::Error,
1268 ];
1269 assert_eq!(levels.len(), 4);
1270 }
1271
1272 #[test]
1273 fn test_request_id_format_aligns_with_w3c_span_id_length() {
1274 let id = RequestId {
1277 timestamp_secs: 0x12345678,
1278 counter: 0x9ABCDEF0,
1279 };
1280 assert_eq!(id.to_hex().len(), 16);
1281 }
1282}