Notes · Sep 5, 2026

logfmt vs JSON logs: what a parser actually needs from a line

Both formats carry the same four things. They differ in what happens to the fifth — the line that does not fit.

The four things

A log line becomes a record when a parser can find a timestamp, a level, a message, and the fields — the key=value pairs that follow the message in logfmt, or the object keys in JSON lines. With those, a run of lines becomes rows you can filter by level, search by field, and compare against another run by record rather than by line order.

checkout-api.log
2026-04-21T14:32:08.412Z INFO  [checkout-api] req=b29f0c1a stripe.charge.completed amount=17054 latency=88ms
2026-04-21T14:32:07.940Z WARN  [checkout-api] req=4471dd90 retry.scheduled attempt=2 backoff=400ms
2026-04-21T14:32:07.813Z ERROR [checkout-api] req=9c02fe13 stripe.webhook.signature_invalid
2026-04-21T14:32:07.512Z INFO  [checkout-api] req=08bb31f7 cart.items.priced items=2 total=14900
…

Where logfmt falls short

logfmt is readable in a terminal and cheap to write, and a parser has to guess its shape: which token is the level, where the message ends and the pairs begin, whether a value with spaces was quoted. A stack trace breaks it outright — the continuation lines match no pattern. The honest answer is a pattern shown before it is applied, a match count, and the unmatched lines kept rather than dropped.

unstringify scores a pasted log against 9 built-in shapes — Kibana Discover · copied rows, Kibana document · field per line, Apache / nginx combined, syslog · RFC 5424, syslog · RFC 3164, Rails / Ruby logger, bracket prefix + JSON tail, timestamped logfmt, logfmt — and shows the winner as chips with a live preview: “matches N of M lines · K kept as raw”. A regex of your own replaces the guess, and a pattern you wrote is saved for the next file that matches it.

Where JSON lines fall short

JSON lines need no pattern: every line is a document, every key is a field. What they lose is readability, and a single broken line — a log rotated mid-write, a payload with a raw newline — is a broken document if you treat the file as one JSON array. Treat each line on its own and the damage stays on that line. The five-line sample on the NDJSON page has 1 broken line; 4 read, one is counted and shown, and “broken only” filters to it.

checkout-api.ndjson
{"ts":"2026-04-21T14:32:08.412Z","level":"info","msg":"stripe.charge.completed","amount":17054,"order_id":"ord_8f3aa19c4d"}
{"ts":"2026-04-21T14:32:07.940Z","level":"warn","msg":"retry.scheduled","attempt":2,"backoff_ms":400}
{"ts":"2026-04-21T14:32:07.813Z","level":"error","msg":"stripe.webhook.signature_invalid","payload":"{\"event\":\"charge.succeeded\",\"id\":\"evt_1PZ3xK\"}"}
{"ts":"2026-04-21T14:32:07.512Z","level":"info","msg":"cart.items.priced","items":2,"total":14900
…

Which to write

If a machine reads your logs first — a collector, a search index — write JSON lines and keep the payload fields flat. If a person reads them first — a terminal, a tail — write logfmt with the timestamp and level first, and quote values that hold spaces. Either way, keep one record per line: the parser, whichever it is, is counting lines.

Try it

Try it on the file that broke your afternoon.

free while in alpha · no signup