Skip to main content

sz_rust_core/middleware/
log.rs

1//! Log 中间件 — 请求/响应日志(对齐 PHP `think-logger`)
2//!
3//! sz-rust 自研中间件,PHP 端无全局 Log 中间件(PHP `app/middleware.php` 仅含
4//! `SessionInit` + `AllowCrossDomain`)。本模块在 [`crate::middleware::order::DEFAULT_ORDER`]
5//! 中位于第 3 位(`Trace` → `Cors` → **`Log`** → `RateLimit` → `Auth`)。
6//!
7//! ## 行为
8//!
9//! 1. **入口**:生成 `RequestId`(如果 extensions 中没有,则新生成),注入 extensions
10//! 2. **记录起始时间**:`std::time::Instant::now()`
11//! 3. **调用 `next.run(req)`**:传递请求给下游
12//! 4. **出口**:根据响应状态码记录日志
13//!    - 2xx/3xx → `tracing::info!`
14//!    - 4xx → `tracing::warn!`(对齐 PHP `apart_level=['error','sql']` 的级别分离思想)
15//!    - 5xx → `tracing::error!`
16//!
17//! ## 日志字段
18//!
19//! | 字段 | 来源 | 说明 |
20//! |------|------|------|
21//! | `request_id` | `generate_request_id()` | 全局唯一计数器 + 时间戳,16 字符 hex |
22//! | `method` | `Request::method()` | HTTP 方法 |
23//! | `uri` | `Request::uri().path()` | 请求路径(不含查询字符串) |
24//! | `status` | `Response::status().as_u16()` | HTTP 状态码 |
25//! | `duration_ms` | `Instant::elapsed()` | 请求耗时(毫秒) |
26//!
27//! ## PHP 对齐
28//!
29//! PHP 端无 Log 中间件,业务代码通过 `Log::info()` 等主动调用。
30//! sz-rust 的 Log 中间件是自研增强,提供:
31//! - 请求生命周期自动日志(无需业务代码手动调用)
32//! - 请求 ID 追踪(贯穿整个请求链路)
33//! - 响应状态码分级日志(4xx Warn / 5xx Error)
34//!
35//! 日志级别对齐 think-logger 的 4 级(debug/info/warn/error),
36//! `apart_level` 思想对齐 PHP `config/log.php` 的 `['error','sql']` 独立文件配置。
37//!
38//! ## 用法
39//!
40//! ```ignore
41//! use sz_rust_core::middleware::log::log_middleware;
42//! use axum::Router;
43//!
44//! let app: Router = Router::new()
45//!     .route("/", axum::routing::get(|| async { "ok" }))
46//!     .layer(axum::middleware::from_fn(log_middleware));
47//! ```
48
49use axum::extract::Request;
50use axum::middleware::Next;
51use axum::response::Response;
52use std::sync::atomic::{AtomicU64, Ordering};
53use std::time::Instant;
54
55use crate::log::LogLevel;
56
57/// 请求 ID(注入到 request extensions,供下游 handler 和日志使用)
58///
59/// 生成方式:全局 `AtomicU64` 计数器 + 当前时间戳,保证进程内唯一。
60/// 格式:16 字符 hex(`{timestamp_secs:08x}{counter:08x}`)。
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub struct RequestId {
63    /// 时间戳部分(UNIX 秒)
64    timestamp_secs: u64,
65    /// 计数器部分(进程内递增)
66    counter: u64,
67}
68
69impl RequestId {
70    /// 返回 16 字符 hex 字符串
71    ///
72    /// 格式:`{timestamp_secs:08x}{counter:08x}`(对齐 W3C traceparent 的 16 字符 span_id 长度)。
73    pub fn to_hex(&self) -> String {
74        format!("{:08x}{:08x}", self.timestamp_secs, self.counter)
75    }
76
77    /// 返回时间戳部分
78    pub fn timestamp_secs(&self) -> u64 {
79        self.timestamp_secs
80    }
81
82    /// 返回计数器部分
83    pub fn counter(&self) -> u64 {
84        self.counter
85    }
86}
87
88impl std::fmt::Display for RequestId {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        f.write_str(&self.to_hex())
91    }
92}
93
94/// 全局 request_id 计数器(进程内递增)
95static REQUEST_ID_COUNTER: AtomicU64 = AtomicU64::new(0);
96
97/// 生成新的 `RequestId`
98///
99/// 使用全局 `AtomicU64` 计数器 + 当前 UNIX 时间戳,保证进程内唯一。
100/// 多线程安全(`fetch_add` 是原子操作)。
101pub fn generate_request_id() -> RequestId {
102    let counter = REQUEST_ID_COUNTER.fetch_add(1, Ordering::Relaxed);
103    let timestamp_secs = std::time::SystemTime::now()
104        .duration_since(std::time::UNIX_EPOCH)
105        .map(|d| d.as_secs())
106        .unwrap_or(0);
107    RequestId {
108        timestamp_secs,
109        counter,
110    }
111}
112
113/// Log 中间件配置
114#[derive(Debug, Clone, Default)]
115pub struct LogConfig {
116    /// 排除路径(不记录日志,对齐 PHP 端白名单思想)
117    ///
118    /// 支持精确匹配(如 `/health`)和通配符匹配(如 `/health/*`)。
119    pub exclude_paths: Vec<String>,
120}
121
122impl LogConfig {
123    /// 创建带排除路径的配置
124    pub fn with_exclude_paths(mut self, paths: Vec<String>) -> Self {
125        self.exclude_paths = paths;
126        self
127    }
128
129    /// 判断路径是否被排除
130    ///
131    /// 支持精确匹配和 `*` 通配符匹配(复用 [`crate::middleware::auth::is_route_allowed`] 的逻辑)。
132    pub fn is_excluded(&self, path: &str) -> bool {
133        crate::middleware::auth::is_route_allowed(path, &self.exclude_paths)
134    }
135}
136
137/// 根据响应状态码返回日志级别
138///
139/// 对齐 PHP `config/log.php` 的 `apart_level=['error','sql']` 思想:
140/// - 2xx/3xx → `Info`(成功请求)
141/// - 4xx → `Warn`(客户端错误)
142/// - 5xx → `Error`(服务端错误)
143///
144/// 其他状态码(如 1xx)默认为 `Info`。
145pub fn log_level_for_status(status: u16) -> LogLevel {
146    match status {
147        400..=499 => LogLevel::Warn,
148        500..=599 => LogLevel::Error,
149        _ => LogLevel::Info,
150    }
151}
152
153/// 格式化请求日志消息
154///
155/// 输出格式:`request_id=<hex> method=<METHOD> uri=<path> status=<code> duration_ms=<ms>`
156///
157/// 此函数主要用于测试可验证的纯函数,中间件实际输出通过 `tracing` 宏的结构化字段实现。
158pub fn format_request_log(
159    method: &str,
160    uri: &str,
161    status: u16,
162    duration_ms: u64,
163    request_id: &RequestId,
164) -> String {
165    format!(
166        "request_id={} method={} uri={} status={} duration_ms={}",
167        request_id.to_hex(),
168        method,
169        uri,
170        status,
171        duration_ms
172    )
173}
174
175/// Log 中间件 — 请求/响应日志
176///
177/// ## 校验流程
178///
179/// 1. **提取请求信息**:method, uri(在 `req` 被消费之前)
180/// 2. **生成 RequestId**:如果 extensions 中没有,则新生成
181/// 3. **记录起始时间**:`Instant::now()`
182/// 4. **注入 RequestId**:插入 request extensions
183/// 5. **调用 `next.run(req)`**:传递请求给下游
184/// 6. **计算耗时**:`start.elapsed()`
185/// 7. **记录日志**:根据状态码选择级别,输出结构化日志
186///
187/// ## 排除路径
188///
189/// 如果请求路径在 [`LogConfig::exclude_paths`] 中,则不记录日志(但仍注入 RequestId)。
190///
191/// ## 用法
192///
193/// ```ignore
194/// use sz_rust_core::middleware::log::{log_middleware, LogConfig};
195/// use axum::Router;
196///
197/// let config = LogConfig::default();
198/// let app: Router = Router::new()
199///     .route("/", axum::routing::get(|| async { "ok" }))
200///     .layer(axum::middleware::from_fn_with_state(config, log_middleware_with_config));
201/// ```
202pub async fn log_middleware(req: Request, next: Next) -> Response {
203    log_middleware_inner(req, next, &LogConfig::default()).await
204}
205
206/// 带配置的 Log 中间件
207pub async fn log_middleware_with_config(
208    axum::extract::State(config): axum::extract::State<LogConfig>,
209    req: Request,
210    next: Next,
211) -> Response {
212    log_middleware_inner(req, next, &config).await
213}
214
215async fn log_middleware_inner(req: Request, next: Next, config: &LogConfig) -> Response {
216    // 1. 提取请求信息(在 req 被消费之前)
217    let method = req.method().clone();
218    let uri = req.uri().path().to_string();
219
220    // 2. 生成 RequestId(如果 extensions 中没有,则新生成)
221    let request_id = req
222        .extensions()
223        .get::<RequestId>()
224        .copied()
225        .unwrap_or_else(generate_request_id);
226
227    // 3. 记录起始时间
228    let start = Instant::now();
229
230    // 4. 注入 RequestId 到 extensions
231    let mut req = req;
232    req.extensions_mut().insert(request_id);
233
234    // 5. 调用 next
235    let response = next.run(req).await;
236
237    // 6. 计算耗时
238    let duration_ms = start.elapsed().as_millis() as u64;
239
240    // 7. 记录日志(排除路径不记录)
241    if !config.is_excluded(&uri) {
242        let status = response.status().as_u16();
243        let level = log_level_for_status(status);
244        let request_id_hex = request_id.to_hex();
245
246        match level {
247            LogLevel::Debug => tracing::debug!(
248                request_id = %request_id_hex,
249                method = %method,
250                uri = %uri,
251                status = status,
252                duration_ms = duration_ms,
253                "request completed"
254            ),
255            LogLevel::Info => tracing::info!(
256                request_id = %request_id_hex,
257                method = %method,
258                uri = %uri,
259                status = status,
260                duration_ms = duration_ms,
261                "request completed"
262            ),
263            LogLevel::Warn => tracing::warn!(
264                request_id = %request_id_hex,
265                method = %method,
266                uri = %uri,
267                status = status,
268                duration_ms = duration_ms,
269                "request completed"
270            ),
271            LogLevel::Error => tracing::error!(
272                request_id = %request_id_hex,
273                method = %method,
274                uri = %uri,
275                status = status,
276                duration_ms = duration_ms,
277                "request completed"
278            ),
279        }
280    }
281
282    response
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288    use axum::body::Body;
289    use axum::http::StatusCode;
290    use axum::Router;
291    use http_body_util::BodyExt;
292    use tower::ServiceExt;
293
294    // ====================================================================
295    // 辅助函数
296    // ====================================================================
297
298    async fn read_body(resp: Response) -> String {
299        let bytes = resp.into_body().collect().await.unwrap().to_bytes();
300        String::from_utf8(bytes.to_vec()).unwrap()
301    }
302
303    fn make_request(method: &str, uri: &str) -> Request {
304        Request::builder()
305            .method(method)
306            .uri(uri)
307            .body(Body::empty())
308            .unwrap()
309    }
310
311    /// 构建测试用 Router
312    fn build_app() -> Router {
313        Router::new()
314            .route(
315                "/ok",
316                axum::routing::get(|| async { axum::http::StatusCode::OK }),
317            )
318            .route(
319                "/notfound",
320                axum::routing::get(|| async { axum::http::StatusCode::NOT_FOUND }),
321            )
322            .route(
323                "/error",
324                axum::routing::get(|| async { axum::http::StatusCode::INTERNAL_SERVER_ERROR }),
325            )
326            .route("/body", axum::routing::get(|| async { "hello" }))
327            .layer(axum::middleware::from_fn(log_middleware))
328    }
329
330    // ====================================================================
331    // RequestId 单元测试
332    // ====================================================================
333
334    #[test]
335    fn test_request_id_to_hex_is_16_chars() {
336        let id = RequestId {
337            timestamp_secs: 0x12345678,
338            counter: 0x9ABCDEF0,
339        };
340        let hex = id.to_hex();
341        assert_eq!(hex.len(), 16);
342        assert_eq!(hex, "123456789abcdef0");
343    }
344
345    #[test]
346    fn test_request_id_to_hex_zero() {
347        let id = RequestId {
348            timestamp_secs: 0,
349            counter: 0,
350        };
351        assert_eq!(id.to_hex(), "0000000000000000");
352    }
353
354    #[test]
355    fn test_request_id_to_hex_max() {
356        let id = RequestId {
357            timestamp_secs: u64::MAX,
358            counter: u64::MAX,
359        };
360        // u64::MAX = 0xFFFFFFFFFFFFFFFF,但 format!("{:08x}", u64::MAX) 会输出 16 字符
361        let hex = id.to_hex();
362        assert_eq!(hex.len(), 32); // 每部分 16 字符,总共 32 字符
363    }
364
365    #[test]
366    fn test_request_id_display_matches_to_hex() {
367        let id = RequestId {
368            timestamp_secs: 0x12345678,
369            counter: 0x9ABCDEF0,
370        };
371        assert_eq!(format!("{}", id), id.to_hex());
372    }
373
374    #[test]
375    fn test_request_id_accessors() {
376        let id = RequestId {
377            timestamp_secs: 100,
378            counter: 200,
379        };
380        assert_eq!(id.timestamp_secs(), 100);
381        assert_eq!(id.counter(), 200);
382    }
383
384    #[test]
385    fn test_request_id_equality() {
386        let id1 = RequestId {
387            timestamp_secs: 1,
388            counter: 2,
389        };
390        let id2 = RequestId {
391            timestamp_secs: 1,
392            counter: 2,
393        };
394        let id3 = RequestId {
395            timestamp_secs: 1,
396            counter: 3,
397        };
398        assert_eq!(id1, id2);
399        assert_ne!(id1, id3);
400    }
401
402    // ====================================================================
403    // generate_request_id 单元测试
404    // ====================================================================
405
406    #[test]
407    fn test_generate_request_id_returns_unique() {
408        let id1 = generate_request_id();
409        let id2 = generate_request_id();
410        // 计数器递增,保证唯一
411        assert_ne!(id1.counter(), id2.counter());
412        assert_eq!(id2.counter(), id1.counter() + 1);
413    }
414
415    #[test]
416    fn test_generate_request_id_hex_is_16_chars() {
417        let id = generate_request_id();
418        let hex = id.to_hex();
419        // 注意:如果 timestamp_secs 或 counter 超过 u32::MAX,hex 会超过 16 字符
420        // 但在正常情况下(timestamp < 2106 年,counter < 40 亿次),hex 是 16 字符
421        assert!(hex.len() >= 16);
422    }
423
424    // ====================================================================
425    // log_level_for_status 单元测试
426    // ====================================================================
427
428    #[test]
429    fn test_log_level_for_2xx_returns_info() {
430        assert_eq!(log_level_for_status(200), LogLevel::Info);
431        assert_eq!(log_level_for_status(201), LogLevel::Info);
432        assert_eq!(log_level_for_status(204), LogLevel::Info);
433    }
434
435    #[test]
436    fn test_log_level_for_3xx_returns_info() {
437        assert_eq!(log_level_for_status(301), LogLevel::Info);
438        assert_eq!(log_level_for_status(302), LogLevel::Info);
439        assert_eq!(log_level_for_status(304), LogLevel::Info);
440    }
441
442    #[test]
443    fn test_log_level_for_4xx_returns_warn() {
444        assert_eq!(log_level_for_status(400), LogLevel::Warn);
445        assert_eq!(log_level_for_status(401), LogLevel::Warn);
446        assert_eq!(log_level_for_status(403), LogLevel::Warn);
447        assert_eq!(log_level_for_status(404), LogLevel::Warn);
448        assert_eq!(log_level_for_status(422), LogLevel::Warn);
449        assert_eq!(log_level_for_status(499), LogLevel::Warn);
450    }
451
452    #[test]
453    fn test_log_level_for_5xx_returns_error() {
454        assert_eq!(log_level_for_status(500), LogLevel::Error);
455        assert_eq!(log_level_for_status(501), LogLevel::Error);
456        assert_eq!(log_level_for_status(502), LogLevel::Error);
457        assert_eq!(log_level_for_status(503), LogLevel::Error);
458        assert_eq!(log_level_for_status(599), LogLevel::Error);
459    }
460
461    #[test]
462    fn test_log_level_for_1xx_returns_info() {
463        // 1xx 信息响应默认为 Info
464        assert_eq!(log_level_for_status(100), LogLevel::Info);
465        assert_eq!(log_level_for_status(101), LogLevel::Info);
466    }
467
468    #[test]
469    fn test_log_level_for_boundary() {
470        // 边界测试:399 → Info,400 → Warn,499 → Warn,500 → Error,599 → Error,600 → Info
471        assert_eq!(log_level_for_status(399), LogLevel::Info);
472        assert_eq!(log_level_for_status(400), LogLevel::Warn);
473        assert_eq!(log_level_for_status(499), LogLevel::Warn);
474        assert_eq!(log_level_for_status(500), LogLevel::Error);
475        assert_eq!(log_level_for_status(599), LogLevel::Error);
476        assert_eq!(log_level_for_status(600), LogLevel::Info);
477    }
478
479    // ====================================================================
480    // format_request_log 单元测试
481    // ====================================================================
482
483    #[test]
484    fn test_format_request_log_basic() {
485        let request_id = RequestId {
486            timestamp_secs: 0x12345678,
487            counter: 0x9ABCDEF0,
488        };
489        let msg = format_request_log("GET", "/api/users", 200, 15, &request_id);
490        assert_eq!(
491            msg,
492            "request_id=123456789abcdef0 method=GET uri=/api/users status=200 duration_ms=15"
493        );
494    }
495
496    #[test]
497    fn test_format_request_log_post_method() {
498        let request_id = RequestId {
499            timestamp_secs: 0,
500            counter: 1,
501        };
502        let msg = format_request_log("POST", "/api/orders", 201, 42, &request_id);
503        assert_eq!(
504            msg,
505            "request_id=0000000000000001 method=POST uri=/api/orders status=201 duration_ms=42"
506        );
507    }
508
509    #[test]
510    fn test_format_request_log_error_status() {
511        let request_id = RequestId {
512            timestamp_secs: 0,
513            counter: 0,
514        };
515        let msg = format_request_log("GET", "/missing", 404, 5, &request_id);
516        assert_eq!(
517            msg,
518            "request_id=0000000000000000 method=GET uri=/missing status=404 duration_ms=5"
519        );
520    }
521
522    #[test]
523    fn test_format_request_log_with_query_string_in_uri() {
524        // uri 应该是原始 path(含查询字符串),由调用方决定是否截取
525        let request_id = RequestId {
526            timestamp_secs: 0,
527            counter: 0,
528        };
529        let msg = format_request_log("GET", "/api?foo=bar", 200, 1, &request_id);
530        assert!(msg.contains("uri=/api?foo=bar"));
531    }
532
533    // ====================================================================
534    // LogConfig 单元测试
535    // ====================================================================
536
537    #[test]
538    fn test_log_config_default_empty_exclude_paths() {
539        let config = LogConfig::default();
540        assert!(config.exclude_paths.is_empty());
541    }
542
543    #[test]
544    fn test_log_config_with_exclude_paths() {
545        let config = LogConfig::default().with_exclude_paths(vec!["/health".to_string()]);
546        assert_eq!(config.exclude_paths, vec!["/health".to_string()]);
547    }
548
549    #[test]
550    fn test_log_config_is_excluded_exact_match() {
551        let config = LogConfig::default().with_exclude_paths(vec!["/health".to_string()]);
552        assert!(config.is_excluded("/health"));
553        assert!(!config.is_excluded("/health/detail"));
554        assert!(!config.is_excluded("/api"));
555    }
556
557    #[test]
558    fn test_log_config_is_excluded_wildcard_match() {
559        let config = LogConfig::default().with_exclude_paths(vec!["/health/*".to_string()]);
560        assert!(config.is_excluded("/health/check"));
561        assert!(config.is_excluded("/health/deep/nested"));
562        assert!(!config.is_excluded("/health"));
563        assert!(!config.is_excluded("/api"));
564    }
565
566    #[test]
567    fn test_log_config_is_excluded_empty_list() {
568        let config = LogConfig::default();
569        assert!(!config.is_excluded("/any"));
570    }
571
572    #[test]
573    fn test_log_config_is_excluded_multiple_entries() {
574        let config = LogConfig::default()
575            .with_exclude_paths(vec!["/health".to_string(), "/metrics/*".to_string()]);
576        assert!(config.is_excluded("/health"));
577        assert!(config.is_excluded("/metrics/prometheus"));
578        assert!(!config.is_excluded("/api"));
579    }
580
581    // ====================================================================
582    // log_middleware 集成测试
583    // ====================================================================
584
585    #[tokio::test]
586    async fn test_log_middleware_returns_response_unchanged() {
587        // 验证中间件不修改响应体
588        let app = build_app();
589        let resp = app.oneshot(make_request("GET", "/body")).await.unwrap();
590        let body = read_body(resp).await;
591        assert_eq!(body, "hello");
592    }
593
594    #[tokio::test]
595    async fn test_log_middleware_returns_correct_status() {
596        let app = build_app();
597        let resp = app.oneshot(make_request("GET", "/ok")).await.unwrap();
598        assert_eq!(resp.status(), StatusCode::OK);
599    }
600
601    #[tokio::test]
602    async fn test_log_middleware_injects_request_id() {
603        // 验证 request_id 被注入 extensions
604        let app = Router::new()
605            .route(
606                "/",
607                axum::routing::get(|req: Request| async move {
608                    let request_id = req.extensions().get::<RequestId>().unwrap();
609                    format!("request_id:{}", request_id.to_hex())
610                }),
611            )
612            .layer(axum::middleware::from_fn(log_middleware));
613
614        let resp = app.oneshot(make_request("GET", "/")).await.unwrap();
615        assert_eq!(resp.status(), StatusCode::OK);
616        let body = read_body(resp).await;
617        assert!(body.starts_with("request_id:"));
618        // 验证 hex 长度至少 16 字符
619        let hex = body.strip_prefix("request_id:").unwrap();
620        assert!(hex.len() >= 16);
621    }
622
623    #[tokio::test]
624    async fn test_log_middleware_generates_unique_request_ids() {
625        // 验证多个请求生成不同的 request_id
626        let app = Router::new()
627            .route(
628                "/",
629                axum::routing::get(|req: Request| async move {
630                    let request_id = req.extensions().get::<RequestId>().unwrap();
631                    request_id.to_hex()
632                }),
633            )
634            .layer(axum::middleware::from_fn(log_middleware));
635
636        let resp1 = app.clone().oneshot(make_request("GET", "/")).await.unwrap();
637        let hex1 = read_body(resp1).await;
638
639        let resp2 = app.oneshot(make_request("GET", "/")).await.unwrap();
640        let hex2 = read_body(resp2).await;
641
642        assert_ne!(hex1, hex2);
643    }
644
645    #[tokio::test]
646    async fn test_log_middleware_preserves_existing_request_id() {
647        // 验证已存在的 request_id 不被覆盖
648        let existing_id = RequestId {
649            timestamp_secs: 0xDEADBEEF,
650            counter: 0x12345678,
651        };
652        let app = Router::new()
653            .route(
654                "/",
655                axum::routing::get(|req: Request| async move {
656                    let request_id = req.extensions().get::<RequestId>().unwrap();
657                    request_id.to_hex()
658                }),
659            )
660            .layer(axum::middleware::from_fn(log_middleware))
661            .layer(
662                tower::ServiceBuilder::new().layer(tower::layer::layer_fn(move |service| {
663                    tower::util::MapRequest::new(service, move |mut req: Request| {
664                        req.extensions_mut().insert(existing_id);
665                        req
666                    })
667                })),
668            );
669
670        let resp = app.oneshot(make_request("GET", "/")).await.unwrap();
671        let body = read_body(resp).await;
672        assert_eq!(body, "deadbeef12345678");
673    }
674
675    #[tokio::test]
676    async fn test_log_middleware_records_2xx_status() {
677        // 验证 2xx 响应正常处理(日志级别由 log_level_for_status 决定)
678        let app = build_app();
679        let resp = app.oneshot(make_request("GET", "/ok")).await.unwrap();
680        assert_eq!(resp.status(), StatusCode::OK);
681    }
682
683    #[tokio::test]
684    async fn test_log_middleware_records_4xx_status() {
685        let app = build_app();
686        let resp = app.oneshot(make_request("GET", "/notfound")).await.unwrap();
687        assert_eq!(resp.status(), StatusCode::NOT_FOUND);
688    }
689
690    #[tokio::test]
691    async fn test_log_middleware_records_5xx_status() {
692        let app = build_app();
693        let resp = app.oneshot(make_request("GET", "/error")).await.unwrap();
694        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
695    }
696
697    #[tokio::test]
698    async fn test_log_middleware_with_config_excludes_path() {
699        // 验证排除路径不记录日志(但仍注入 request_id)
700        let config = LogConfig::default().with_exclude_paths(vec!["/health".to_string()]);
701        let app = Router::new()
702            .route("/health", axum::routing::get(|| async { "healthy" }))
703            .layer(axum::middleware::from_fn_with_state(
704                config,
705                log_middleware_with_config,
706            ));
707
708        let resp = app.oneshot(make_request("GET", "/health")).await.unwrap();
709        assert_eq!(resp.status(), StatusCode::OK);
710        let body = read_body(resp).await;
711        assert_eq!(body, "healthy");
712    }
713
714    #[tokio::test]
715    async fn test_log_middleware_with_config_wildcard_exclude() {
716        // 验证通配符排除路径
717        let config = LogConfig::default().with_exclude_paths(vec!["/metrics/*".to_string()]);
718        let app = Router::new()
719            .route(
720                "/metrics/prometheus",
721                axum::routing::get(|| async { "metrics" }),
722            )
723            .layer(axum::middleware::from_fn_with_state(
724                config,
725                log_middleware_with_config,
726            ));
727
728        let resp = app
729            .oneshot(make_request("GET", "/metrics/prometheus"))
730            .await
731            .unwrap();
732        assert_eq!(resp.status(), StatusCode::OK);
733    }
734
735    #[tokio::test]
736    async fn test_log_middleware_preserves_method_and_uri() {
737        // 验证 method 和 uri 被正确提取(通过日志消息格式验证)
738        // 由于 tracing 宏输出在测试中难以捕获,这里验证中间件不破坏请求
739        let app = build_app();
740        let resp = app.oneshot(make_request("GET", "/ok")).await.unwrap();
741        assert_eq!(resp.status(), StatusCode::OK);
742    }
743
744    #[tokio::test]
745    async fn test_log_middleware_duration_is_non_negative() {
746        // 验证 duration_ms 是非负的(通过响应正常返回间接验证)
747        let app = build_app();
748        let start = std::time::Instant::now();
749        let resp = app.oneshot(make_request("GET", "/ok")).await.unwrap();
750        let elapsed = start.elapsed();
751        assert!(resp.status().is_success());
752        // 中间件内部记录的 duration_ms 应该 <= 测试外部的 elapsed
753        assert!(elapsed.as_millis() < 5000); // 5 秒上限(防止死循环)
754    }
755
756    #[tokio::test]
757    async fn test_log_middleware_handles_post_request() {
758        let app = Router::new()
759            .route(
760                "/submit",
761                axum::routing::post(|| async { axum::http::StatusCode::CREATED }),
762            )
763            .layer(axum::middleware::from_fn(log_middleware));
764
765        let req = Request::builder()
766            .method("POST")
767            .uri("/submit")
768            .body(Body::empty())
769            .unwrap();
770        let resp = app.oneshot(req).await.unwrap();
771        assert_eq!(resp.status(), StatusCode::CREATED);
772    }
773
774    #[tokio::test]
775    async fn test_log_middleware_chains_with_other_middleware() {
776        // 验证 Log 中间件与其他中间件链式调用
777        async fn add_header_middleware(req: Request, next: Next) -> Response {
778            let mut resp = next.run(req).await;
779            resp.headers_mut()
780                .insert("X-Custom", "value".parse().unwrap());
781            resp
782        }
783
784        let app = Router::new()
785            .route("/", axum::routing::get(|| async { "ok" }))
786            .layer(axum::middleware::from_fn(add_header_middleware))
787            .layer(axum::middleware::from_fn(log_middleware));
788
789        let resp = app.oneshot(make_request("GET", "/")).await.unwrap();
790        assert_eq!(resp.status(), StatusCode::OK);
791        assert_eq!(resp.headers().get("X-Custom").unwrap(), "value");
792    }
793
794    // ====================================================================
795    // PHP 行为对齐验证
796    // ====================================================================
797
798    #[test]
799    fn test_php_apart_level_alignment() {
800        // 对齐 PHP `config/log.php` 的 `apart_level=['error','sql']` 思想:
801        // 4xx → Warn(客户端错误,类似 PHP warning)
802        // 5xx → Error(服务端错误,对齐 PHP error 独立文件)
803        assert_eq!(log_level_for_status(200), LogLevel::Info);
804        assert_eq!(log_level_for_status(404), LogLevel::Warn);
805        assert_eq!(log_level_for_status(500), LogLevel::Error);
806    }
807
808    #[test]
809    fn test_php_think_logger_level_alignment() {
810        // 对齐 PHP think-logger 的 4 级日志(debug/info/warn/error)
811        // sz-rust 的 LogLevel 也是 4 级,一一对应
812        let levels = [
813            LogLevel::Debug,
814            LogLevel::Info,
815            LogLevel::Warn,
816            LogLevel::Error,
817        ];
818        assert_eq!(levels.len(), 4);
819    }
820
821    #[test]
822    fn test_request_id_format_aligns_with_w3c_span_id_length() {
823        // 对齐 W3C traceparent 的 span_id 长度(16 字符 hex)
824        // 便于未来 Trace 中间件实现时与 trace_id 格式兼容
825        let id = RequestId {
826            timestamp_secs: 0x12345678,
827            counter: 0x9ABCDEF0,
828        };
829        assert_eq!(id.to_hex().len(), 16);
830    }
831}