How to format json structured log entries for readable analysis
- Step 1Isolate the single log line you care about — From your terminal, log file, or observability UI, copy exactly one JSON log entry — not a block. Save it to a
.jsonfile (the tool takes a file, not a paste box). A whole NDJSON file will not parse. - Step 2Unwrap the message field if the log is double-encoded — Cloud platforms often wrap the original log JSON as a string inside a
messagefield. Extract that inner JSON string first (its\"escapes are the tell), save it as the file, then format that. - Step 3Drop the line onto the prettifier — Drag the
.jsonfile in orbrowse. It is read in-browser viafile.text()— trace IDs and user data stay local. - Step 4Pick 4-space for deep error nesting — Choose
4 spaceswhen the entry has a deeply nestederr/reqtree (easier to follow), or2 spacesfor shallow entries. Those are the only two widths. - Step 5Read the error object first — After prettifying, jump to
err(orerror): itsmessage,type/name, andstacklines diagnose the root cause. Pino encodes levels as integers —"level": 50iserror(see the level table below). - Step 6Compare two lines with key sorting on — To compare a failing and a succeeding request, prettify both with
Sort keys alphabeticallyON so the same fields sit at the same place, making the differing field obvious.
Pino numeric level codes in prettified output
Pino (and Bunyan) write log levels as integers. After prettifying you read the integer directly — here is the mapping. pino-pretty would convert these to labels in a live terminal.
| level | Pino label | Bunyan label | When you see it in an incident |
|---|---|---|---|
| 10 | trace | trace | Very verbose; rarely on in production |
| 20 | debug | debug | Developer detail; usually filtered out |
| 30 | info | info | Normal request completion |
| 40 | warn | warn | Degraded but handled |
| 50 | error | error | The line you are probably reading — has an err object |
| 60 | fatal | fatal (Bunyan: fatal) | Process-ending failure; check err.stack immediately |
Common structured-logger shapes you will format
Where the error and request context live in each logger's JSON. Field names are the defaults; your app may rename them.
| Logger / source | Key error field | Request context | Notes |
|---|---|---|---|
| Pino | err (with type, message, stack) | req, res | level is an integer; time is epoch ms |
| Bunyan | err | req, res | Adds v (format version), hostname, pid |
| Winston (JSON transport) | error or stack | varies (you define meta) | Field names are app-defined; very flexible |
| AWS CloudWatch | inside message (often double-encoded) | inside message | Unwrap the inner JSON string before formatting |
| GCP Cloud Logging | jsonPayload / severity | httpRequest | severity is a string label, not an integer |
What this tool can and cannot do for logs
Set expectations: it formats one well-formed JSON document. Multi-line and field-extraction tasks need a sibling tool.
| Task | This tool? | Use instead |
|---|---|---|
| Prettify one log line | Yes | — |
| Format a whole .ndjson log file | No (parse error) | Format line by line, or wrap lines in [ ] |
Pull just err.stack or $..traceId | No | json-path-extractor |
| Explain why a line won't parse | Shows the position | json-validator for detail |
Strip a leading INFO: text prefix | No (won't parse) | Remove the prefix manually first |
Cookbook
Real log lines made readable. Trace IDs and user data are synthetic.
Pino error line revealing the stack
ExampleA single Pino error entry is unreadable on one line. Prettify at 4-space to expose the nested err object and its stack.
Input (one Pino line):
{"level":50,"time":1718200000000,"msg":"request failed","req":{"method":"GET","url":"/api/orders"},"err":{"type":"TypeError","message":"cannot read id","stack":"TypeError: cannot read id\n at h (a.js:10)"}}
Output (4-space, abbreviated):
{
"level": 50,
"time": 1718200000000,
"msg": "request failed",
"req": { "method": "GET", "url": "/api/orders" },
"err": {
"type": "TypeError",
"message": "cannot read id",
"stack": "TypeError: cannot read id\n at h (a.js:10)"
}
}Unwrap a double-encoded CloudWatch entry
ExampleCloudWatch wraps the app's JSON as a string inside message. Extract the inner JSON first, then prettify it.
CloudWatch event:
{"message":"{\"level\":50,\"msg\":\"db timeout\",\"err\":{\"code\":\"ETIMEDOUT\"}}"}
Step 1 - take the inner string and unescape it:
{"level":50,"msg":"db timeout","err":{"code":"ETIMEDOUT"}}
Step 2 - prettify that:
{
"level": 50,
"msg": "db timeout",
"err": { "code": "ETIMEDOUT" }
}Compare a failing vs succeeding request
ExampleTwo log lines for the same route, one 200 one 500. Sort keys ON aligns fields so the differing field jumps out.
Both prettified, Sort keys ON:
OK (200): FAIL (500):
{ {
"level": 30, "level": 50, <- differs
"req": {...same...}, "req": {...same...},
"res": { "res": {
"statusCode": 200 "statusCode": 500 <- differs
} }
} }A whole NDJSON block won't parse
ExamplePasting several log lines at once fails because NDJSON is not one JSON document. Format a single line, or wrap the lines in an array.
Input (3 log lines):
{"level":30,"msg":"a"}
{"level":30,"msg":"b"}
{"level":50,"msg":"c"}
Result: invalid JSON: Unexpected non-whitespace
character after JSON at position 22
Fix: format one line, OR wrap as an array:
[ {"level":30,"msg":"a"}, {"level":30,"msg":"b"},
{"level":50,"msg":"c"} ]Numeric trace ID rounds — keep it a string
ExampleA 64-bit traceId carried as a number is corrupted on parse. Loggers should emit IDs as strings; prettify confirms the safe shape.
Logged as number (unsafe):
{"traceId":13835058055282163712}
Prettified:
{
"traceId": 13835058055282164000 <- changed!
}
Logged as string (safe):
{"traceId":"13835058055282163712"}
Prettified:
{
"traceId": "13835058055282163712" <- exact
}Errors and edge cases
Real errors and silent failures sourced from each platform's own documentation. Match the wording to the row, fix what the row says to fix.
Pasting a multi-line log block (NDJSON)
Parse errorA log file is one JSON object per line — not a single document. Strict JSON.parse stops after the first object and throws Unexpected non-whitespace character after JSON. Format one line at a time, or wrap the lines you need in a JSON array [ ... ] and prettify that array.
Log line has a leading text prefix
Parse errorLines like 2026-06-12 INFO {"level":30,...} or app | {"msg":...} are not pure JSON — the prefix breaks the parser. Strip everything before the first { (or after the JSON) so the file contains only the JSON object, then prettify.
64-bit trace/span ID stored as a number
Silent precision lossA traceId/spanId above 2^53−1 is rounded on parse, so the prettified value differs from the real ID — and you could chase the wrong trace. Most tracers emit IDs as strings to avoid this; if yours doesn't, treat the displayed number as approximate and pull the raw value from the source line.
Truncated / interleaved log line
Parse errorHigh-throughput logs sometimes truncate a line at a buffer boundary or interleave two writers. The result is incomplete JSON and the parser reports where it broke (e.g. Unexpected end of JSON input). That error is itself a signal — grab a clean copy of the line from the source, not the rendered console.
Double-encoded message field
Needs unwrapCloud platforms store the app's JSON as an escaped string inside message/log. Prettifying the outer object shows the inner JSON as one long escaped string. Extract and unescape the inner string into its own file, then prettify that to read the real structure.
Pino level shown as an integer
ExpectedPino and Bunyan write level as a number (30 = info, 50 = error). The prettifier does not translate it to a label — that is pino-pretty's job in a live terminal. Use the level table in this guide to read the code. This is expected behavior, not a defect.
Epoch timestamp left as a raw number
Expectedtime and ts are usually epoch milliseconds (e.g. 1718200000000). The prettifier keeps them as numbers — it does not convert to a human date. Note that very large epoch nanoseconds (19 digits) would exceed safe-integer range and round; epoch milliseconds (13 digits) are safe.
Sort keys hides the natural level/msg ordering
Order changedPino conventionally leads with level, time, pid, hostname, msg. Turning Sort keys on alphabetizes that, so err and level jump around. It is useful for two-line comparison but not for scanning a single entry — leave it off when reading one line top to bottom.
Stack trace newlines stay escaped in strings
By designA stack value is a single JSON string containing \n escapes. Prettifying indents the object but does not turn the \n inside the string into real line breaks — JSON string contents are not reflowed. To read the stack as multiple lines, copy the string value and unescape it separately.
Log line larger than 2 MB
Rejected — over tier limitA single log line over 2 MB (rare, but happens with embedded payloads or base64 blobs) is blocked on free tier; Pro raises the limit to 100 MB. Very long lines also hit the 5,000-character preview truncation — display only; Copy/Download return the full line.
Frequently asked questions
How do I read Pino log levels after prettifying?
Pino encodes levels as integers: 10 trace, 20 debug, 30 info, 40 warn, 50 error, 60 fatal. The prettified output shows the number (e.g. "level": 50 for an error); use the level table in this guide to map it. In a live terminal, node app.js | npx pino-pretty converts the numbers to colored labels.
Can I prettify a whole log file at once?
No — a log file is NDJSON (one object per line), which is not a single JSON document and will throw Unexpected non-whitespace character after JSON. Format one line at a time, or wrap the specific lines you need in a JSON array [ ... ] and prettify that.
Why won't my log line parse?
Usually one of three reasons: a non-JSON prefix (timestamp or app |), the line is truncated/interleaved, or you pasted more than one line. Strip everything outside the single JSON object and ensure it is exactly one complete entry. The error message includes the position where parsing broke.
How do I handle CloudWatch logs where JSON is inside a message field?
Those are double-encoded: the app's JSON is an escaped string inside message. Extract that inner string, unescape it, save it as the file, then prettify. CloudWatch Logs Insights can also help with parse and display commands, but for one line, unwrapping and prettifying is fastest.
Will my trace ID stay exact?
Only if it is a string or a number ≤ 2^53−1. A 64-bit numeric traceId/spanId is rounded on parse and the displayed value will be wrong. Most tracers emit IDs as strings precisely to avoid this — if yours uses numbers, read the exact value from the raw source line.
Can I extract just the error stack from a log line?
Not with the prettifier — it formats the whole document. Use json-path-extractor with a path like $.err.stack (or $..stack to find it at any depth) to pull just that field, then read or unescape it.
Does prettifying turn the \n in a stack trace into real line breaks?
No. The stack is a single JSON string; prettifying indents the surrounding object but leaves the string's contents (including \n) intact. To see the stack as multiple lines, copy the string value out and unescape it in an editor or with a small script.
Can I compare two log lines side by side?
Yes — prettify each line with Sort keys ON so identical fields land in the same position, then diff the two outputs. For a structural comparison rather than a textual one, run them through json-diff.
Does it convert epoch timestamps to dates?
No — time/ts stay as the raw epoch number. Prettifying is whitespace-only for numbers (plus canonicalization). Convert epochs to dates separately if needed. Epoch milliseconds (13 digits) are within safe-integer range; epoch nanoseconds (19 digits) would round.
Is my log data uploaded when I format it?
No. The line is read with file.text() and parsed/formatted in your browser. Error messages, trace IDs, session tokens, and user data in the entry never reach JAD Apps servers. Only an anonymous run counter is stored when you are signed in.
What if the log line is JSONC or has a comment?
Real structured-log JSON is strict and never has comments, so this is uncommon. If a hand-edited line does, strict parse rejects it — repair with json-format-fixer first. But double-check you are looking at a genuine log line and not a config snippet.
Can I automate log formatting in a triage script?
For live terminals, pipe through pino-pretty. For batch local processing on Pro, the json-prettifier runner mirrors this tool (indent 2/4, sortKeys, trim-then-parse) and keeps log data on your machine. To carve specific fields out of many lines, script around json-path-extractor.
Privacy first
Conversion runs locally in your browser. No file is uploaded — only metadata counters are saved for signed-in dashboard stats.