From console.log to structured logs
In development console.log("user logged in", id) is convenient. In production
the logs are read by a machine (an aggregator like Datadog, Loki, or CloudWatch),
not a person watching the terminal. That's why logs must be structured:
each line is a JSON object with consistent fields.
// Bad: free text, impossible to filter or aggregate.
console.log("Error saving order 42 for user 7");
// Good: JSON with fields. You can filter by level, search by userId...
console.log(JSON.stringify({
level: "error",
message: "Error saving the order",
orderId: 42,
userId: 7,
ts: "2026-06-22T10:00:00Z",
}));
Log levels
Each entry carries a severity level, so it can be filtered:
info: normal events ("server started", "request received").warn: something unexpected but recoverable ("retrying connection").error: a failure that requires attention (exception, failed request).
In production the "noise" is usually reduced by showing only warn and error, while
in development you see everything. Libraries like pino or winston provide this out of the box.
function createLog(level, message) {
return { level, message, ts: new Date().toISOString() };
}
Metrics
Logs tell what happened; metrics measure how much and how fast: requests per second, latency (p95, p99), memory and CPU usage, error rate. They are exposed (e.g. in Prometheus format) and graphed on dashboards (Grafana) with alerts when something goes out of range.
Health checks
A health check is a simple endpoint, usually GET /health, that
responds 200 and a body like { status: "ok" } if the service is alive.
It is not called by a human: it's queried by the load balancer, the orchestrator
(Kubernetes), or the process manager to decide whether the instance should receive
traffic or be restarted.
function health(req, res) {
res.status(200).json({ status: "ok" });
}
Sometimes a distinction is made between liveness (is the process alive?) and readiness (is it ready to receive traffic, e.g. with the DB connected?).
Traceability
When a request crosses several services, a correlation identifier (request id / trace id) is attached to it, which is propagated and included in all the logs of that request. That way you can reconstruct the full path of a single request across the whole system. It's the basis of distributed tracing (OpenTelemetry).
Examples
Structured logger with levels, emitted as JSON
function createLog(level, message, extra) {
return Object.assign({ level, message, ts: new Date().toISOString() }, extra || {});
}
console.log(JSON.stringify(createLog("info", "Server started", { port: 3000 })));
console.log(JSON.stringify(createLog("error", "Connection lost")));