1use serde_json::{Map, Value};
8
9#[derive(Debug, Clone, Default)]
15pub struct ParsedLog {
16 pub message: String,
18 pub level: Option<String>,
20 pub msg: Option<String>,
22 pub logger: Option<String>,
24 pub fields_json: Option<String>,
28}
29
30impl ParsedLog {
31 fn plain(message: impl Into<String>) -> Self {
33 Self {
34 message: message.into(),
35 ..Default::default()
36 }
37 }
38}
39
40const MAX_PARSE_LINE_LEN: usize = 65536;
44
45pub fn parse(line: &str, format: &str) -> ParsedLog {
50 if line.len() > MAX_PARSE_LINE_LEN {
51 return ParsedLog::plain(line);
52 }
53 match format {
54 "json" => parse_json(line).unwrap_or_else(|| ParsedLog::plain(line)),
55 "logfmt" => parse_logfmt(line).unwrap_or_else(|| ParsedLog::plain(line)),
56 _ => ParsedLog::plain(line),
57 }
58}
59
60fn parse_json(line: &str) -> Option<ParsedLog> {
65 let value: Value = serde_json::from_str(line.trim()).ok()?;
66 let obj = value.as_object()?;
67
68 let level = extract_level(obj, false);
69 let msg = extract_msg(obj);
70 let logger = extract_logger(obj);
71
72 let fields_json = serde_json::to_string(&value).ok()?;
75
76 Some(ParsedLog {
77 message: line.to_string(),
78 level,
79 msg,
80 logger,
81 fields_json: Some(fields_json),
82 })
83}
84
85fn parse_logfmt(line: &str) -> Option<ParsedLog> {
90 let pairs = parse_logfmt_pairs(line)?;
91
92 let mut obj = Map::new();
94 for (key, value) in &pairs {
95 let json_val = match value {
96 LogfmtValue::Bare => Value::Bool(true),
98 LogfmtValue::Quoted(value) => Value::String(value.clone()),
100 LogfmtValue::Unquoted(value) => unquoted_logfmt_value(value),
101 };
102 obj.insert(key.clone(), json_val);
103 }
104
105 let level = extract_level(&obj, true);
107 let msg = extract_msg(&obj);
108 let logger = extract_logger(&obj);
109 let value = Value::Object(obj);
110 let fields_json = serde_json::to_string(&value).ok()?;
111
112 Some(ParsedLog {
113 message: line.to_string(),
114 level,
115 msg,
116 logger,
117 fields_json: Some(fields_json),
118 })
119}
120
121enum LogfmtValue {
133 Bare,
136 Quoted(String),
137 Unquoted(String),
138}
139
140fn unquoted_logfmt_value(value: &str) -> Value {
145 let typed = match value {
146 "true" => Some(Value::Bool(true)),
147 "false" => Some(Value::Bool(false)),
148 "null" => Some(Value::Null),
149 _ => value
150 .parse::<i64>()
151 .ok()
152 .map(|n| Value::Number(n.into()))
153 .or_else(|| {
154 value
155 .parse::<f64>()
156 .ok()
157 .and_then(serde_json::Number::from_f64)
158 .map(Value::Number)
159 }),
160 };
161 match typed {
162 Some(typed) if json_spelling_is(&typed, value) => typed,
163 _ => Value::String(value.to_string()),
164 }
165}
166
167fn json_spelling_is(typed: &Value, text: &str) -> bool {
170 let mut buf = [0u8; 40];
172 let mut cursor = std::io::Cursor::new(&mut buf[..]);
173 if serde_json::to_writer(&mut cursor, typed).is_err() {
174 return false;
175 }
176 let len = cursor.position() as usize;
177 &buf[..len] == text.as_bytes()
178}
179
180fn parse_logfmt_pairs(line: &str) -> Option<Vec<(String, LogfmtValue)>> {
183 let bytes = line.as_bytes();
184 let mut pairs = Vec::new();
185 let mut bare_key_count = 0;
186 let mut i = 0;
187
188 while i < bytes.len() {
189 while i < bytes.len() && bytes[i].is_ascii_whitespace() {
191 i += 1;
192 }
193 if i >= bytes.len() {
194 break;
195 }
196
197 let key_start = i;
199 while i < bytes.len() && !bytes[i].is_ascii_whitespace() && bytes[i] != b'=' {
200 i += 1;
201 }
202 let key = &line[key_start..i];
203 if key.is_empty() {
204 i += 1;
206 continue;
207 }
208 if !key
213 .bytes()
214 .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'.' || b == b'-' || b == b'@')
215 {
216 while i < bytes.len() && bytes[i] == b'=' {
219 i += 1;
220 if i < bytes.len() && bytes[i] == b'"' {
221 i += 1;
222 while i < bytes.len() && bytes[i] != b'"' {
223 if bytes[i] == b'\\' && i + 1 < bytes.len() {
224 i += 2;
225 } else {
226 i += 1;
227 }
228 }
229 if i < bytes.len() {
230 i += 1;
231 }
232 } else {
233 while i < bytes.len() && !bytes[i].is_ascii_whitespace() {
234 i += 1;
235 }
236 }
237 }
238 continue;
239 }
240
241 if i < bytes.len() && bytes[i] == b'=' {
243 i += 1; if i < bytes.len() && bytes[i] == b'"' {
247 i += 1; let val_start = i;
250 while i < bytes.len() && bytes[i] != b'"' {
251 if bytes[i] == b'\\' && i + 1 < bytes.len() {
252 i += 2;
253 } else {
254 i += 1;
255 }
256 }
257 let value = unescape_logfmt_value(&line[val_start..i]);
258 if i < bytes.len() {
259 i += 1; }
261 pairs.push((key.to_string(), LogfmtValue::Quoted(value)));
262 } else {
263 let val_start = i;
265 while i < bytes.len() && !bytes[i].is_ascii_whitespace() {
266 i += 1;
267 }
268 pairs.push((
269 key.to_string(),
270 LogfmtValue::Unquoted(line[val_start..i].to_string()),
271 ));
272 }
273 } else {
274 pairs.push((key.to_string(), LogfmtValue::Bare));
276 bare_key_count += 1;
277 }
278 }
279
280 if pairs.is_empty() {
281 return None;
282 }
283 if !line.contains('=') {
285 return None;
286 }
287 if bare_key_count * 2 > pairs.len() {
291 return None;
292 }
293 Some(pairs)
294}
295
296fn unescape_logfmt_value(s: &str) -> String {
297 let mut result = String::with_capacity(s.len());
298 let mut chars = s.chars();
299 while let Some(c) = chars.next() {
300 if c == '\\' {
301 if let Some(next) = chars.next() {
302 result.push(next);
303 }
304 } else {
305 result.push(c);
306 }
307 }
308 result
309}
310
311fn extract_level(obj: &Map<String, Value>, numeric_text: bool) -> Option<String> {
322 for key in &["level", "severity", "lvl", "PRIORITY", "@level"] {
323 if let Some(val) = obj.get(*key)
324 && let Some(level) = normalize_level_value(val, numeric_text)
325 {
326 return Some(level);
327 }
328 }
329 None
330}
331
332fn normalize_level_value(val: &Value, numeric_text: bool) -> Option<String> {
337 match val {
338 Value::String(s) => normalize_level_str(s).or_else(|| {
339 numeric_text
340 .then(|| s.parse::<i64>().ok())
341 .flatten()
342 .and_then(normalize_level_number)
343 }),
344 Value::Number(n) => normalize_level_number(n.as_i64()?),
345 _ => None,
346 }
347}
348
349fn normalize_level_number(n: i64) -> Option<String> {
352 match n {
354 50 | 60 => Some("error".into()),
355 40 => Some("warn".into()),
356 30 => Some("info".into()),
357 20 => Some("debug".into()),
358 10 => Some("trace".into()),
359 0..=3 => Some("error".into()),
361 4 | 5 => Some("warn".into()),
362 6 => Some("info".into()),
363 7 => Some("debug".into()),
364 _ => None,
365 }
366}
367
368pub fn normalize_level_str(s: &str) -> Option<String> {
369 let lower = s.to_ascii_lowercase();
370 match lower.as_str() {
371 "error" | "err" | "fatal" | "critical" | "panic" | "alert" | "emerg" => {
372 Some("error".into())
373 }
374 "warn" | "warning" | "wrn" => Some("warn".into()),
375 "info" | "inf" | "information" | "notice" => Some("info".into()),
376 "debug" | "dbg" => Some("debug".into()),
377 "trace" | "trc" => Some("trace".into()),
378 _ => None,
379 }
380}
381
382fn extract_first_string(obj: &Map<String, Value>, keys: &[&str]) -> Option<String> {
383 for key in keys {
384 if let Some(Value::String(s)) = obj.get(*key) {
385 return Some(s.clone());
386 }
387 }
388 None
389}
390
391fn extract_msg(obj: &Map<String, Value>) -> Option<String> {
393 extract_first_string(obj, &["msg", "message", "event", "@message"])
394}
395
396fn extract_logger(obj: &Map<String, Value>) -> Option<String> {
398 extract_first_string(obj, &["logger", "name", "component", "module"])
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
406 fn test_json_parse() {
407 let line = r#"{"level":"info","msg":"server started","port":8080}"#;
408 let parsed = parse(line, "json");
409 assert_eq!(parsed.level.as_deref(), Some("info"));
410 assert_eq!(parsed.msg.as_deref(), Some("server started"));
411 assert!(parsed.fields_json.is_some());
412 }
413
414 #[test]
415 fn test_json_level_normalization() {
416 let line = r#"{"level":"FATAL","msg":"crash"}"#;
417 let parsed = parse(line, "json");
418 assert_eq!(parsed.level.as_deref(), Some("error"));
419 }
420
421 #[test]
422 fn test_json_pino_integer_level() {
423 let line = r#"{"level":50,"msg":"error occurred"}"#;
424 let parsed = parse(line, "json");
425 assert_eq!(parsed.level.as_deref(), Some("error"));
426 }
427
428 #[test]
429 fn test_json_syslog_priority() {
430 let line = r#"{"PRIORITY":3,"msg":"system error"}"#;
431 let parsed = parse(line, "json");
432 assert_eq!(parsed.level.as_deref(), Some("error"));
433 }
434
435 #[test]
436 fn test_json_msg_aliases() {
437 let line = r#"{"event":"hello","level":"info"}"#;
439 let parsed = parse(line, "json");
440 assert_eq!(parsed.msg.as_deref(), Some("hello"));
441 }
442
443 #[test]
444 fn test_logfmt_parse() {
445 let line = r#"level=info msg="server started" port=8080"#;
446 let parsed = parse(line, "logfmt");
447 assert_eq!(parsed.level.as_deref(), Some("info"));
448 assert_eq!(parsed.msg.as_deref(), Some("server started"));
449 assert!(parsed.fields_json.is_some());
450 }
451
452 fn logfmt_fields(line: &str) -> Value {
453 let parsed = parse(line, "logfmt");
454 serde_json::from_str(parsed.fields_json.as_deref().unwrap()).unwrap()
455 }
456
457 #[test]
458 fn test_logfmt_types_values_only_when_nothing_is_lost() {
459 let fields = logfmt_fields(
460 "level=info status=500 took=0.25 neg=-3 ok=true gone=null id=007 version=1.10 \
461 big=12345678901234567890 exp=1e3 flag=TRUE",
462 );
463 assert_eq!(fields["status"], serde_json::json!(500));
464 assert_eq!(fields["took"], serde_json::json!(0.25));
465 assert_eq!(fields["neg"], serde_json::json!(-3));
466 assert_eq!(fields["ok"], Value::Bool(true));
467 assert_eq!(fields["gone"], Value::Null);
468 for (key, text) in [
469 ("id", "007"),
470 ("version", "1.10"),
471 ("big", "12345678901234567890"),
472 ("exp", "1e3"),
473 ("flag", "TRUE"),
474 ] {
475 assert_eq!(fields[key], Value::String(text.into()), "{key}");
476 }
477 }
478
479 #[test]
480 fn test_logfmt_quoted_values_stay_strings() {
481 let fields = logfmt_fields(r#"level=info code="007" count="42" ok="true""#);
482 assert_eq!(fields["code"], Value::String("007".into()));
483 assert_eq!(fields["count"], Value::String("42".into()));
484 assert_eq!(fields["ok"], Value::String("true".into()));
485 }
486
487 #[test]
490 fn test_logfmt_numeric_level_text_is_normalized() {
491 for (line, level) in [
492 (r#"level="30" msg=hi"#, "info"),
493 ("level=50 msg=hi", "error"),
494 (r#"PRIORITY="3" msg=hi"#, "error"),
495 ("level=03 msg=hi", "error"),
496 ] {
497 assert_eq!(
498 parse(line, "logfmt").level.as_deref(),
499 Some(level),
500 "{line}"
501 );
502 }
503 assert_eq!(
504 logfmt_fields(r#"level="30" msg=hi"#)["level"],
505 Value::String("30".into())
506 );
507 }
508
509 #[test]
512 fn test_json_numeric_level_string_is_not_normalized() {
513 assert_eq!(parse(r#"{"level":"30","msg":"hi"}"#, "json").level, None);
514 }
515
516 #[test]
517 fn test_logfmt_bare_key() {
518 let line = r#"level=debug ready msg="ok""#;
519 let parsed = parse(line, "logfmt");
520 assert_eq!(parsed.level.as_deref(), Some("debug"));
521 assert_eq!(parsed.msg.as_deref(), Some("ok"));
522 let fields: Value = serde_json::from_str(parsed.fields_json.as_deref().unwrap()).unwrap();
524 assert_eq!(fields["ready"], Value::Bool(true));
525 }
526
527 #[test]
528 fn test_logfmt_quoted_value_with_spaces() {
529 let line = r#"level=error msg="connection refused: timeout""#;
530 let parsed = parse(line, "logfmt");
531 assert_eq!(parsed.msg.as_deref(), Some("connection refused: timeout"));
532 }
533
534 #[test]
535 fn test_text_format() {
536 let line = r#"{"level":"info"}"#;
537 let parsed = parse(line, "text");
538 assert!(parsed.level.is_none());
539 assert!(parsed.fields_json.is_none());
540 assert_eq!(parsed.message, line);
541 }
542
543 #[test]
544 fn test_json_parse_failure_falls_back() {
545 let line = "{not valid json";
546 let parsed = parse(line, "json");
547 assert!(parsed.level.is_none());
548 assert!(parsed.fields_json.is_none());
549 assert_eq!(parsed.message, line);
550 }
551
552 #[test]
553 fn test_logfmt_logger_extraction() {
554 let line = r#"level=info msg="hi" logger=myapp"#;
555 let parsed = parse(line, "logfmt");
556 assert_eq!(parsed.logger.as_deref(), Some("myapp"));
557 }
558
559 #[test]
560 fn test_logfmt_multiple_bare_keys_still_parsed() {
561 let line = r#"level=debug ready enabled msg="ok""#;
563 let parsed = parse(line, "logfmt");
564 assert_eq!(parsed.level.as_deref(), Some("debug"));
565 assert_eq!(parsed.msg.as_deref(), Some("ok"));
566 let fields: Value = serde_json::from_str(parsed.fields_json.as_deref().unwrap()).unwrap();
567 assert_eq!(fields["ready"], Value::Bool(true));
568 assert_eq!(fields["enabled"], Value::Bool(true));
569 }
570
571 #[test]
572 fn test_logfmt_rejects_go_standard_log() {
573 let line = "2026/07/23 22:02:39 INFO acquired instance lock path=/foo/bar";
576 let parsed = parse(line, "logfmt");
577 assert!(parsed.level.is_none());
579 assert!(parsed.fields_json.is_none());
580 assert_eq!(parsed.message, line);
581 }
582
583 #[test]
584 fn test_logfmt_explicit_empty_value() {
585 let line = r#"level=debug msg="" extra="""#;
588 let parsed = parse(line, "logfmt");
589 assert_eq!(parsed.level.as_deref(), Some("debug"));
590 assert_eq!(parsed.msg.as_deref(), Some(""));
591 let fields: Value = serde_json::from_str(parsed.fields_json.as_deref().unwrap()).unwrap();
592 assert_eq!(fields["msg"], Value::String("".into()));
593 assert_eq!(fields["extra"], Value::String("".into()));
594 }
595
596 #[test]
597 fn test_logfmt_hyphenated_key() {
598 let line = r#"level=info msg="ok" request-id=abc123"#;
600 let parsed = parse(line, "logfmt");
601 assert_eq!(parsed.level.as_deref(), Some("info"));
602 let fields: Value = serde_json::from_str(parsed.fields_json.as_deref().unwrap()).unwrap();
603 assert_eq!(fields["request-id"], Value::String("abc123".into()));
604 }
605
606 #[test]
607 fn test_logfmt_at_prefixed_key() {
608 let line = r#"@level=info @message="server started" port=8080"#;
610 let parsed = parse(line, "logfmt");
611 assert_eq!(parsed.level.as_deref(), Some("info"));
612 assert_eq!(parsed.msg.as_deref(), Some("server started"));
613 let fields: Value = serde_json::from_str(parsed.fields_json.as_deref().unwrap()).unwrap();
614 assert_eq!(fields["port"], Value::Number(8080.into()));
615 }
616
617 #[test]
618 fn test_json_nested_not_extracted_as_msg() {
619 let line = r#"{"level":"info","fields":{"message":"nested"}}"#;
621 let parsed = parse(line, "json");
622 assert_eq!(parsed.level.as_deref(), Some("info"));
623 assert_eq!(parsed.msg, None); }
625}