In development, console.log is fine. In production, it's a liability. You need logs that are searchable, metrics that show trends before they become outages, and health endpoints that your infrastructure can query automatically. This lesson covers the production observability stack for Express.
Structured loggingWhat is structured logging?Writing log entries as machine-readable JSON objects with consistent fields instead of plain text, making them searchable by log analysis tools. with Winston
Think of structured logging like using a spreadsheet instead of a diary. A plain text log like "Request failed 2024-01-15" is hard to search and impossible to aggregate. A JSONWhat is json?A text format for exchanging data between systems. It uses key-value pairs and arrays, and every programming language can read and write it. log like { "level": "error", "status": 500, "url": "/api/users", "duration": "243ms" } can be filtered, counted, and graphed by any log analysis tool.
// utils/logger.js
import winston from 'winston';
const { combine, timestamp, json, errors } = winston.format;
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
defaultMeta: {
service: 'my-api',
environment: process.env.NODE_ENV
},
format: combine(
timestamp(),
errors({ stack: true }), // Include stack traces for errors
json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
export default logger;Log levels
| Level | Use it for |
|---|---|
error | Unhandled exceptions, failed operations that need immediate attention |
warn | Deprecated usage, recoverable errors, suspicious activity |
info | Normal application events (server started, request completed) |
debug | Detailed diagnostic info for development (disabled in production) |
LOG_LEVEL=warn in production if your info logs are too noisy. You can always dial it back to debug on a specific instance when troubleshooting without redeploying.Request logging middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it.
Every request that hits your APIWhat is api?A set of rules that lets one program talk to another, usually over the internet, by sending requests and getting responses. should produce a log entry with enough context to reconstruct what happened. This is invaluable when you're debugging a user-reported issue and need to find their specific request in a sea of logs.
// middleware/requestLogger.js
import logger from '../utils/logger.js';
export const requestLogger = (req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = Date.now() - start;
logger.info('Request completed', {
requestId: req.id,
method: req.method,
url: req.originalUrl,
status: res.statusCode,
duration: `${duration}ms`,
ip: req.ip,
userAgent: req.get('User-Agent'),
userId: req.user?.id
});
});
next();
};Notice that we use the finish event on the response rather than logging inside the route handlerWhat is route handler?A Next.js file named route.js inside the app/ directory that handles HTTP requests directly - the App Router equivalent of API routes.. This ensures we capture the final status codeWhat is status code?A three-digit number in an HTTP response that tells the client what happened: 200 means success, 404 means not found, 500 means the server broke. after all middleware has run, including error handlers.
Health checkWhat is health check?An API endpoint that verifies your application and its dependencies are working, so monitoring tools can alert you when something fails. endpointWhat is endpoint?A specific URL path on a server that handles a particular type of request, like GET /api/users.
A health check is a dedicated endpoint that your infrastructure queries to confirm your app is alive and ready to serve traffic. Load balancers use it to decide whether to send requests to a particular instance. Monitoring tools use it to page you when it stops responding.
// A simple health check
app.get('/health', (req, res) => {
res.status(200).json({
uptime: process.uptime(),
status: 'ok',
timestamp: Date.now()
});
});
// A deeper health check that verifies dependencies
app.get('/health/deep', async (req, res) => {
try {
await db.query('SELECT 1'); // Verify database connection
res.status(200).json({
status: 'ok',
uptime: process.uptime(),
database: 'connected'
});
} catch (error) {
res.status(503).json({
status: 'error',
database: 'disconnected'
});
}
});Metrics with Prometheus
Logs tell you what happened. Metrics tell you how your system is behaving over time. The prom-client library exposes metrics in Prometheus format, which integrates with Grafana for dashboards and alerting.
// metrics.js
import promClient from 'prom-client';
const register = new promClient.Registry();
// Collect default Node.js metrics (heap usage, event loop lag, etc.)
promClient.collectDefaultMetrics({ register });
// Custom histogram for request durations
export const httpRequestDuration = new promClient.Histogram({
name: 'http_request_duration_seconds',
help: 'Duration of HTTP requests in seconds',
labelNames: ['method', 'route', 'status'],
buckets: [0.01, 0.05, 0.1, 0.3, 0.5, 1, 2, 5],
registers: [register]
});
// Expose metrics endpoint for Prometheus to scrape
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.end(await register.metrics());
});Use the histogram in a middlewareWhat is middleware?A function that runs between receiving a request and sending a response. It can check authentication, log data, or modify the request before your main code sees it. to record every request:
app.use((req, res, next) => {
const end = httpRequestDuration.startTimer();
res.on('finish', () => {
end({
method: req.method,
route: req.route?.path || 'unknown',
status: res.statusCode
});
});
next();
});Quick reference
| Tool | Purpose | Key package |
|---|---|---|
| Winston | Structured logging with levels and transports | winston |
| Morgan | HTTP request logging (simpler alternative) | morgan |
| prom-client | Prometheus metrics exposure | prom-client |
| Grafana | Metrics dashboards and alerting | (hosted service) |
/health endpoint | Liveness probe for load balancers | (custom code) |