diff --git a/apps/app/src/main.rs b/apps/app/src/main.rs index 9709556..9712374 100644 --- a/apps/app/src/main.rs +++ b/apps/app/src/main.rs @@ -1,10 +1,12 @@ use actix_cors::Cors; use actix_web::cookie::time::Duration; +use actix_web::dev::{Service, ServiceRequest, ServiceResponse}; use actix_web::middleware::Logger; use actix_web::{cookie::Key, web, App, HttpResponse, HttpServer}; use clap::Parser; use db::cache::AppCache; use db::database::AppDatabase; +use futures::future::LocalBoxFuture; use observability::{ init_tracing_subscriber, install_recorder, prometheus_handler, spawn_http_metrics_poller, HttpMetrics, HttpSnapshotGuard, MetricsMiddleware, TracingSpanMiddleware, @@ -14,6 +16,7 @@ use service::AppService; use session::config::{PersistentSession, SessionLifecycle, TtlExtensionPolicy}; use session::storage::RedisClusterSessionStore; use session::SessionMiddleware; +use std::task::{Context, Poll}; mod args; @@ -27,6 +30,82 @@ pub struct AppState { 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); + +impl SkipNoisyPaths { + fn new(logger: S) -> Self { + Self(logger) + } +} + +impl actix_web::dev::Transform for SkipNoisyPaths +where + S: actix_web::dev::Transform< + ServiceRequest, + Response = ServiceResponse, + Error = actix_web::Error, + InitError = (), + >, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Transform = SkipNoisyPathsService; + type InitError = (); + + fn new_transform(&self, service: S::Transform) -> , + Error = actix_web::Error, + InitError = (), + >>::Future { + futures::future::ok(SkipNoisyPathsService { + service, + _marker: std::marker::PhantomData, + }) + } +} + +struct SkipNoisyPathsService { + service: S, + _marker: std::marker::PhantomData, +} + +impl actix_web::dev::Service for SkipNoisyPathsService +where + S: actix_web::dev::Service, Error = actix_web::Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Future = LocalBoxFuture<'static, Result>; + + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + 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 { if let Some(secret) = cfg.env.get("APP_SESSION_SECRET") { let bytes: Vec = secret.as_bytes().iter().cycle().take(64).copied().collect(); @@ -128,7 +207,7 @@ async fn main() -> anyhow::Result<()> { App::new() .wrap(cors) .wrap(session_mw) - .wrap(Logger::default().exclude("/health")) + .wrap(SkipNoisyPaths::new(Logger::default())) .wrap(metrics_mw) .wrap(TracingSpanMiddleware::new()) .app_data(web::Data::new(AppState { diff --git a/apps/static/src/main.rs b/apps/static/src/main.rs index c85d0a2..876b9ed 100644 --- a/apps/static/src/main.rs +++ b/apps/static/src/main.rs @@ -1,7 +1,10 @@ use actix_cors::Cors; use actix_files::Files; +use actix_web::dev::{Service, ServiceRequest, ServiceResponse}; use actix_web::{http::header, middleware::Logger, web, App, HttpResponse, HttpServer}; +use futures::future::LocalBoxFuture; use std::path::PathBuf; +use std::task::{Context, Poll}; /// Static file server for avatar, blob, and other static files /// 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); + +impl SkipNoisyPaths { + fn new(logger: S) -> Self { + Self(logger) + } +} + +impl actix_web::dev::Transform for SkipNoisyPaths +where + S: actix_web::dev::Transform< + ServiceRequest, + Response = ServiceResponse, + Error = actix_web::Error, + InitError = (), + >, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Transform = SkipNoisyPathsService; + type InitError = (); + + fn new_transform(&self, service: S::Transform) -> , + Error = actix_web::Error, + InitError = (), + >>::Future { + futures::future::ok(SkipNoisyPathsService { + service, + _marker: std::marker::PhantomData, + }) + } +} + +struct SkipNoisyPathsService { + service: S, + _marker: std::marker::PhantomData, +} + +impl Service for SkipNoisyPathsService +where + S: Service, Error = actix_web::Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Future = LocalBoxFuture<'static, Result>; + + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + 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] async fn main() -> anyhow::Result<()> { env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); @@ -79,7 +155,7 @@ async fn main() -> anyhow::Result<()> { App::new() .wrap(cors) - .wrap(Logger::default()) + .wrap(SkipNoisyPaths::new(Logger::default())) .route("/health", web::get().to(health)) .service( Files::new("/avatar", root.join("avatar"))