Emit JSON Logs with Pino and Attach a Request ID to Every Line
Structured Logging
Replace ad-hoc console.log calls with koa-pino-logger to emit structured JSON logs enriched with a request ID, HTTP method, path, status, and response time.
What you'll learn
- Add koa-pino-logger as request-logging middleware
- Attach a unique request ID to every log line via ctx.state
- Configure log levels for development versus production
console.log is fine for development but becomes a liability in production:
messages are unstructured, hard to query, and carry no request context. Pino is
the fastest production logger for Node.js and emits newline-delimited JSON that
log aggregators (Loki, Datadog, CloudWatch) can index directly.
Installation
npm i koa-pino-logger pino pino-pretty Wiring Up the Logger
Register koa-pino-logger before your router so every request is timed and
logged on the way out.
import Koa from "koa";
import pinoLogger from "koa-pino-logger";
const app = new Koa();
app.use(
pinoLogger({
level: process.env.LOG_LEVEL ?? "info",
transport:
process.env.NODE_ENV !== "production"
? { target: "pino-pretty" }
: undefined,
})
);
// … router, other middleware …
app.listen(3000); Each request now produces one JSON line after the response is sent:
{"level":30,"time":1716038400000,"req":{"method":"GET","url":"/ping"},"res":{"statusCode":200},"responseTime":4,"msg":"request completed"}
Adding a Request ID
A request ID lets you correlate every log line for a single request — invaluable when debugging concurrent traffic.
import { randomUUID } from "node:crypto";
app.use(async (ctx, next) => {
ctx.state.requestId =
ctx.get("X-Request-Id") || randomUUID();
ctx.set("X-Request-Id", ctx.state.requestId);
await next();
});
app.use(
pinoLogger({
genReqId: (req, res) => res.getHeader("x-request-id"),
})
); Logging Inside Route Handlers
koa-pino-logger attaches a child logger to ctx.log. Use it instead of
console.log so child log lines inherit the request ID automatically.
router.get("/orders/:id", async (ctx) => {
ctx.log.info({ orderId: ctx.params.id }, "fetching order");
const order = await db.findOrder(ctx.params.id);
if (!order) {
ctx.log.warn({ orderId: ctx.params.id }, "order not found");
ctx.throw(404, "Order not found");
}
ctx.body = order;
}); Log Levels
| Level | Numeric | When to use |
|---|---|---|
trace | 10 | Verbose debugging — disable in production |
debug | 20 | Development diagnostics |
info | 30 | Normal request flow (default) |
warn | 40 | Recoverable problems |
error | 50 | Unhandled errors, caught exceptions |
fatal | 60 | Process must exit |
Set LOG_LEVEL=debug in development and LOG_LEVEL=warn or LOG_LEVEL=info
in production to control verbosity without a code change.
Up Next
Expose health and readiness endpoints and track real-time metrics with prom-client.
Monitoring and Metrics →