Skip to main content
Cloud DevOps Solutions
Get a quote Talk to an engineer

Logging & Tracing

Structured Logging: Standards Your Engineers Will Actually Follow

Structured logging means writing each log event as machine-readable key-value data, usually JSON, with a consistent set of fields such as level, service, version and trace ID. We write the standard, provide libraries and examples per language, and train your teams to adopt it.

Key takeaways

  • One JSON object per line with agreed field names lets every downstream tool search, filter and alert without custom parsing.
  • A short mandatory field set beats a long optional one: timestamp, level, service, env, version, message and trace ID.
  • Log levels need written meaning, or error becomes noise and nobody trusts alerts.
  • Sensitive data rules belong in the standard from day one, not after an audit.
  • Adoption is a people task: examples, lint rules, review checklists and a short workshop matter more than the document.

Why unstructured logs hold everything back

Free-text logs are written for the person who wrote the code. “Failed to process order 8471 for user bob after 3 retries” reads fine, until you need to count failures per customer, alert on retries, or route by severity. Then someone writes a regular expression, another service words its message differently, and the dashboard works for half your fleet.

Every other part of our logging, ELK, Loki and OpenTelemetry service gets easier when logs are structured: dashboards, routing, alerting and PII scrubbing. That is why this guideline work often comes first, even though it is the least technical-looking item in the list.

What good looks like

  • Every service writes one JSON object per line to standard output.
  • A small set of fields is present everywhere with identical names and types.
  • Levels mean the same thing across teams.
  • Messages are stable strings; variable data lives in fields.
  • Secrets, tokens and personal data never appear, by rule and by test.

The standard we write

Mandatory fields

FieldExampleWhy
timestamp2026-10-08T09:14:03.221ZSingle ISO 8601 UTC format
levelerrorRouting and alert filters
servicecheckout-apiOwnership and grouping
envproductionSeparates environments
version1.42.0Compare releases
messagepayment authorisation failedStable human summary
trace_id32 hex charactersJoins logs to traces

Optional but recommended: span_id, request_id, http.method, http.status_code, duration_ms, error.type, error.stack. Use consistent names with OpenTelemetry semantic conventions where they exist, which keeps logs and traces aligned. See OpenTelemetry for background.

Message rules

Keep messages constant and put variables in fields. Compare these two:

{"level":"error","message":"payment authorisation failed","order_id":"8471","provider":"stripe","attempt":3,"duration_ms":1840,"error.type":"timeout"}

versus “Payment for order 8471 failed after 3 attempts (timeout)”. The first can be grouped by message, counted by provider and alerted on by attempt count. The second cannot without parsing.

Levels

We define these in one paragraph each and put them in the repository. Errors must be actionable. If a condition is expected, such as a user mistyping a password, it is not an error. Warnings are for degradation that is handled. Info is sparing: a service handling thousands of requests should not log several info lines for every one in production. Debug stays off by default.

How it looks per language

The goal is a tiny shared setup so developers never hand-build JSON.

// Go (standard library log/slog)
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo})).
    With("service", "checkout-api", "env", os.Getenv("ENV"), "version", version)
logger.Error("payment authorisation failed", "order_id", orderID, "provider", "stripe")
# Python (structlog)
import structlog
structlog.configure(processors=[
    structlog.contextvars.merge_contextvars,
    structlog.processors.add_log_level,
    structlog.processors.TimeStamper(fmt="iso", utc=True),
    structlog.processors.JSONRenderer(),
])
log = structlog.get_logger().bind(service="checkout-api")
log.error("payment authorisation failed", order_id="8471")

For Node we usually use pino, which writes JSON by default and is fast. For Java we configure Logback or Log4j2 with a JSON encoder and place identifiers in the MDC. In each case a small middleware adds request_id and trace identifiers to every line within a request, so developers do not repeat them. Library and option names should be checked against the versions you run.

Decisions and trade-offs

JSON or logfmt. JSON is universally supported and handles nesting. Logfmt is more readable in a terminal. We default to JSON and give engineers a local pretty-printer for development.

Nested or flat fields. Flat dotted names such as http.status_code index predictably in Elasticsearch and work as Loki parsed fields. Deep nesting complicates mappings.

Stack traces. Put them in a single field as a string, not many lines, so they stay one event. This avoids needing the multiline parsers described on our Fluent Bit page.

Retrofit or leave legacy alone. Migrating a stable legacy service may cost more than it returns. We convert legacy output at the collector where possible and apply the standard to all new and actively changed code.

Common failure modes

  • Field type drift. One service logs user_id as a number, another as a string, and Elasticsearch rejects documents. A schema and a type check prevent it.
  • Cardinality in labels. Turning high-variance fields into Loki labels causes trouble. Only a few stable fields become labels.
  • Logging whole request bodies. This leaks personal data and inflates volume. The standard bans it by default.
  • Everything is info. Without level discipline, filters are useless. Code review checks levels.
  • Standard without enforcement. A short CI check that parses sample output keeps it alive.

Adopting the standard in an existing codebase

Most teams already have thousands of log statements, and rewriting them all is neither realistic nor necessary. We take a staged approach that delivers value early.

First, switch the output format at the logger configuration level, so every existing message becomes valid JSON with the mandatory fields filled in by the framework. Free text stays in the message field, but timestamps, level, service name and request identifier are now consistent everywhere. This single change usually gives the largest gain for the least effort.

Second, fix the worst offenders. Using search over your existing logs, we list the highest-volume messages and the ones involved in recent incidents, and rewrite those with proper fields rather than interpolated strings.

Third, add the checklist to code review so new code arrives in the right shape. Old statements are improved opportunistically when someone touches them.

What not to convert

Some logs are best left alone. Third-party libraries often write their own formats, and it is cheaper to parse those in the collector than to patch them. Logs from legacy components that will be retired soon do not need a rewrite. The aim is a searchable, consistent system, not a perfect one.

Measuring adoption

We track the share of log lines that parse cleanly as JSON with all mandatory fields present, broken down by service. A simple query on that ratio shows which teams need help and gives leadership a number that moves as the work progresses. Services below an agreed threshold are listed in the sprint review until they catch up.

Delivery and handover

You receive the written standard (kept short), the starter configuration for each of your languages, a review checklist, and a workshop for your engineers. We also provide a validation script that can run in CI. For the wider engineering culture around operations, see how this ties to Managed DevOps practices.

How to tell it is working

The share of services emitting valid structured logs rises, the volume of unparsed lines falls, dashboards need no per-service exceptions, and engineers can find a failing request using one identifier. If you want a guideline fitted to your stack, book a consultation.

References

How we deliver it

  1. 01

    Audit current logging

    We sample real log output from each service and note formats, levels, noise and leaked sensitive values.

  2. 02

    Define the schema

    A small mandatory field set, naming conventions, level definitions and rules for errors and sensitive data.

  3. 03

    Provide language starters

    Ready logger configuration for Go, Python, Node and Java, with context and trace ID injection built in.

  4. 04

    Pilot on two services

    We migrate a pair of services, check dashboards and parsers, and fix the guidelines before wider rollout.

  5. 05

    Run team onboarding

    A practical workshop, review checklist and CI check help engineers apply the standard day to day.

  6. 06

    Measure adoption

    We track the share of services emitting valid structured logs and the volume of unparsed lines.

Tools and technologies

slogzapzerologstructlogpython-json-loggerpinowinstonLogbackLog4j2OpenTelemetryJSON SchemaFluent Bit

Frequently asked questions

What is structured logging?
Structured logging writes each event as key-value data rather than a sentence. A line like a JSON object with level, service, message and request ID can be filtered and aggregated directly. Plain text needs regular expressions to extract the same facts, and those break whenever wording changes.
Which fields should every log line have?
At minimum: timestamp, level, service name, environment, version, a message, and trace and span IDs when a request is in flight. Add domain fields like order or tenant identifiers where safe. Keep the mandatory set small so engineers actually comply.
What should each log level mean?
Error: something failed and a person may need to act. Warn: unexpected but handled. Info: significant normal events such as startup or completed business operations. Debug: detail for developers, off by default in production. Writing these down and enforcing them prevents alert fatigue.
Does JSON logging cost more to store?
Field names add bytes per line, but compression handles repeated keys well. The larger effect is usually behavioural: structured logs make it easier to cut noise and route by level, which saves more than the format costs. Loki and Elasticsearch both handle JSON well.
How do you get developers to follow the standard?
Make the right way the easy way: shared logger setup, copy-paste examples, a lint or test that checks required fields, and a review checklist. A one-hour workshop using your own services as examples works better than a long document nobody reads.

Talk to an engineer about structured logging guidelines

Book a free 30-minute call. We review your current setup and tell you honestly whether and how we can help.

Written by Saddique Khan, DevOps / SRE / Cloud Consultant Engineer. Last updated 2026-10-08.