Guide

Structured Logging: Stop Grep-ing Through Plain Text

Plain text logs break the moment you need to filter, aggregate, or alert. Structured logging with JSON gives you queryable, machine-readable logs from day one.

LogFlow TeamJuly 14, 20267 min read

You've been there. Production is down, someone says "check the logs," and you're running grep "error" app.log | tail -100 on a server. You find 300 lines that say "error" but can't tell which user was affected, which endpoint failed, or when it started.

This is the plain text logging trap — easy to start, impossible to scale.

What Structured Logging Actually Means

Instead of this:

2026-07-14 09:23:15 ERROR Payment failed for user 4821 on order ORD-9912

You emit this:

{
  "timestamp": "2026-07-14T09:23:15.442Z",
  "level": "error",
  "message": "Payment failed",
  "service": "billing",
  "userId": 4821,
  "orderId": "ORD-9912",
  "provider": "stripe",
  "errorCode": "card_declined",
  "latencyMs": 2340
}

Same event, but now every field is queryable. You can filter by level = error AND service = billing, aggregate error rates by provider, or alert when errorCode = card_declined spikes above a threshold.

Why Plain Text Falls Apart

Plain text works for one developer on one service. It breaks as soon as you have:

Multiple services. Each team formats logs differently. One writes [ERROR], another writes level=error, a third uses severity: 3. Good luck correlating across them.

Alerting needs. You want to fire a Slack alert when error rate exceeds 5% for a service. With plain text you need fragile regex parsing. With structured logs you query level = error GROUP BY service.

Debugging under pressure. At 3am you need to find all requests for a specific user that hit a specific endpoint in the last hour. Structured logs: one query. Plain text: prayer.

How to Implement It

Node.js with Pino

Pino is the fastest Node.js logger and outputs JSON by default:

import pino from 'pino'

const logger = pino({
  level: process.env.LOG_LEVEL || 'info',
})

// Every call produces structured JSON
logger.info({ userId: 4821, action: 'login' }, 'User authenticated')
logger.error({ orderId: 'ORD-9912', err }, 'Payment processing failed')

Python with structlog

import structlog

logger = structlog.get_logger()

logger.info("user_authenticated", user_id=4821, action="login")
logger.error("payment_failed", order_id="ORD-9912", error=str(e))

Go with slog (standard library)

Go 1.21+ includes structured logging in the standard library:

slog.Error("payment failed",
    "userId", 4821,
    "orderId", "ORD-9912",
    "error", err,
)

The Five Fields Every Log Should Have

Regardless of language or framework, include these in every log entry:

Field Why
timestamp ISO 8601 with milliseconds. Without this, you can't correlate events across services.
level debug, info, warn, error, fatal. Enables filtering and alerting.
message Human-readable summary of the event. Keep it short and consistent.
service Which service emitted the log. Essential in microservice architectures.
trace_id Correlation ID that follows a request across services. Makes distributed debugging possible.

Everything else — userId, orderId, latencyMs — goes into contextual fields specific to the event.

Common Mistakes

Logging entire request/response bodies. This bloats volume, risks leaking PII, and makes search slower. Log the fields you need, not the whole object.

Inconsistent field names. If one service uses user_id and another uses userId, aggregation breaks. Pick a convention and enforce it.

Stringifying everything. { "latency": "234" } is a string. { "latency": 234 } is a number you can aggregate with AVG(). Types matter.

Logging at the wrong level. If you log every successful HTTP request at info level, you bury the signal in noise. Consider using debug for successful requests and info only for meaningful state changes.

From Structured Logs to Actionable Insights

Once your logs are structured, a log management tool can:

  • Search by any field — find all errors for a specific user in seconds
  • Build dashboards — error rate by service, p99 latency over time, top error messages
  • Set up alerts — get notified when error rate spikes, a service goes silent, or a new error pattern appears
  • Correlate with traces — follow a request across services using trace_id

This is exactly what LogFlow is built for. Ship structured JSON logs via our SDK, REST API, or OpenTelemetry, and you get real-time search, anomaly detection, and alerts without managing any infrastructure.

Getting Started

If you're starting fresh, the fastest path:

  1. Pick a structured logger for your language (Pino, structlog, slog, Serilog)
  2. Define your five core fields and enforce naming conventions
  3. Send logs to a centralized platform instead of files on disk
  4. Set up one alert on error rate — you'll be surprised what you catch

The gap between "we have logs" and "our logs are useful" is structured logging. Once you cross it, you won't go back.

Start monitoring your logs today

Free plan available. No credit card required. Up and running in 2 minutes.

Get started free