Skip to main content
node

Logs Are a User Experience Tool If You Let Them Be

Structured logs with userId, sessionId, and requestId turn support investigations from hour-long forensics into two-minute lookups — a user experience decision.


5 min read

Support ticket arrives: "I submitted the form and nothing happened." Without structured logging, this is a 45-minute investigation involving log grepping, deployment timestamps, and asking the user to reproduce the issue. With structured logging that includes userId, sessionId, and requestId, it's a two-minute lookup. That difference is a user experience decision, even though it's entirely a backend engineering choice.


What Structured Logging Actually Means

Unstructured logs look like this:

[2027-01-15T14:23:01Z] ERROR Failed to process payment for user john@example.com

Structured logs look like this:

{
  "level": "error",
  "timestamp": "2027-01-15T14:23:01Z",
  "message": "payment processing failed",
  "userId": "usr_01HK2X",
  "sessionId": "sess_7fGbQ",
  "requestId": "req_3mNpR",
  "paymentProvider": "stripe",
  "errorCode": "card_declined",
  "amount": 4900,
  "currency": "usd",
  "service": "payments-api",
  "environment": "production"
}

The second form is queryable. It's filterable. In Datadog, CloudWatch Logs Insights, Loki, or any log aggregation system, you can run userId = "usr_01HK2X" and see every event that user touched across every service.

The User Context Fields That Change Everything

Three fields should be on every log line in a system that has users:

userId — The authenticated user. If your auth middleware attaches the user to the request, the logger can pull it from there without the application layer doing anything explicit.

sessionId — Not the auth session ID necessarily, but a stable identifier for this browser/device session. Useful for correlating events before and after login.

requestId — A UUID generated per request, propagated to all downstream service calls via headers (X-Request-ID). When a frontend error report includes a requestId, you can find the exact server-side trace that produced it.

Here's a Node.js/Express pattern using pino that attaches all three:

import pino from "pino";
import { v4 as uuid } from "uuid";

const logger = pino({ level: "info" });

export function requestLogger(req, res, next) {
  const requestId = req.headers["x-request-id"] ?? uuid();
  const userId = req.user?.id ?? "anonymous";
  const sessionId = req.session?.id ?? null;

  // Child logger with request context bound — no manual spreading per log call
  req.log = logger.child({ requestId, userId, sessionId, path: req.path });

  // Echo the requestId back so the client can include it in error reports
  res.setHeader("X-Request-ID", requestId);

  next();
}

// Usage anywhere in the request lifecycle:
// req.log.info({ action: "checkout.initiated", cartId }, "user started checkout");

Every log line from that request carries the full context. Zero discipline required from the engineer writing the business logic — they just call req.log.info().

Log User Intent, Not Just System Events

Most logs describe what the system did: "query executed," "cache miss," "email sent." The more useful pattern is logging what the user was trying to do:

// System event — tells you what happened
req.log.info({ query: sql, durationMs }, "database query executed");

// User intent — tells you why it happened
req.log.info({ action: "search.performed", query: searchTerm, resultCount }, "user searched product catalog");

The second form lets you reconstruct the user's session as a narrative: they landed on the homepage, searched for "running shoes," clicked the third result, added it to the cart, and then hit an error at checkout. That narrative is what support and product teams need. System events alone give you fragments.

Log Levels That Match What Different Teams Need

The standard levels — debug, info, warn, error — need to be applied with intent:

  • debug: Internal state, variable values, branch taken. Off in production. Useful in local dev.
  • info: User actions, state transitions, integration calls initiated. Always on. This is your audit trail.
  • warn: Recoverable problems, degraded behavior, deprecated API usage. Alert on sustained rates, not individual occurrences.
  • error: Unrecoverable failures that affected a user. Alert on every occurrence.

Product teams want info logs. They want to know what users do. Engineers want error and warn. Your logging configuration should produce structured output that both audiences can query without a manual.

How This Changes Support Response Time

When a user reports an issue, a good support workflow looks like this:

  1. Ask the user for the timestamp and any error message they saw.
  2. If the UI sends a requestId with error reports (it should), look up that request directly.
  3. If not, query by userId and time range. Get the full session trace in 10 seconds.
  4. The error log has errorCode, paymentProvider, and amount. The support agent can tell the user exactly what failed in the first reply.

That first reply being accurate and specific is a user experience outcome. The user feels heard and gets resolution faster. The engineering cost is a few hours of logging infrastructure setup.

The alternative is logs that tell you the server threw a 500, and nothing else. That's not an observability problem — it's a choice.