DevPath · Learn to code ESPTEN

Production and deployment

Logging and observability

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:

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")));
Put this into practice

DevPath is a hands-on course: you read the theory here; in the app you put it into practice with exercises that really run, offline.

Start free in the app →
← Configuration: 12-factor and environmentsDeployment and resilience →