console.log('Order created') in production means no request ID, no log level, no JSON format, and no way to trace what happened when something breaks at 3am.
Claude Code generates pino structured logging from CLAUDE.md rules — so you get observability that actually works.
The CLAUDE.md Rules
## Logging Rules
- Use pino (fast, JSON output, production-ready)
- No console.log or console.error — use logger exclusively
- pino-http for automatic HTTP request/response logging
- X-Request-ID header on all requests (generate if missing)
- All logs must include request_id
- Log levels: trace/debug/info/warn/error/fatal
- NEVER log passwords, tokens, credit card numbers, or PII
- Use pino redact for automatic sensitive field removal
These constraints give Claude Code enough context to generate a logger that's safe by default.
logger.ts
import pino from 'pino';
export const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
redact: {
paths: [
'password',
'token',
'accessToken',
'refreshToken',
'authorization',
'req.headers.authorization',
'req.headers.cookie',
'*.password',
'*.token',
],
censor: '[REDACTED]',
},
...(process.env.NODE_ENV !== 'production' && {
transport: {
target: 'pino-pretty',
options: { colorize: true },
},
}),
});
In production: compact JSON, fast serialization. In development: human-readable colored output. The redact config means even if someone logs { body } containing a password field, it never reaches the log sink.
requestIdMiddleware.ts
import { Request, Response, NextFunction } from 'express';
import { randomUUID } from 'crypto';
export function requestIdMiddleware(
req: Request,
res: Response,
next: NextFunction
): void {
const requestId = (req.headers['x-request-id'] as string) ?? randomUUID();
req.id = requestId;
res.setHeader('X-Request-ID', requestId);
next();
}
Clients that generate their own request IDs (e.g., mobile apps) can pass X-Request-ID. Otherwise one is generated. Either way, it propagates back in the response header — so the client can correlate its own logs with server logs.
app.ts
import pinoHttp from 'pino-http';
import { requestIdMiddleware } from './requestIdMiddleware';
import { logger } from './logger';
// requestId middleware must come first
app.use(requestIdMiddleware);
app.use(
pinoHttp({
logger,
genReqId: (req) => req.id,
autoLogging: {
ignore: (req) => req.url === '/health',
},
})
);
genReqId wires the request ID from the middleware into pino-http's log entries. /health is excluded from auto-logging to avoid log spam from load balancer probes.
getRequestLogger(req)
import { Request } from 'express';
import { logger } from './logger';
export function getRequestLogger(req: Request) {
return logger.child({
requestId: req.id,
userId: (req as any).user?.id,
tenantId: (req as any).tenant?.id,
});
}
Child loggers inherit the parent config (redact, level, transport) and add context fields. Every log line from a route handler automatically includes requestId, userId, and tenantId.
Usage in a Route Handler
router.post('/orders', async (req, res) => {
const log = getRequestLogger(req);
log.info({ body: req.body }, 'Creating order');
const order = await orderService.create(req.body, req.user.id);
log.info({ orderId: order.id }, 'Order created');
res.status(201).json(order);
});
What the JSON Output Looks Like
{
"level": 30,
"time": 1741680000000,
"pid": 1234,
"hostname": "app-pod-7f9b",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"userId": "usr_abc123",
"tenantId": "tenant_xyz",
"msg": "Order created",
"orderId": "ord_789"
}
Every log line has: timestamp, level, request ID, user context, and the actual message. This is what you need for distributed tracing and log aggregation (Datadog, CloudWatch, Loki, etc.).
What CLAUDE.md Gives You
The pattern: write the logging rules in CLAUDE.md → Claude Code generates compliant code on the first pass.
-
X-Request-IDon all requests → trace any log back to its HTTP request -
pino-httpauto-logging → HTTP access logs without manual instrumentation -
redact→ passwords and tokens never reach the log aggregator, by default -
logger.child()→ every log line carries user and tenant context automatically
Without CLAUDE.md, you get console.log in route handlers. With it, you get production-grade observability.
Want the full set of Node.js backend CLAUDE.md rules I use — including Docker, error handling, database patterns, and security constraints? It's packaged as a Code Review Pack on PromptWorks (¥980, /code-review).
What's in your logging setup that you wish you had from day one?
Top comments (0)