← Alle Neuigkeiten
devops4 Min. Lesezeit

Structured logging is the gap between a vibe-coded app and a production app

Von Seaworthy · 1. Juni 2026

A monitoring screen displaying system metrics and analytics dashboards

Photo by Stephen Dawson on Unsplash

Here is an honest description of the logs from most AI-generated Express apps I've looked at: a wall of console.log('User logged in') calls, some console.error(err) sprinkled in for drama, and nothing else. This works fine on a developer's laptop. It is completely useless in production.

Log aggregators like Datadog, Grafana Loki, and AWS CloudWatch Logs Insights work by parsing log lines into fields they can query. Give them a plain string and they store it as an opaque blob. You can grep for it, slowly, if you know exactly what you're looking for. You cannot filter by severity, correlate with a request ID, or set up an alert that triggers when error rates spike. Structured logging fixes all of this.

What structured logging actually means

Structured logging means emitting each log entry as a JSON object with consistent, known fields. Instead of:

User 42 logged in from 203.0.113.5

you emit:

{"timestamp":"2026-06-01T10:23:44.112Z","level":"info","msg":"user login","userId":42,"ip":"203.0.113.5","requestId":"req-f3a9c","service":"auth-api","version":"1.4.2"}

Every field is queryable. Your log aggregator can now answer "show me all login events for user 42 in the last hour" or "how many logins failed in the last five minutes" without you writing any special parser.

Migrating from console.log to pino

Pino is the right choice for Express apps. It is fast, it emits valid JSON by default, and it has a well-maintained ecosystem of transports. The migration is not complicated.

npm install pino pino-http
import express from 'express';
import pino from 'pino';
import pinoHttp from 'pino-http';

const logger = pino({
  level: process.env.LOG_LEVEL ?? 'info',
  base: {
    service: 'my-api',
    version: process.env.npm_package_version ?? 'unknown',
  },
});

const app = express();

// Attach pino-http before your routes.
// This adds req.log to every request: a child logger pre-bound
// with the request ID and HTTP metadata.
app.use(
  pinoHttp({
    logger,
    genReqId: (req) =>
      (req.headers['x-request-id'] as string) ??
      crypto.randomUUID(),
  })
);

app.get('/users/:id', async (req, res) => {
  // Use req.log, not the top-level logger, so the requestId is included.
  req.log.info({ userId: req.params.id }, 'fetching user');

  try {
    const user = await db.getUser(req.params.id);
    res.json(user);
  } catch (err) {
    req.log.error({ err, userId: req.params.id }, 'user fetch failed');
    res.status(500).json({ error: 'internal server error' });
  }
});

pino-http automatically logs every request and response, including status code, response time, and any error. You get that for free. The only thing you need to add manually is business-relevant context: which user, which resource, what happened.

The mandatory fields

Every log entry needs at minimum four fields. timestamp in ISO 8601 UTC. level as a string (trace, debug, info, warn, error, fatal). request_id so you can pull every log line for a single HTTP request. service and version so that when you have more than one deployed component, you know which one produced the log.

Pino adds timestamp and level automatically. You add service and version in the base config (as above). request_id comes from pino-http's genReqId. That is the whole setup.

One thing worth getting right from the start: always prefer req.log over the module-level logger inside a request handler. The child logger is already bound to the request context. Using the top-level logger loses the requestId, which makes the logs almost as useless as plain strings.

Correlating across services with traceparent

If you have more than one service, you need a way to follow a single user request across all of them. The W3C traceparent header is the standard way to do this.

When a request arrives, read the traceparent header if it exists, otherwise generate one. Pass it downstream in every outbound HTTP call. Log it alongside the request_id.

app.use((req, res, next) => {
  const traceparent =
    (req.headers['traceparent'] as string) ??
    `00-${crypto.randomUUID().replace(/-/g, '')}-${crypto.randomUUID().slice(0, 16).replace(/-/g, '')}-01`;

  // Store it so route handlers can attach it to outbound calls.
  res.locals.traceparent = traceparent;
  res.setHeader('traceparent', traceparent);

  req.log = req.log.child({ traceparent });
  next();
});

Now Datadog or Grafana can correlate every log line across every service involved in handling that one request. This is the difference between "something went wrong somewhere" and "here is exactly what happened, in order, across all three services."

Log levels without over-logging

I see two failure modes with log levels. The first is logging everything at info. The second is not logging errors because you are worried about noise. Both are wrong in the same direction: they make your logs useless.

The right approach is narrow. Use trace for internal state you only want during active debugging and which you will never turn on in production. Use debug for the next level down, things a developer might turn on temporarily to diagnose a specific issue. Use info for significant business events: user created, payment processed, job completed. Use warn for things that are wrong but recoverable: a retry succeeded, a circuit breaker opened. Use error for failures that need human attention.

fatal is for situations where the process cannot continue and is about to exit. It should fire once and then the process should stop.

The critical rule: never log inside a tight loop at info or above unless you have a rate limit in place. Logging every row of a 10,000-row database result will cost you real money in log ingestion bills and will bury the signals you actually care about.

Structured logging is not glamorous work. It is the kind of thing that takes an afternoon and then silently saves you hours every time something breaks in production. Seaworthy flags missing structured logging as an ops basics issue in the projects it scans.

Dieser Artikel wurde von KI generiert und fasst öffentlich verfügbare Quellen zusammen.

Von den Machern von Seaworthy

Seaworthy scannt dein Repository nach den Problemen, die in Artikeln wie diesem behandelt werden.

Sicherheitslücken, offengelegte Geheimnisse und Fehlkonfigurationen — erkannt, bevor du bereitstellst. Kostenlos, kein Konto nötig.

$ npx seaworthycode