Skip to main content

brainwires_proxy/middleware/
logging.rs

1//! Structured JSONL traffic logging middleware.
2
3use crate::error::ProxyResult;
4use crate::middleware::{LayerAction, ProxyLayer};
5use crate::types::{ProxyRequest, ProxyResponse};
6
7const DEFAULT_MAX_BODY_LOG_BYTES: usize = 4096;
8
9/// Logs request/response pairs as structured JSON via `tracing`.
10pub struct LoggingLayer {
11    /// Whether to include body content in logs.
12    pub log_bodies: bool,
13    /// Maximum body bytes to include in log output.
14    pub max_body_log_bytes: usize,
15}
16
17impl LoggingLayer {
18    pub fn new() -> Self {
19        Self {
20            log_bodies: false,
21            max_body_log_bytes: DEFAULT_MAX_BODY_LOG_BYTES,
22        }
23    }
24
25    pub fn with_bodies(mut self, enabled: bool) -> Self {
26        self.log_bodies = enabled;
27        self
28    }
29
30    pub fn with_max_body_bytes(mut self, max: usize) -> Self {
31        self.max_body_log_bytes = max;
32        self
33    }
34}
35
36impl Default for LoggingLayer {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42#[async_trait::async_trait]
43impl ProxyLayer for LoggingLayer {
44    async fn on_request(&self, request: ProxyRequest) -> ProxyResult<LayerAction> {
45        let body_preview = if self.log_bodies {
46            let bytes = request.body.as_bytes();
47            let len = bytes.len().min(self.max_body_log_bytes);
48            String::from_utf8_lossy(&bytes[..len]).into_owned()
49        } else {
50            format!("[{} bytes]", request.body.len())
51        };
52
53        tracing::info!(
54            request_id = %request.id,
55            method = %request.method,
56            uri = %request.uri,
57            transport = ?request.transport,
58            body = %body_preview,
59            "proxy request"
60        );
61
62        Ok(LayerAction::Forward(request))
63    }
64
65    async fn on_response(&self, response: ProxyResponse) -> ProxyResult<ProxyResponse> {
66        let body_preview = if self.log_bodies {
67            let bytes = response.body.as_bytes();
68            let len = bytes.len().min(self.max_body_log_bytes);
69            String::from_utf8_lossy(&bytes[..len]).into_owned()
70        } else {
71            format!("[{} bytes]", response.body.len())
72        };
73
74        tracing::info!(
75            request_id = %response.id,
76            status = %response.status,
77            body = %body_preview,
78            "proxy response"
79        );
80
81        Ok(response)
82    }
83
84    fn name(&self) -> &str {
85        "logging"
86    }
87}