正在加载,请稍候…

Observability: Logs, Metrics, and Traces for Node.js

Implement the three pillars of observability in Node.js services. Set up structured logging, custom metrics, and distributed tracing with OpenTelemetry.

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.