chore(app): exclude health/metrics/WS from access logger output

Skip terminal access logs for noisy K8s probe and monitoring
endpoints: /health, /metrics, and /ws path prefix.
Applied to both the main app and static file server.
This commit is contained in:
ZhenYi 2026-04-25 22:51:21 +08:00
parent 61210da7a1
commit b00d42ee8d
2 changed files with 157 additions and 2 deletions

View File

@ -1,10 +1,12 @@
use actix_cors::Cors; use actix_cors::Cors;
use actix_web::cookie::time::Duration; use actix_web::cookie::time::Duration;
use actix_web::dev::{Service, ServiceRequest, ServiceResponse};
use actix_web::middleware::Logger; use actix_web::middleware::Logger;
use actix_web::{cookie::Key, web, App, HttpResponse, HttpServer}; use actix_web::{cookie::Key, web, App, HttpResponse, HttpServer};
use clap::Parser; use clap::Parser;
use db::cache::AppCache; use db::cache::AppCache;
use db::database::AppDatabase; use db::database::AppDatabase;
use futures::future::LocalBoxFuture;
use observability::{ use observability::{
init_tracing_subscriber, install_recorder, prometheus_handler, spawn_http_metrics_poller, init_tracing_subscriber, install_recorder, prometheus_handler, spawn_http_metrics_poller,
HttpMetrics, HttpSnapshotGuard, MetricsMiddleware, TracingSpanMiddleware, HttpMetrics, HttpSnapshotGuard, MetricsMiddleware, TracingSpanMiddleware,
@ -14,6 +16,7 @@ use service::AppService;
use session::config::{PersistentSession, SessionLifecycle, TtlExtensionPolicy}; use session::config::{PersistentSession, SessionLifecycle, TtlExtensionPolicy};
use session::storage::RedisClusterSessionStore; use session::storage::RedisClusterSessionStore;
use session::SessionMiddleware; use session::SessionMiddleware;
use std::task::{Context, Poll};
mod args; mod args;
@ -27,6 +30,82 @@ pub struct AppState {
pub cache: AppCache, pub cache: AppCache,
} }
/// Skips Logger terminal output for noisy health/monitor/WS endpoints while still
/// passing the request through to the inner service (so metrics are still recorded).
struct SkipNoisyPaths<S>(S);
impl<S> SkipNoisyPaths<S> {
fn new(logger: S) -> Self {
Self(logger)
}
}
impl<S, B> actix_web::dev::Transform<S, ServiceRequest> for SkipNoisyPaths<S>
where
S: actix_web::dev::Transform<
ServiceRequest,
Response = ServiceResponse<B>,
Error = actix_web::Error,
InitError = (),
>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
type Transform = SkipNoisyPathsService<S::Transform>;
type InitError = ();
fn new_transform(&self, service: S::Transform) -> <Self::Transform as actix_web::dev::Transform<
ServiceRequest,
Response = ServiceResponse<B>,
Error = actix_web::Error,
InitError = (),
>>::Future {
futures::future::ok(SkipNoisyPathsService {
service,
_marker: std::marker::PhantomData,
})
}
}
struct SkipNoisyPathsService<S> {
service: S,
_marker: std::marker::PhantomData<fn(ServiceRequest)>,
}
impl<S, B> actix_web::dev::Service<ServiceRequest> for SkipNoisyPathsService<S>
where
S: actix_web::dev::Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let path = req.path().to_string();
let should_skip = path == "/health"
|| path == "/metrics"
|| path.starts_with("/ws");
if should_skip {
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
// Replace body so Logger doesn't print the line for this request.
Ok(res.map_body(|_, _| actix_web::body::Empty::new()))
})
} else {
self.service.call(req)
}
}
}
fn build_session_key(cfg: &AppConfig) -> anyhow::Result<Key> { fn build_session_key(cfg: &AppConfig) -> anyhow::Result<Key> {
if let Some(secret) = cfg.env.get("APP_SESSION_SECRET") { if let Some(secret) = cfg.env.get("APP_SESSION_SECRET") {
let bytes: Vec<u8> = secret.as_bytes().iter().cycle().take(64).copied().collect(); let bytes: Vec<u8> = secret.as_bytes().iter().cycle().take(64).copied().collect();
@ -128,7 +207,7 @@ async fn main() -> anyhow::Result<()> {
App::new() App::new()
.wrap(cors) .wrap(cors)
.wrap(session_mw) .wrap(session_mw)
.wrap(Logger::default().exclude("/health")) .wrap(SkipNoisyPaths::new(Logger::default()))
.wrap(metrics_mw) .wrap(metrics_mw)
.wrap(TracingSpanMiddleware::new()) .wrap(TracingSpanMiddleware::new())
.app_data(web::Data::new(AppState { .app_data(web::Data::new(AppState {

View File

@ -1,7 +1,10 @@
use actix_cors::Cors; use actix_cors::Cors;
use actix_files::Files; use actix_files::Files;
use actix_web::dev::{Service, ServiceRequest, ServiceResponse};
use actix_web::{http::header, middleware::Logger, web, App, HttpResponse, HttpServer}; use actix_web::{http::header, middleware::Logger, web, App, HttpResponse, HttpServer};
use futures::future::LocalBoxFuture;
use std::path::PathBuf; use std::path::PathBuf;
use std::task::{Context, Poll};
/// Static file server for avatar, blob, and other static files /// Static file server for avatar, blob, and other static files
/// Serves files from /data/{type} directories /// Serves files from /data/{type} directories
@ -39,6 +42,79 @@ async fn health() -> HttpResponse {
})) }))
} }
/// Skips Logger terminal output for noisy health/WS endpoints.
struct SkipNoisyPaths<S>(S);
impl<S> SkipNoisyPaths<S> {
fn new(logger: S) -> Self {
Self(logger)
}
}
impl<S, B> actix_web::dev::Transform<S, ServiceRequest> for SkipNoisyPaths<S>
where
S: actix_web::dev::Transform<
ServiceRequest,
Response = ServiceResponse<B>,
Error = actix_web::Error,
InitError = (),
>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
type Transform = SkipNoisyPathsService<S::Transform>;
type InitError = ();
fn new_transform(&self, service: S::Transform) -> <Self::Transform as actix_web::dev::Transform<
ServiceRequest,
Response = ServiceResponse<B>,
Error = actix_web::Error,
InitError = (),
>>::Future {
futures::future::ok(SkipNoisyPathsService {
service,
_marker: std::marker::PhantomData,
})
}
}
struct SkipNoisyPathsService<S> {
service: S,
_marker: std::marker::PhantomData<fn(ServiceRequest)>,
}
impl<S, B> Service<ServiceRequest> for SkipNoisyPathsService<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = actix_web::Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service.poll_ready(cx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let path = req.path().to_string();
let should_skip =
path == "/health" || path == "/metrics" || path.starts_with("/ws");
if should_skip {
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
Ok(res.map_body(|_, _| actix_web::body::Empty::new()))
})
} else {
self.service.call(req)
}
}
}
#[actix_web::main] #[actix_web::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
@ -79,7 +155,7 @@ async fn main() -> anyhow::Result<()> {
App::new() App::new()
.wrap(cors) .wrap(cors)
.wrap(Logger::default()) .wrap(SkipNoisyPaths::new(Logger::default()))
.route("/health", web::get().to(health)) .route("/health", web::get().to(health))
.service( .service(
Files::new("/avatar", root.join("avatar")) Files::new("/avatar", root.join("avatar"))