Observability: Logs, Metrics, and Traces for Node.js
The three pillars of observability: logs (what happened), metrics (how much/fast), traces (where).
Structured Logging with Winston
import winston from 'winston';
const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
process.env.NODE_ENV === 'production'
? winston.format.json()
: winston.format.prettyPrint()
),
defaultMeta: {
service: 'api-service',
version: process.env.APP_VERSION,
},
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
],
});
// Use structured logging
logger.info('User registered', {
userId: user.id,
email: user.email,
duration: Date.now() - startTime,
});
logger.error('Payment failed', {
orderId: order.id,
amount: order.total,
error: error.message,
});
Request Context Propagation
import { AsyncLocalStorage } from 'async_hooks';
interface RequestContext {
requestId: string;
userId?: string;
traceId?: string;
}
const requestStorage = new AsyncLocalStorage<RequestContext>();
// Middleware to set context
app.use((req, res, next) => {
const context: RequestContext = {
requestId: req.headers['x-request-id'] as string ?? crypto.randomUUID(),
userId: req.user?.id,
};
requestStorage.run(context, next);
});
// Access context anywhere in the call chain
function log(message: string, meta?: object) {
const ctx = requestStorage.getStore();
logger.info(message, { ...meta, ...ctx });
}
OpenTelemetry Setup
import { NodeSDK } from '@opentelemetry/sdk-node';
import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
// Initialize before app code
const sdk = new NodeSDK({
traceExporter: new OTLPTraceExporter({
url: 'http://otel-collector:4318/v1/traces',
}),
instrumentations: [getNodeAutoInstrumentations()],
serviceName: 'api-service',
});
sdk.start();
Custom Spans
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('api-service');
async function processOrder(orderId: string) {
return tracer.startActiveSpan('processOrder', async (span) => {
span.setAttributes({
'order.id': orderId,
'order.type': 'online',
});
try {
const order = await orderRepo.findById(orderId);
span.setAttribute('order.total', order.total);
const result = await paymentService.charge(order);
span.setStatus({ code: SpanStatusCode.OK });
return result;
} catch (error) {
span.recordException(error as Error);
span.setStatus({ code: SpanStatusCode.ERROR, message: (error as Error).message });
throw error;
} finally {
span.end();
}
});
}
Prometheus Metrics
import { register, Counter, Histogram, Gauge } from 'prom-client';
// Define metrics
const httpRequestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.001, 0.005, 0.01, 0.05, 0.1, 0.5, 1, 5],
});
const httpRequestTotal = new Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
labelNames: ['method', 'route', 'status_code'],
});
// Middleware
app.use((req, res, next) => {
const start = Date.now();
res.on('finish', () => {
const duration = (Date.now() - start) / 1000;
const labels = { method: req.method, route: req.route?.path ?? req.path, status_code: res.statusCode };
httpRequestDuration.observe(labels, duration);
httpRequestTotal.inc(labels);
});
next();
});
// Expose metrics endpoint
app.get('/metrics', async (req, res) => {
res.set('Content-Type', register.contentType);
res.send(await register.metrics());
});
The three pillars together give you everything needed to understand and debug production systems.