From c54f4214dfa1055e0315041beb39d4baebc39100 Mon Sep 17 00:00:00 2001 From: shay7sev Date: Wed, 22 Jul 2026 14:40:07 +0800 Subject: [PATCH] feat(observability): suppress successful health logs --- docs/observability-runbook.md | 15 +++++++ server/src/main.rs | 79 +++++++++++++++++++++++++++++++++-- 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/docs/observability-runbook.md b/docs/observability-runbook.md index 3c6c45c..a0127ea 100644 --- a/docs/observability-runbook.md +++ b/docs/observability-runbook.md @@ -63,6 +63,15 @@ curl -fsS http://127.0.0.1:39180/health 仪表盘适合浏览和筛选;需要精确关联请求或比较版本时,打开 `Explore`,数据源选择 `Loki`。 +仪表盘包含两个日志面板: + +- `HTTP Errors`:只显示能够解析出 HTTP `status >= 400` 的结构化日志。 +- `Service Logs (health excluded)`:显示所选服务的普通日志,并默认排除例行 `/health` 请求。 + +API 不记录成功 `/health` 请求的开始和结束事件,以避免 Docker 日志与 Loki 被固定频率的探针日志占满。 +如果 `/health` 返回非 2xx 或请求在中间件中失败,错误日志仍会保留。需要查看改造前的历史健康检查日志时, +在 `Explore` 中使用明确包含 `/health` 的查询,并把时间范围设置到对应部署时间之前。 + ## 常用 LogQL 查看教学反馈的全部日志: @@ -71,6 +80,12 @@ curl -fsS http://127.0.0.1:39180/health {application="teaching-feedback"} ``` +排除改造前已采集的健康检查日志: + +```logql +{application="teaching-feedback", service="api"} != "\"path\":\"/health\"" +``` + 按服务、级别和文本筛选: ```logql diff --git a/server/src/main.rs b/server/src/main.rs index 7b98580..109a022 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -12,7 +12,7 @@ use std::{path::PathBuf, sync::Arc, time::Duration}; use axum::{ Json, Router, extract::{DefaultBodyLimit, MatchedPath}, - http::{HeaderName, HeaderValue, Request}, + http::{HeaderName, HeaderValue, Request, StatusCode}, response::Response, routing::get, }; @@ -55,6 +55,29 @@ impl From<&Config> for LogContext { } } +fn should_log_http_request_started(path: &str) -> bool { + path != "/health" +} + +fn should_log_http_response(is_health_check: bool, status: StatusCode) -> bool { + !is_health_check || !status.is_success() +} + +#[derive(Clone, Copy, Debug)] +struct HealthCheckResponse; + +async fn mark_health_check_response( + request: axum::extract::Request, + next: axum::middleware::Next, +) -> Response { + let is_health_check = request.uri().path() == "/health"; + let mut response = next.run(request).await; + if is_health_check { + response.extensions_mut().insert(HealthCheckResponse); + } + response +} + #[tokio::main] async fn main() -> Result<(), Box> { dotenvy::dotenv().ok(); @@ -161,6 +184,9 @@ fn build_app( make_http_request_span(request, &request_span_context) }) .on_request(move |request: &Request<_>, span: &Span| { + if !should_log_http_request_started(request.uri().path()) { + return; + } tracing::info!( parent: span, service = "api", @@ -174,6 +200,11 @@ fn build_app( }) .on_response(move |response: &Response, latency: Duration, span: &Span| { let status = response.status(); + let is_health_check = + response.extensions().get::().is_some(); + if !should_log_http_response(is_health_check, status) { + return; + } let latency_ms = u64::try_from(latency.as_millis()).unwrap_or(u64::MAX); let request_id = response .headers() @@ -226,6 +257,7 @@ fn build_app( }, ), ) + .layer(axum::middleware::from_fn(mark_health_check_response)) .layer(PropagateRequestIdLayer::x_request_id()), )) } @@ -304,11 +336,17 @@ async fn shutdown_signal(log_context: LogContext) { #[cfg(test)] mod tests { - use axum::{body::Body, http::Request}; + use axum::{ + body::Body, + http::{Request, StatusCode}, + }; use tower::ServiceExt; use uuid::Uuid; - use super::{AppState, LogContext, build_app}; + use super::{ + AppState, HealthCheckResponse, LogContext, build_app, should_log_http_request_started, + should_log_http_response, + }; use crate::{ auth::AuthService, config::AuthConfig, speech::SpeechService, summary::SummaryService, }; @@ -336,6 +374,41 @@ mod tests { .unwrap() } + #[test] + fn successful_health_request_is_not_logged() { + assert!(!should_log_http_request_started("/health")); + assert!(!should_log_http_response(true, StatusCode::OK)); + } + + #[test] + fn failed_health_request_is_logged() { + assert!(should_log_http_response( + true, + StatusCode::INTERNAL_SERVER_ERROR + )); + } + + #[test] + fn ordinary_successful_request_is_logged() { + assert!(should_log_http_request_started("/api/v1/profiles")); + assert!(should_log_http_response(false, StatusCode::OK)); + } + + #[tokio::test] + async fn health_response_is_marked_for_log_filtering() { + let response = app() + .oneshot( + Request::builder() + .uri("/health") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert!(response.extensions().get::().is_some()); + } + #[tokio::test] async fn health_generates_a_uuid_request_id() { let response = app()