Observabilidad más allá del monitoreo: trazas, logs y métricas
Cómo construir un stack de observabilidad que conecta trazas distribuidas, logs estructurados y métricas para depurar producción más rápido.

El monitoreo te dice que algo está mal. La observabilidad te dice por qué. Esa diferencia importa cuando estás depurando un incidente de producción a las 2 de la madrugada. Si tus dashboards muestran una tasa de errores elevada pero no puedes rastrear una sola solicitud fallida a través de tu sistema, tienes monitoreo sin observabilidad.
Los tres pilares —trazas, logs y métricas— no son útiles de forma aislada. Su verdadero valor está en la correlación: vincular un pico en una métrica con las trazas que lo causaron, y esas trazas con las líneas de log que explican la causa raíz.
Métricas: el punto de partida
Las métricas son números agregados a lo largo del tiempo. Te dicen qué está pasando a nivel de sistema, pero no por qué.
// ❌ Only tracking high-level metrics
const requestCount = new Counter({
name: 'http_requests_total',
help: 'Total HTTP requests',
});
app.use((req, res, next) => {
requestCount.inc();
next();
});
// You know requests increased, but not which endpoints or status codes// ✅ Metrics with dimensions for drill-down
import { Counter, Histogram } from 'prom-client';
const requestDuration = new Histogram({
name: 'http_request_duration_seconds',
help: 'HTTP request duration in seconds',
labelNames: ['method', 'route', 'status_code'],
buckets: [0.01, 0.05, 0.1, 0.5, 1, 2, 5, 10],
});
const requestErrors = new Counter({
name: 'http_request_errors_total',
help: 'HTTP request errors',
labelNames: ['method', 'route', 'error_type'],
});
app.use((req, res, next) => {
const start = process.hrtime.bigint();
res.on('finish', () => {
const duration = Number(process.hrtime.bigint() - start) / 1e9;
const route = req.route?.path ?? 'unknown';
requestDuration
.labels(req.method, route, String(res.statusCode))
.observe(duration);
if (res.statusCode >= 400) {
requestErrors
.labels(req.method, route, String(res.statusCode))
.inc();
}
});
next();
});Con métricas etiquetadas puedes responder preguntas como "¿qué endpoint está lento?" o "¿qué códigos de error están aumentando?" directamente desde los datos de la métrica. Usa histogramas para la latencia (te dan percentiles), contadores para totales y gauges para valores actuales como la profundidad de la cola.
Logging estructurado
Los logs sin estructura no se pueden consultar a escala. Los logs estructurados son buscables, filtrables y se pueden correlacionar con las trazas.
// ❌ Unstructured log lines
console.log(`User ${userId} created order ${orderId} - total: $${total}`);
console.log(`ERROR: Payment failed for order ${orderId}`);
// Good luck searching for "all payment failures over $100 in the last hour"// ✅ Structured JSON logs with consistent fields
import pino from 'pino';
const logger = pino({
level: process.env.LOG_LEVEL ?? 'info',
formatters: {
level: (label) => ({ level: label }),
},
timestamp: pino.stdTimeFunctions.isoTime,
});
// Application code
logger.info({
event: 'order_created',
userId,
orderId,
total,
currency: 'USD',
itemCount: items.length,
});
logger.error({
event: 'payment_failed',
orderId,
userId,
amount: total,
errorCode: 'card_declined',
provider: 'stripe',
traceId: req.headers['x-trace-id'],
});Los logs estructurados tienen dos ventajas: puedes consultarlos (event=payment_failed AND amount>100) y puedes incluir IDs de traza que vinculan cada línea de log con las trazas distribuidas.
Tracing distribuido
Una traza sigue una única solicitud a través de cada servicio que toca. Cada segmento es un span. Los spans forman un árbol que muestra en qué se fue el tiempo.
import { trace, SpanStatusCode } from '@opentelemetry/api';
const tracer = trace.getTracer('order-service');
async function createOrder(userId: string, items: CartItem[]) {
return tracer.startActiveSpan('createOrder', async (span) => {
try {
span.setAttribute('user.id', userId);
span.setAttribute('order.item_count', items.length);
// Child span: validate inventory
const inventory = await tracer.startActiveSpan(
'validateInventory',
async (childSpan) => {
try {
const result = await inventoryService.check(items);
childSpan.setAttribute('inventory.available', result.allAvailable);
return result;
} finally {
childSpan.end();
}
}
);
if (!inventory.allAvailable) {
span.setStatus({
code: SpanStatusCode.ERROR,
message: 'Items out of stock',
});
throw new Error('Items out of stock');
}
// Child span: process payment
const payment = await tracer.startActiveSpan(
'processPayment',
async (childSpan) => {
try {
const result = await paymentService.charge(userId, total);
childSpan.setAttribute('payment.provider', 'stripe');
childSpan.setAttribute('payment.amount', total);
return result;
} finally {
childSpan.end();
}
}
);
span.setAttribute('order.id', payment.orderId);
return { orderId: payment.orderId, status: 'confirmed' };
} catch (err) {
span.recordException(err as Error);
span.setStatus({ code: SpanStatusCode.ERROR });
throw err;
} finally {
span.end();
}
});
}La traza que genera este código muestra: createOrder (350ms) → validateInventory (50ms) + processPayment (280ms). Si el pago es lento, ves exactamente dónde está el cuello de botella y puedes profundizar en los spans del servicio de pagos.
Correlacionar los tres pilares
El verdadero valor aparece cuando vinculas métricas, logs y trazas entre sí. Una alerta de métrica te lleva a las trazas, y esas trazas te llevan a líneas de log específicas.
// Middleware that connects all three pillars
import { trace, context } from '@opentelemetry/api';
function observabilityMiddleware(req: Request, res: Response, next: NextFunction) {
const span = trace.getActiveSpan();
const traceId = span?.spanContext().traceId ?? 'no-trace';
// Attach trace ID to logger — all log lines include it
req.log = logger.child({ traceId, requestId: req.id });
// Attach trace ID to response headers — clients can report it
res.setHeader('X-Trace-Id', traceId);
const start = process.hrtime.bigint();
res.on('finish', () => {
const duration = Number(process.hrtime.bigint() - start) / 1e9;
// Metric with trace exemplar
requestDuration
.labels(req.method, req.route?.path ?? 'unknown', String(res.statusCode))
.observe(duration);
// Structured log with trace correlation
req.log.info({
event: 'request_completed',
method: req.method,
path: req.path,
statusCode: res.statusCode,
duration,
});
});
next();
}El flujo de correlación:
- Alerta de métrica: la latencia P99 de
/api/orderssupera los 2s - Búsqueda de trazas: se buscan trazas hacia
/api/orderscon una duración > 2s en los últimos 10 minutos - Análisis de spans: el span
processPaymentestá consistentemente en 1.8s (normalmente 200ms) - Profundización en logs: se filtran los logs por traceId y aparecen errores
payment_timeoutdesde el endpoint europeo de Stripe
Alertar sobre las señales correctas
Genera alertas sobre síntomas (el impacto que percibe el usuario), no sobre causas (el uso de CPU). Usa el método RED para servicios y el método USE para recursos.
# ❌ Alerting on causes — noisy and often irrelevant
groups:
- name: infrastructure
rules:
- alert: HighCPU
expr: cpu_usage > 80
# CPU at 81% with no user impact → false alarm at 3am
# ✅ Alerting on symptoms — user-facing impact
groups:
- name: service-health
rules:
- alert: HighErrorRate
expr: |
sum(rate(http_request_errors_total{status_code=~"5.."}[5m]))
/
sum(rate(http_requests_total[5m])) > 0.01
for: 5m
labels:
severity: critical
annotations:
summary: "Error rate exceeds 1% for 5 minutes"
- alert: HighLatency
expr: |
histogram_quantile(0.99,
rate(http_request_duration_seconds_bucket[5m])
) > 2
for: 5m
labels:
severity: warning
annotations:
summary: "P99 latency exceeds 2s for 5 minutes"La cláusula for: 5m evita que se generen alertas por picos transitorios. Una sola solicitud lenta es ruido. Cinco minutos de latencia elevada sí es un problema real.
Conclusiones clave
- Las métricas muestran qué está pasando — usa histogramas y contadores etiquetados con dimensiones para poder profundizar
- Los logs estructurados explican por qué — usa logging en JSON con nombres de campo consistentes e IDs de traza
- Las trazas muestran en qué se va el tiempo — instrumenta los límites entre servicios y las operaciones costosas como spans
- La correlación es el multiplicador — vincula métricas → trazas → logs mediante IDs de traza propagados entre servicios
- Alerta sobre síntomas, no sobre causas — las tasas de error y los percentiles de latencia importan más que el uso de CPU
- Usa la cláusula
for— exige violaciones sostenidas antes de alertar para evitar ruido por picos transitorios


