Stack de observabilidad: métricas, logs y trazas
Monta un stack completo con métricas de Prometheus, logging JSON estructurado y trazado con OpenTelemetry, y conecta cada señal con las demás.

La observabilidad es la capacidad de entender qué ocurre dentro de tu sistema a partir de lo que este expone al exterior. Tres señales conforman su base: las métricas te dicen que algo va mal, los logs te dicen qué salió mal, y las trazas te dicen dónde salió mal a través de los límites entre servicios. Cada señal es útil por sí sola, pero se vuelven mucho más potentes cuando están conectadas.
La mayoría de los equipos empieza con una sola señal y va sumando las demás con el tiempo. Configurar las tres correctamente desde el principio ahorra meses de trabajo de adaptación posterior.
Logging estructurado: la base
Los logs son tu primera línea de depuración. Los logs de texto sin estructurar (console.log("something happened")) son casi inútiles a gran escala: no se pueden consultar, agregar ni correlacionar.
// ❌ Unstructured logging — impossible to query at scale
console.log("User logged in");
console.log("Order created for user 123");
console.log("Payment failed: insufficient funds");
// grep for "payment"? Returns every line with "payment"
// Find all errors for user 123? Good luck// ✅ Structured JSON logging
import pino from "pino";
const logger = pino({
level: process.env.LOG_LEVEL ?? "info",
formatters: {
level(label: string) {
return { level: label };
},
},
timestamp: pino.stdTimeFunctions.isoTime,
// Add base context to every log line
base: {
service: "api-gateway",
version: process.env.APP_VERSION ?? "unknown",
environment: process.env.NODE_ENV ?? "development",
},
});
// Request-scoped child logger
function createRequestLogger(
req: Request
): pino.Logger {
return logger.child({
requestId: req.headers["x-request-id"] as string,
userId: req.user?.id,
method: req.method,
path: req.path,
userAgent: req.headers["user-agent"],
});
}
// Usage with context
app.post("/api/orders", async (req, res) => {
const log = createRequestLogger(req);
log.info(
{ itemCount: req.body.items.length },
"Order creation started"
);
try {
const order = await createOrder(req.body);
log.info(
{
orderId: order.id,
total: order.total,
duration: Date.now() - req.startTime,
},
"Order created successfully"
);
res.status(201).json(order);
} catch (error) {
log.error(
{
error: (error as Error).message,
stack: (error as Error).stack,
body: req.body,
},
"Order creation failed"
);
res.status(500).json({ error: "Order creation failed" });
}
});
// Output (one JSON object per line):
// {"level":"info","time":"2024-05-10T10:30:00.000Z",
// "service":"api-gateway","requestId":"abc-123",
// "userId":"user-456","method":"POST",
// "path":"/api/orders","itemCount":3,
// "msg":"Order creation started"}Métricas con Prometheus: cómo medir el comportamiento del sistema
Las métricas son mediciones numéricas recogidas a lo largo del tiempo. Responden a preguntas como «cuántos», «qué tan rápido» y «cuánto» sobre tu sistema.
import {
collectDefaultMetrics,
register,
Histogram,
Counter,
Gauge,
} from "prom-client";
// Collect Node.js runtime metrics
collectDefaultMetrics();
// HTTP request metrics
const httpRequestDuration = new Histogram({
name: "http_request_duration_seconds",
help: "Duration of HTTP requests in seconds",
labelNames: ["method", "route", "status_code"],
buckets: [
0.01, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10,
],
});
const httpRequestsTotal = new Counter({
name: "http_requests_total",
help: "Total number of HTTP requests",
labelNames: ["method", "route", "status_code"],
});
// Business metrics
const ordersCreated = new Counter({
name: "orders_created_total",
help: "Total number of orders created",
labelNames: ["status"],
});
const activeConnections = new Gauge({
name: "active_connections",
help: "Number of active client connections",
});
// Metrics middleware
function metricsMiddleware(
req: Request,
res: Response,
next: NextFunction
) {
const start = process.hrtime.bigint();
res.on("finish", () => {
const durationNs =
Number(process.hrtime.bigint() - start);
const durationSec = durationNs / 1e9;
const route =
req.route?.path ?? req.path ?? "unknown";
const labels = {
method: req.method,
route,
status_code: String(res.statusCode),
};
httpRequestDuration.observe(labels, durationSec);
httpRequestsTotal.inc(labels);
});
next();
}
// Expose /metrics endpoint for Prometheus scraping
app.get("/metrics", async (req, res) => {
res.set("Content-Type", register.contentType);
res.send(await register.metrics());
});
app.use(metricsMiddleware);Trazado distribuido con OpenTelemetry
Las trazas siguen una solicitud a medida que esta atraviesa distintos servicios. Cada span representa una unidad de trabajo dentro de la traza.
import {
NodeTracerProvider,
} from "@opentelemetry/sdk-trace-node";
import {
SimpleSpanProcessor,
} from "@opentelemetry/sdk-trace-base";
import {
OTLPTraceExporter,
} from "@opentelemetry/exporter-trace-otlp-http";
import {
HttpInstrumentation,
} from "@opentelemetry/instrumentation-http";
import {
ExpressInstrumentation,
} from "@opentelemetry/instrumentation-express";
import {
PgInstrumentation,
} from "@opentelemetry/instrumentation-pg";
import {
registerInstrumentations,
} from "@opentelemetry/instrumentation";
import { Resource } from "@opentelemetry/resources";
import {
ATTR_SERVICE_NAME,
ATTR_SERVICE_VERSION,
} from "@opentelemetry/semantic-conventions";
// Initialize tracing — must run before app code
const provider = new NodeTracerProvider({
resource: new Resource({
[ATTR_SERVICE_NAME]: "order-service",
[ATTR_SERVICE_VERSION]:
process.env.APP_VERSION ?? "1.0.0",
}),
});
provider.addSpanProcessor(
new SimpleSpanProcessor(
new OTLPTraceExporter({
url:
process.env.OTEL_EXPORTER_OTLP_ENDPOINT ??
"http://localhost:4318/v1/traces",
})
)
);
provider.register();
// Auto-instrument libraries
registerInstrumentations({
instrumentations: [
new HttpInstrumentation(),
new ExpressInstrumentation(),
new PgInstrumentation(),
],
});Cómo conectar las tres señales
El verdadero poder aparece al correlacionar las señales entre sí. Un ID de traza en tus logs te permite saltar de una línea de log a la traza completa. Las etiquetas de tus métricas te permiten profundizar desde una anomalía hasta las solicitudes concretas que la causaron.
import { trace, context } from "@opentelemetry/api";
// Enrich logs with trace context
function createCorrelatedLogger(
req: Request
): pino.Logger {
const span = trace.getActiveSpan();
const spanContext = span?.spanContext();
return logger.child({
requestId: req.headers["x-request-id"],
traceId: spanContext?.traceId,
spanId: spanContext?.spanId,
userId: req.user?.id,
method: req.method,
path: req.path,
});
}
// Now logs contain trace IDs:
// {"level":"error","traceId":"abc123def456...",
// "spanId":"789xyz...","msg":"Payment failed"}
//
// Click the traceId in your log viewer →
// see the full distributed trace in Jaeger/Tempo
// Create custom spans for business operations
const tracer = trace.getTracer("order-service");
async function processPayment(
orderId: string,
amount: number
) {
return tracer.startActiveSpan(
"process-payment",
async (span) => {
span.setAttribute("order.id", orderId);
span.setAttribute("payment.amount", amount);
try {
const result =
await paymentGateway.charge(amount);
span.setAttribute(
"payment.transaction_id",
result.transactionId
);
span.setStatus({ code: 0 }); // OK
return result;
} catch (error) {
span.setStatus({
code: 2,
message: (error as Error).message,
});
span.recordException(error as Error);
throw error;
} finally {
span.end();
}
}
);
}Reglas de alerta: cómo hacer que las métricas sean accionables
Las métricas sin alertas son solo gráficos que nadie mira. Define alertas basadas en SLO que avisen al equipo de guardia cuando la experiencia del usuario se degrada.
# prometheus-alerts.yml
groups:
- name: api-slos
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_requests_total{status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m]))
> 0.01
for: 5m
labels:
severity: critical
annotations:
summary: "Error rate exceeds 1% SLO"
description: >-
Error rate is {{ $value | humanizePercentage }}
over the last 5 minutes
- alert: HighLatency
expr: |
histogram_quantile(0.95,
sum(rate(http_request_duration_seconds_bucket[5m]))
by (le)
) > 0.5
for: 5m
labels:
severity: warning
annotations:
summary: "P95 latency exceeds 500ms SLO"
- alert: OrderCreationSpike
expr: |
rate(orders_created_total{status="failed"}[5m])
> 0.1
for: 3m
labels:
severity: critical
annotations:
summary: "Order creation failures spiking"Puntos clave
El logging JSON estructurado con contexto por solicitud (ID de solicitud, ID de usuario, ID de traza) convierte los logs de texto imposibles de buscar en datos consultables: cada línea de log debería ser un evento estructurado que se pueda filtrar, agregar y correlacionar entre servicios. Las métricas de Prometheus con histogramas, counters y gauges etiquetados cuantifican el comportamiento del sistema a lo largo del tiempo: instrumenta las solicitudes HTTP, las consultas a la base de datos y las operaciones de negocio con etiquetas significativas, y expón un endpoint /metrics para el scraping. El trazado distribuido con OpenTelemetry sigue las solicitudes a través de los límites entre servicios como una serie de spans: instrumenta automáticamente los clientes HTTP, Express y los drivers de base de datos al arrancar, y añade spans personalizados para las operaciones críticas del negocio que necesitan visibilidad. Conecta las tres señales incluyendo IDs de traza en las líneas de log y haciendo coincidir las etiquetas de las métricas con los atributos de las trazas, lo que habilita un flujo de depuración en el que las métricas detectan anomalías, los logs aportan contexto y las trazas señalan con precisión el servicio y la operación exactos que fallaron.


