1use std::collections::HashMap;
20
21use serde_json::{json, Map, Value};
22
23use crate::context::Context;
24use crate::vars::template::Template;
25
26#[derive(Debug, Clone)]
30pub enum LogFormatValue {
31 Template(Template),
34 Literal(Value),
36}
37
38pub type LogFormat = HashMap<String, LogFormatValue>;
40
41pub fn build_entry(
48 ctx: &Context,
49 log_format: Option<&LogFormat>,
50 include_req_body: bool,
51 include_resp_body: bool,
52) -> Value {
53 match log_format {
54 Some(fmt) => build_custom(ctx, fmt),
55 None => build_default(ctx, include_req_body, include_resp_body),
56 }
57}
58
59pub fn reads_response_body(log_format: Option<&LogFormat>, include_resp_body: bool) -> bool {
70 if include_resp_body {
71 return true;
72 }
73 match log_format {
74 None => true,
75 Some(fmt) => fmt.values().any(|v| match v {
76 LogFormatValue::Template(t) => t.references_response_body(),
77 LogFormatValue::Literal(_) => false,
78 }),
79 }
80}
81
82pub fn parse_log_format(config: &HashMap<String, Value>) -> Result<Option<LogFormat>, String> {
88 match config.get("log_format") {
89 None | Some(Value::Null) => Ok(None),
90 Some(Value::Object(m)) => {
91 let mut out = HashMap::with_capacity(m.len());
92 for (k, v) in m {
93 let entry = match v {
94 Value::String(s) => LogFormatValue::Template(Template::parse(s).0),
95 Value::Number(_) | Value::Bool(_) => LogFormatValue::Literal(v.clone()),
96 _ => return Err(format!("log_format['{}'] must be a scalar", k)),
97 };
98 out.insert(k.clone(), entry);
99 }
100 Ok(Some(out))
101 }
102 Some(_) => Err("log_format must be an object of name -> template".to_string()),
103 }
104}
105
106fn build_custom(ctx: &Context, fmt: &LogFormat) -> Value {
107 let mut out = Map::new();
108 for (name, entry) in fmt {
109 let rendered = match entry {
110 LogFormatValue::Template(tpl) => Value::String(tpl.render_with_legacy(ctx)),
111 LogFormatValue::Literal(v) => v.clone(),
112 };
113 out.insert(name.clone(), rendered);
114 }
115 Value::Object(out)
116}
117
118fn build_default(ctx: &Context, include_req_body: bool, include_resp_body: bool) -> Value {
119 let mut request = json!({
120 "method": ctx.request.method,
121 "uri": request_uri(ctx),
122 "host": ctx.request.host,
123 "scheme": ctx.request.scheme,
124 "headers": headers_to_json(&ctx.request.headers),
125 "size": ctx.request.body.len(),
126 });
127 if include_req_body {
128 request["body"] = json!(String::from_utf8_lossy(&ctx.request.body));
129 }
130
131 let mut response = json!({
132 "status": ctx.response.status_code,
133 "headers": headers_to_json(&ctx.response.headers),
134 "size": ctx.response.body.len(),
135 });
136 if include_resp_body {
137 response["body"] = json!(String::from_utf8_lossy(&ctx.response.body));
138 }
139
140 let mut entry = Map::new();
141 entry.insert("request".to_string(), request);
142 entry.insert("response".to_string(), response);
143 entry.insert(
144 "client_ip".to_string(),
145 json!(client_ip(&ctx.request.remote_addr)),
146 );
147 if let Some(latency) = latency_ms(ctx) {
148 entry.insert("latency".to_string(), json!(latency));
149 }
150 if let Some(start) = ctx
151 .message
152 .get("__request_start_ms")
153 .and_then(|v| v.as_u64())
154 {
155 entry.insert("start_time".to_string(), json!(start));
156 }
157 if let Some(consumer) = ctx.message.get("consumer.name") {
158 entry.insert("consumer".to_string(), consumer.clone());
159 }
160 if !ctx.errors.is_empty() {
161 entry.insert(
162 "errors".to_string(),
163 json!(ctx
164 .errors
165 .iter()
166 .map(|e| json!({ "node_id": e.node_id, "code": e.code, "message": e.message }))
167 .collect::<Vec<_>>()),
168 );
169 }
170 Value::Object(entry)
171}
172
173fn latency_ms(ctx: &Context) -> Option<u64> {
175 let start = ctx.message.get("__request_start_ms")?.as_u64()?;
176 let now = std::time::SystemTime::now()
177 .duration_since(std::time::UNIX_EPOCH)
178 .ok()?
179 .as_millis() as u64;
180 Some(now.saturating_sub(start))
181}
182
183fn headers_to_json(headers: &HashMap<String, Vec<String>>) -> Value {
185 let mut m = Map::new();
186 for (k, v) in headers {
187 m.insert(k.clone(), json!(v));
188 }
189 Value::Object(m)
190}
191
192fn request_uri(ctx: &Context) -> String {
194 let mut pairs: Vec<String> = Vec::new();
195 for (k, values) in &ctx.request.query_params {
196 for v in values {
197 pairs.push(format!("{}={}", k, v));
198 }
199 }
200 pairs.sort();
201 if pairs.is_empty() {
202 ctx.request.path.clone()
203 } else {
204 format!("{}?{}", ctx.request.path, pairs.join("&"))
205 }
206}
207
208fn client_ip(remote_addr: &str) -> String {
210 match remote_addr.rsplit_once(':') {
211 Some((ip, _)) if !ip.contains(':') => ip.to_string(),
212 _ => remote_addr.to_string(),
213 }
214}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use crate::context::{GatewayRequest, GatewayResponse, Protocol};
220 use bytes::Bytes;
221
222 fn ctx() -> Context {
223 let mut headers = HashMap::new();
224 headers.insert("user-agent".to_string(), vec!["curl/8".to_string()]);
225 let mut query = HashMap::new();
226 query.insert("q".to_string(), vec!["x".to_string()]);
227 let mut message = HashMap::new();
228 message.insert("consumer.name".to_string(), json!("alice"));
229 Context {
230 request: GatewayRequest {
231 method: "GET".to_string(),
232 path: "/api/items".to_string(),
233 host: "example.com".to_string(),
234 scheme: "https".to_string(),
235 headers,
236 query_params: query,
237 body: Bytes::from_static(b"req"),
238 remote_addr: "10.0.0.5:44321".to_string(),
239 protocol: Protocol::Http1,
240 },
241 response: GatewayResponse {
242 status_code: 200,
243 headers: HashMap::new(),
244 body: Bytes::from_static(b"hello"),
245 stream: None,
246 },
247 message,
248 errors: Vec::new(),
249 }
250 }
251
252 #[test]
253 fn test_default_entry() {
254 let e = build_entry(&ctx(), None, false, false);
255 assert_eq!(e["request"]["method"], "GET");
256 assert_eq!(e["request"]["uri"], "/api/items?q=x");
257 assert_eq!(e["request"]["size"], 3);
258 assert_eq!(e["response"]["status"], 200);
259 assert_eq!(e["response"]["size"], 5);
260 assert_eq!(e["client_ip"], "10.0.0.5");
261 assert_eq!(e["consumer"], "alice");
262 assert!(e.get("body").is_none());
263 }
264
265 #[test]
266 fn test_default_entry_with_bodies() {
267 let e = build_entry(&ctx(), None, true, true);
268 assert_eq!(e["request"]["body"], "req");
269 assert_eq!(e["response"]["body"], "hello");
270 }
271
272 #[test]
273 fn test_custom_log_format() {
274 let mut config = HashMap::new();
275 config.insert(
276 "log_format".to_string(),
277 json!({
278 "who": "$consumer_name@$remote_addr",
279 "path": "$uri",
280 "code": "$status",
281 "const": 7,
282 }),
283 );
284 let fmt = parse_log_format(&config).unwrap().unwrap();
285 let e = build_entry(&ctx(), Some(&fmt), false, false);
286 assert_eq!(e["who"], "alice@10.0.0.5");
287 assert_eq!(e["path"], "/api/items");
288 assert_eq!(e["code"], "200");
289 assert_eq!(e["const"], 7);
290 }
291
292 #[test]
293 fn test_custom_log_format_superset_template_and_legacy_dollar() {
294 let mut config = HashMap::new();
297 config.insert(
298 "log_format".to_string(),
299 json!({ "combo": "{{request.method}} $uri" }),
300 );
301 let fmt = parse_log_format(&config).unwrap().unwrap();
302 let e = build_entry(&ctx(), Some(&fmt), false, false);
303 assert_eq!(e["combo"], "GET /api/items");
304 }
305
306 #[test]
307 fn test_parse_log_format() {
308 let mut config = HashMap::new();
309 assert!(parse_log_format(&config).unwrap().is_none());
310 config.insert("log_format".to_string(), json!({ "a": "$uri" }));
311 assert!(parse_log_format(&config).unwrap().is_some());
312 config.insert("log_format".to_string(), json!("not an object"));
313 assert!(parse_log_format(&config).is_err());
314 config.insert("log_format".to_string(), json!({ "a": { "nested": 1 } }));
315 assert!(parse_log_format(&config).is_err());
316 }
317
318 fn fmt(entries: &[(&str, &str)]) -> LogFormat {
319 entries
320 .iter()
321 .map(|(k, v)| {
322 let (t, _) = Template::parse(v);
323 (k.to_string(), LogFormatValue::Template(t))
324 })
325 .collect()
326 }
327
328 #[test]
332 fn test_body_free_log_format_does_not_read_the_response_body() {
333 let f = fmt(&[
334 ("path", "{{request.path}}"),
335 ("status", "{{response.status}}"),
336 ]);
337 assert!(!reads_response_body(Some(&f), false));
338 }
339
340 #[test]
342 fn test_log_format_referencing_the_body_reads_it() {
343 assert!(reads_response_body(
344 Some(&fmt(&[("b", "{{response.body}}")])),
345 false
346 ));
347 assert!(reads_response_body(
348 Some(&fmt(&[("b", "$resp_body")])),
349 false
350 ));
351 }
352
353 #[test]
357 fn test_absent_log_format_reads_the_response_body() {
358 assert!(reads_response_body(None, false));
359 }
360
361 #[test]
363 fn test_include_resp_body_reads_the_response_body() {
364 let f = fmt(&[("path", "{{request.path}}")]);
365 assert!(reads_response_body(Some(&f), true));
366 }
367}