Skip to content

Structured Logging

You want to send structured JSON logs to a production logging pipeline but ergo exposes two distinct logging integration surfaces:

  1. logger() middleware — accepts a log callback function (info) => void called on response finish with the full structured log entry
  2. graceful() utility — accepts a log object with .info(), .warn(), .error() methods for lifecycle events

You need to wire a real structured logger (pino or winston) to both surfaces, correlate request IDs across application logs, and enrich log output with OpenTelemetry trace context.

import http from "node:http";
import pino from "pino";
import { handler, compose, logger } from "@centralping/ergo";
const log = pino({ level: "info" });
const pipeline = compose(
{fn: logger({
log: (info) => log.info(info),
error: (info) => log.error(info),
}), setPath: "log"},
// ... remaining middleware
);
const server = http.createServer(handler(pipeline));
server.listen(3000);
import http from "node:http";
import winston from "winston";
import { handler, compose, logger } from "@centralping/ergo";
const log = winston.createLogger({
level: "info",
format: winston.format.json(),
transports: [new winston.transports.Console()],
});
const pipeline = compose(
{fn: logger({
log: (info) => log.info({ message: "request completed", ...info }),
error: (info) => log.error({ message: "request error", ...info }),
}), setPath: "log"},
// ... remaining middleware
);
const server = http.createServer(handler(pipeline));
server.listen(3000);

Access the request ID from acc.log.requestId in your execute handler to correlate application-level logs with the HTTP request:

router.get("/users", {
execute: async (req, res, acc) => {
const { requestId } = acc.log;
log.info({ requestId, msg: "fetching users from database" });
const users = await db.query("SELECT * FROM users");
log.info({ requestId, msg: "query complete", count: users.length });
return { response: { body: users } };
},
});

For high-volume services, create a child logger scoped to the request:

router.get("/users", {
execute: async (req, res, acc) => {
const reqLog = log.child({ requestId: acc.log.requestId });
reqLog.info("fetching users from database");
const users = await db.query("SELECT * FROM users");
reqLog.info({ msg: "query complete", count: users.length });
return { response: { body: users } };
},
});

When OpenTelemetry is configured, place tracing() before logger() in the pipeline so that acc.trace.traceId and acc.trace.spanId are available for log enrichment:

import { handler, compose, tracing, logger } from "@centralping/ergo";
const pipeline = compose(
{fn: tracing(), setPath: "trace"},
{fn: logger({
log: (info) => log.info(info),
}), setPath: "log"},
// ... remaining middleware
);

When tracing() is placed before logger() as shown above, the info object passed to log automatically includes traceId and spanId from acc.trace — no manual enrichment is needed.

SurfaceOption keyTypeCalled withUse case
logger() middlewarelog / error(info) => voidFull structured log entry on response finishPer-request HTTP logging
graceful() utilitylog{info, warn, error}String messages during lifecycle eventsServer startup/shutdown logging

Both surfaces accept the same logger instance in ergo-router — a pino or winston logger satisfies both because it has method-based .info(), .warn(), .error() (for graceful()) and can be wrapped in a callback (info) => logger.info(info) (for logger()).

The logger() middleware stores a data snapshot at acc.log containing request metadata (requestId, method, URL, IP, host info). This is the accumulated domain value — the same pattern as acc.url, acc.cookies, or acc.route. It does not have logging methods.

To log from an execute handler, use your own logger instance and reference acc.log.requestId for correlation.

Incoming request
logger() middleware
├── Resolves requestId (res header → req header → uuid())
├── Stores metadata at acc.log
└── Registers response-finish callback
execute handler
├── Reads acc.log.requestId
└── Uses own logger instance with requestId for correlation
Response finish
└── logger() calls log(info) with full entry including duration
  • JSON output: Use pino() (JSON by default) or winston.format.json() for machine-parseable log output
  • Log levels: Set level: "info" in production; use "debug" in development for verbose output
  • Transport separation: Use pino transports or winston transports to route logs to files, external services, or stdout for container environments
  • Sensitive data: The logger() middleware automatically redacts authorization, proxy-authorization, cookie, and set-cookie headers — no additional configuration needed