feat(observability): suppress successful health logs
This commit is contained in:
@@ -63,6 +63,15 @@ curl -fsS http://127.0.0.1:39180/health
|
|||||||
|
|
||||||
仪表盘适合浏览和筛选;需要精确关联请求或比较版本时,打开 `Explore`,数据源选择 `Loki`。
|
仪表盘适合浏览和筛选;需要精确关联请求或比较版本时,打开 `Explore`,数据源选择 `Loki`。
|
||||||
|
|
||||||
|
仪表盘包含两个日志面板:
|
||||||
|
|
||||||
|
- `HTTP Errors`:只显示能够解析出 HTTP `status >= 400` 的结构化日志。
|
||||||
|
- `Service Logs (health excluded)`:显示所选服务的普通日志,并默认排除例行 `/health` 请求。
|
||||||
|
|
||||||
|
API 不记录成功 `/health` 请求的开始和结束事件,以避免 Docker 日志与 Loki 被固定频率的探针日志占满。
|
||||||
|
如果 `/health` 返回非 2xx 或请求在中间件中失败,错误日志仍会保留。需要查看改造前的历史健康检查日志时,
|
||||||
|
在 `Explore` 中使用明确包含 `/health` 的查询,并把时间范围设置到对应部署时间之前。
|
||||||
|
|
||||||
## 常用 LogQL
|
## 常用 LogQL
|
||||||
|
|
||||||
查看教学反馈的全部日志:
|
查看教学反馈的全部日志:
|
||||||
@@ -71,6 +80,12 @@ curl -fsS http://127.0.0.1:39180/health
|
|||||||
{application="teaching-feedback"}
|
{application="teaching-feedback"}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
排除改造前已采集的健康检查日志:
|
||||||
|
|
||||||
|
```logql
|
||||||
|
{application="teaching-feedback", service="api"} != "\"path\":\"/health\""
|
||||||
|
```
|
||||||
|
|
||||||
按服务、级别和文本筛选:
|
按服务、级别和文本筛选:
|
||||||
|
|
||||||
```logql
|
```logql
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use std::{path::PathBuf, sync::Arc, time::Duration};
|
|||||||
use axum::{
|
use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
extract::{DefaultBodyLimit, MatchedPath},
|
extract::{DefaultBodyLimit, MatchedPath},
|
||||||
http::{HeaderName, HeaderValue, Request},
|
http::{HeaderName, HeaderValue, Request, StatusCode},
|
||||||
response::Response,
|
response::Response,
|
||||||
routing::get,
|
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]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||||
dotenvy::dotenv().ok();
|
dotenvy::dotenv().ok();
|
||||||
@@ -161,6 +184,9 @@ fn build_app(
|
|||||||
make_http_request_span(request, &request_span_context)
|
make_http_request_span(request, &request_span_context)
|
||||||
})
|
})
|
||||||
.on_request(move |request: &Request<_>, span: &Span| {
|
.on_request(move |request: &Request<_>, span: &Span| {
|
||||||
|
if !should_log_http_request_started(request.uri().path()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
tracing::info!(
|
tracing::info!(
|
||||||
parent: span,
|
parent: span,
|
||||||
service = "api",
|
service = "api",
|
||||||
@@ -174,6 +200,11 @@ fn build_app(
|
|||||||
})
|
})
|
||||||
.on_response(move |response: &Response, latency: Duration, span: &Span| {
|
.on_response(move |response: &Response, latency: Duration, span: &Span| {
|
||||||
let status = response.status();
|
let status = response.status();
|
||||||
|
let is_health_check =
|
||||||
|
response.extensions().get::<HealthCheckResponse>().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 latency_ms = u64::try_from(latency.as_millis()).unwrap_or(u64::MAX);
|
||||||
let request_id = response
|
let request_id = response
|
||||||
.headers()
|
.headers()
|
||||||
@@ -226,6 +257,7 @@ fn build_app(
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
.layer(axum::middleware::from_fn(mark_health_check_response))
|
||||||
.layer(PropagateRequestIdLayer::x_request_id()),
|
.layer(PropagateRequestIdLayer::x_request_id()),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -304,11 +336,17 @@ async fn shutdown_signal(log_context: LogContext) {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use axum::{body::Body, http::Request};
|
use axum::{
|
||||||
|
body::Body,
|
||||||
|
http::{Request, StatusCode},
|
||||||
|
};
|
||||||
use tower::ServiceExt;
|
use tower::ServiceExt;
|
||||||
use uuid::Uuid;
|
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::{
|
use crate::{
|
||||||
auth::AuthService, config::AuthConfig, speech::SpeechService, summary::SummaryService,
|
auth::AuthService, config::AuthConfig, speech::SpeechService, summary::SummaryService,
|
||||||
};
|
};
|
||||||
@@ -336,6 +374,41 @@ mod tests {
|
|||||||
.unwrap()
|
.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::<HealthCheckResponse>().is_some());
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn health_generates_a_uuid_request_id() {
|
async fn health_generates_a_uuid_request_id() {
|
||||||
let response = app()
|
let response = app()
|
||||||
|
|||||||
Reference in New Issue
Block a user