warpdrive_proxy/middleware/
logging.rs1use async_trait::async_trait;
20use pingora::http::ResponseHeader;
21use pingora::prelude::*;
22use std::time::Instant;
23use tracing::info;
24
25use super::{Middleware, MiddlewareContext};
26use crate::metrics::{HTTP_REQUEST_DURATION_SECONDS, HTTP_REQUESTS_TOTAL};
27
28pub struct LoggingMiddleware;
32
33impl LoggingMiddleware {
34 pub fn new() -> Self {
36 Self
37 }
38
39 fn get_header_str<'a>(headers: &'a http::HeaderMap, name: &str) -> &'a str {
41 headers
42 .get(name)
43 .and_then(|v| v.to_str().ok())
44 .unwrap_or("")
45 }
46}
47
48impl Default for LoggingMiddleware {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54#[async_trait]
55impl Middleware for LoggingMiddleware {
56 async fn request_filter(
61 &self,
62 _session: &mut Session,
63 ctx: &mut MiddlewareContext,
64 ) -> Result<()> {
65 ctx.request_start = Instant::now();
67 Ok(())
68 }
69
70 async fn response_filter(
75 &self,
76 session: &mut Session,
77 upstream_response: &mut ResponseHeader,
78 ctx: &mut MiddlewareContext,
79 ) -> Result<()> {
80 let elapsed = ctx.request_start.elapsed();
81 let req_header = session.req_header();
82 let resp_header = upstream_response;
83
84 let method = req_header.method.as_str();
86 let path = req_header.uri.path();
87 let query = req_header.uri.query().unwrap_or("");
88 let remote_addr = ctx.real_client_ip.to_string();
90
91 let req_content_type = Self::get_header_str(&req_header.headers, "content-type");
92 let req_content_length = req_header
93 .headers
94 .get("content-length")
95 .and_then(|v| v.to_str().ok())
96 .and_then(|s| s.parse::<i64>().ok())
97 .unwrap_or(-1);
98
99 let user_agent = Self::get_header_str(&req_header.headers, "user-agent");
100
101 let status = resp_header.status.as_u16();
103 ctx.status_code = status;
104
105 let resp_content_type = Self::get_header_str(&resp_header.headers, "content-type");
106 let resp_content_length = resp_header
107 .headers
108 .get("content-length")
109 .and_then(|v| v.to_str().ok())
110 .and_then(|s| s.parse::<i64>().ok())
111 .unwrap_or(-1);
112
113 let cache_status = Self::get_header_str(&resp_header.headers, "x-cache");
114
115 let status_str = status.to_string();
117 let method_str = method.to_string();
118
119 HTTP_REQUESTS_TOTAL
120 .with_label_values(&[&status_str, &method_str])
121 .inc();
122
123 HTTP_REQUEST_DURATION_SECONDS
124 .with_label_values(&[&method_str, &status_str])
125 .observe(elapsed.as_secs_f64());
126
127 info!(
129 path = %path,
130 status = status,
131 dur = elapsed.as_millis() as u64,
132 method = %method,
133 req_content_length = req_content_length,
134 req_content_type = %req_content_type,
135 resp_content_length = resp_content_length,
136 resp_content_type = %resp_content_type,
137 remote_addr = %remote_addr,
138 user_agent = %user_agent,
139 cache = %cache_status,
140 query = %query,
141 "Request"
142 );
143
144 Ok(())
145 }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_logging_middleware_creation() {
154 let middleware = LoggingMiddleware::new();
155 drop(middleware);
157
158 let middleware_default = LoggingMiddleware::default();
159 drop(middleware_default);
160 }
161
162 #[test]
163 fn test_get_header_str() {
164 let mut headers = http::HeaderMap::new();
165 headers.insert("content-type", "text/html".parse().unwrap());
166
167 assert_eq!(
168 LoggingMiddleware::get_header_str(&headers, "content-type"),
169 "text/html"
170 );
171 assert_eq!(LoggingMiddleware::get_header_str(&headers, "missing"), "");
172 }
173}