Structured Logging 101: Why JSON Won

Structured logging means emitting logs as JSON objects instead of text lines. Here's why JSON won, and the conventions that make logs actually useful.

JR2 min read

Every team starts with printf-style logging: interpolate some values into a string, ship it, grep it when something breaks. And every team eventually hits the wall - the question they need to answer isn't "find this string" anymore, it's "count errors by route over the last hour," and grep can't do that.

That's the moment to switch to structured logging.

What structured logging is#

Unstructured logging encodes information into prose:

User user_58231 purchased plan=pro for $49

Structured logging emits the same event as an object with named fields:

{
  "event": "purchase",
  "user_id": "user_58231",
  "plan": "pro",
  "mrr": 49
}

The information is identical. The difference is that every field is machine-addressable without parsing anything - no regexes, no praying the log format never changes.

Why JSON specifically#

Structured logging could use any format, but JSON won for practical reasons: every programming language speaks it natively, it's schemaless (add a field whenever you want, no migration), it handles nesting and arrays, and it stays readable enough for humans eyeballing a raw stream. The entire tooling ecosystem - collectors, search engines, analytics databases - assumes it.

The conventions that matter#

The format is the easy part; consistency is what makes logs useful:

  • Stable field names. user_id everywhere, never uid in one service and userId in another.
  • A level and a timestamp. level (info/warn/error) and an ISO-8601 UTC timestamp on every entry.
  • Flat, focused payloads. Log what you'll query or debug with; skip the rest.
  • No PII or secrets. Logs are long-lived and broadly readable - treat every field as public inside your company.

These overlap heavily with analytics events, so the full list lives in our event tracking best practices.

Where structure pays off#

With fields instead of prose, "how many 500s did each endpoint return today?" becomes a GROUP BY route instead of an afternoon with awk. Latency percentiles per function, error counts per deploy, traffic per customer - all become one-line queries. This is also why the storage layer matters: columnar databases are dramatically better at these aggregations than search indexes, as we covered in ClickHouse vs Elasticsearch for log analytics.

The dead-simple version#

You don't need an agent, a collector, or a pipeline to start. With GraphJSON, a structured log line is one HTTP POST:

curl -X POST https://api.graphjson.com/api/log \
  -H "Content-Type: application/json" \
  -d '{"api_key": "<your-api-key>", "collection": "api_events", "json": "{\"event\": \"api_request_completed\", \"route\": \"/checkout\", \"status\": 500}"}'

Every field can become queryable, chartable, and alertable without a daily ETL rebuild. See the documented freshness and service boundaries. If you're on Vercel, the log drain integration streams your platform logs in with zero code at all.

JR

Written by JR

Founder and builder of GraphJSON.