Links#
https://github.com/pinojs/pino
https://getpino.io/
https://kubernetes.io/docs/concepts/cluster-administration/logging/
https://pm2.io/docs/runtime/guide/log-management/
https://man7.org/linux/man-pages/man8/logrotate.8.html
1. Important Points#
logging rules:
production logs should be structured JSON
container app should write to stdout/stderr
log should include service / env / request_id / trace_id
do not log password / token / cookie / authorization header
error log should include stack
log level must be configurable
log rotation should be done by runtime platform when possible
recommended:
local:
pretty logs for humans
Docker / K8S / cloud:
JSON logs to stdout/stderr
collector handles storage, search, retention, rotation
VM + systemd:
stdout to journald, or file + logrotate
2. Log Levels#
| Level |
Use Case |
trace |
very detailed debug, usually disabled |
debug |
local/dev troubleshooting |
info |
important business/runtime events |
warn |
recoverable abnormal condition |
error |
request failed / dependency failed |
fatal |
process cannot continue |
rules:
info:
service started
order created
job completed
warn:
retrying dependency call
slow request
invalid but handled state
error:
request failed
database query failed
message processing failed
fatal:
config invalid
cannot connect critical dependency during startup
3. Pino Setup#
install#
npm install pino
npm install -D pino-pretty
logger.ts#
import pino from "pino";
const isLocal = process.env.NODE_ENV === "local";
export const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
base: {
service: process.env.SERVICE_NAME ?? "order-api",
env: process.env.NODE_ENV ?? "local"
},
redact: {
paths: [
"req.headers.authorization",
"req.headers.cookie",
"password",
"token",
"*.password",
"*.token"
],
censor: "[REDACTED]"
},
transport: isLocal
? {
target: "pino-pretty",
options: {
colorize: true,
translateTime: "SYS:standard"
}
}
: undefined
});
why:
pino writes JSON by default
pino-pretty only for local human-friendly output
redact prevents common secret fields from leaking
4. Request Logging#
import { randomUUID } from "node:crypto";
import type { NextFunction, Request, Response } from "express";
import { logger } from "./logger.js";
export function requestLogger(req: Request, res: Response, next: NextFunction) {
const start = Date.now();
const requestId = req.header("x-request-id") ?? randomUUID();
res.setHeader("x-request-id", requestId);
const childLogger = logger.child({
request_id: requestId,
method: req.method,
path: req.path
});
req.log = childLogger;
res.on("finish", () => {
childLogger.info({
status_code: res.statusCode,
duration_ms: Date.now() - start
}, "request completed");
});
next();
}
declare global {
namespace Express {
interface Request {
log: import("pino").Logger;
}
}
}
fields:
request_id:
join logs of one request
method / path / status_code:
HTTP dimension
duration_ms:
latency troubleshooting
5. Error Logging#
import type { ErrorRequestHandler } from "express";
import { AppError } from "./AppError.js";
export const errorHandler: ErrorRequestHandler = (error, req, res, next) => {
if (error instanceof AppError) {
req.log.warn({
error_code: error.code,
status_code: error.statusCode
}, error.message);
res.status(error.statusCode).json({
error: {
code: error.code,
message: error.message
}
});
return;
}
req.log.error({
err: error
}, "unexpected error");
res.status(500).json({
error: {
code: "INTERNAL_ERROR",
message: "internal server error"
}
});
};
rules:
expected business error:
warn
unexpected error:
error with stack
response:
do not return raw stack to client
{
"level": 30,
"time": 1790599200000,
"service": "order-api",
"env": "prod",
"request_id": "req-1001",
"method": "POST",
"path": "/orders",
"status_code": 201,
"duration_ms": 18,
"msg": "request completed"
}
recommended fields:
time
level
msg
service
env
version
request_id
trace_id
user_id, only when safe
method
path / route
status_code
duration_ms
error_code
7. Sensitive Data#
never log:
password
token
authorization header
cookie
session id
private key
credit card
raw personal data
safe pattern:
log user_id only when necessary
log order_id / tenant_id for debugging
mask email / phone if needed
log error code instead of raw external response body
8. Docker#
container logging:
write application logs to stdout/stderr
do not write rotating log files inside container by default
Docker logging driver / runtime handles collection and rotation
services:
order-api:
image: order-api:1.0.0
environment:
NODE_ENV: production
LOG_LEVEL: info
logging:
driver: json-file
options:
max-size: "100m"
max-file: "5"
9. Kubernetes#
K8S logging:
app writes stdout/stderr
kubelet/container runtime stores container logs on node
kubectl logs reads container logs
cluster-level logging needs separate backend
kubectl logs deploy/order-api
kubectl logs deploy/order-api --previous
kubectl logs pod/order-api-xxxxx -c order-api
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-api
spec:
template:
spec:
containers:
- name: order-api
image: order-api:1.0.0
env:
- name: NODE_ENV
value: prod
- name: LOG_LEVEL
value: info
rotation:
kubelet manages container log rotation
common kubelet settings:
containerLogMaxSize
containerLogMaxFiles
collection:
Fluent Bit / Fluentd / Vector / OpenTelemetry Collector
send to Elasticsearch / Loki / CloudWatch Logs / Google Cloud Logging / Azure Monitor
10. VM / systemd#
stdout to journald#
[Unit]
Description=order-api
After=network.target
[Service]
User=app
WorkingDirectory=/opt/order-api
Environment=NODE_ENV=prod
Environment=LOG_LEVEL=info
ExecStart=/usr/bin/node /opt/order-api/dist/main.js
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
journalctl -u order-api -f
journalctl -u order-api --since "1 hour ago"
file log#
[Service]
StandardOutput=append:/var/log/order-api/app.log
StandardError=append:/var/log/order-api/error.log
file log:
use only when required by VM operation model
configure logrotate
make sure app user can write log directory
11. logrotate#
use logrotate when:
app writes to files
PM2 writes file logs
systemd redirects stdout/stderr to files
do not need app-level logrotate when:
app writes stdout/stderr in container
platform log driver already rotates logs
/var/log/order-api/*.log {
daily
rotate 14
missingok
notifempty
compress
delaycompress
copytruncate
create 0640 app app
}
directives:
daily:
rotate daily
rotate 14:
keep 14 rotated files
compress:
gzip old logs
copytruncate:
copy current log then truncate original file
useful when process cannot reopen log file
create:
create new log file with mode/user/group
sudo logrotate -d /etc/logrotate.d/order-api
sudo logrotate -f /etc/logrotate.d/order-api
12. PM2#
npm install -g pm2
pm2 start dist/main.js --name order-api
pm2 logs order-api
pm2 install pm2-logrotate
pm2 set pm2-logrotate:max_size 100M
pm2 set pm2-logrotate:retain 14
pm2 set pm2-logrotate:compress true
PM2 notes:
default logs are under $HOME/.pm2/logs
pm2-logrotate rotates PM2-managed log files
for containers, prefer one process and stdout/stderr instead of PM2
13. Cloud Logging#
| Platform |
Common Target |
| AWS ECS / EKS |
CloudWatch Logs / FireLens |
| GCP GKE / Cloud Run |
Cloud Logging |
| Azure AKS / App Service |
Azure Monitor / Log Analytics |
| Alibaba ACK |
Simple Log Service |
| Self-managed K8S |
Loki / Elasticsearch / OpenSearch |
cloud logging checklist:
JSON logs
service/env labels
retention policy
access control
sensitive data redaction
dashboard and alert links
14. Production Checklist#
application:
structured JSON logs
request_id / trace_id included
log level configurable
secrets redacted
unexpected errors include stack
runtime:
container writes stdout/stderr
VM file logs use logrotate
K8S log rotation understood
centralized log backend configured
operation:
retention policy defined
log volume monitored
error rate alert
slow request logs queryable
access to logs restricted