// // Copyright (c) 2025 rustmailer.com (https://rustmailer.com) // // This file is part of the Bichon Email Archiving Project // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use std::{ num::NonZeroU32, sync::{Arc, LazyLock}, time::Instant, }; use governor::{ clock::{QuantaClock, QuantaInstant}, middleware::NoOpMiddleware, state::InMemoryState, Quota, RateLimiter, }; use poem::{ http::header, web::RealIp, Endpoint, FromRequest, IntoResponse, Middleware, Request, Response, Result, }; use tracing::{error, info, warn, Instrument}; pub type GovRateLimiter = RateLimiter< governor::state::NotKeyed, InMemoryState, QuantaClock, NoOpMiddleware, >; static RATE_LIMITER: LazyLock = LazyLock::new(LogRateLimiter::new); pub struct LogRateLimiter { limiter: Arc, } impl LogRateLimiter { pub fn new() -> Self { let quota = Quota::per_second(NonZeroU32::new(10).unwrap()); let limiter = RateLimiter::direct(quota); Self { limiter: Arc::new(limiter), } } pub async fn should_log(&self, status: u16) -> bool { let cost = match status { 500_u16.. => NonZeroU32::new(1).unwrap(), // ERROR 400_u16..=499_u16 => NonZeroU32::new(3).unwrap(), // WARN _ => NonZeroU32::new(5).unwrap(), // INFO }; self.limiter.check_n(cost).is_ok() } } #[derive(Default)] pub struct Tracing; impl Middleware for Tracing { type Output = TracingEndpoint; fn transform(&self, ep: E) -> Self::Output { TracingEndpoint { inner: ep } } } /// Endpoint for the `Tracing` middleware. pub struct TracingEndpoint { inner: E, } impl Endpoint for TracingEndpoint { type Output = Response; async fn call(&self, req: Request) -> Result { let remote_addr = RealIp::from_request_without_body(&req) .await .ok() .and_then(|real_ip| real_ip.0) .map(|addr| addr.to_string()) .unwrap_or_else(|| req.remote_addr().to_string()); let method = req.method().clone(); let path = req.uri().path().to_string(); let query = req.uri().query().map(|q| q.to_string()); let referer = req .headers() .get(header::REFERER) .and_then(|v| v.to_str().ok().map(|v| v.to_string())); let content_length = req .headers() .get(header::CONTENT_LENGTH) .and_then(|v| v.to_str().ok().map(|v| v.to_string())); let span = tracing::info_span!( "request", remote_addr = %remote_addr, method = %method, path = %path, query = ?query, referer = ?referer, //user_agent = ?user_agent, // forwarded = ?forwarded, content_length = ?content_length, ); async move { let now = Instant::now(); let res = self.inner.call(req).await; let duration = now.elapsed(); match res { Ok(resp) => { let resp = resp.into_response(); let status = resp.status().as_u16(); log_response(status, duration).await; Ok(resp) } Err(err) => { let status = err.status().as_u16(); log_response(status, duration).await; Err(err) } } } .instrument(span) .await } } #[inline] async fn log_response(status: u16, duration: std::time::Duration) { if RATE_LIMITER.should_log(status).await { match status { 500.. => { error!( status = %status, duration = ?duration, "request completed with server error" ); } 400..=499 => { warn!( status = %status, duration = ?duration, "request completed with client error" ); } _ => { info!( status = %status, duration = ?duration, "request completed successfully" ); } } } }