En un servidor tradicional, cuando algo falla, siempre te queda el último recurso: SSH, tail -f, y a leer. En serverless ese recurso no existe. Una request puede atravesar API Gateway, una Lambda, una cola SQS y otra Lambda, y si tus logs son strings sueltos escritos con console.log, reconstruir qué pasó es arqueología. Aprendí esto de la peor manera: un bug intermitente en producción que tardé días en localizar porque no podía seguir una request de punta a punta. Este artículo es el sistema de logging que uso desde entonces: logs estructurados, un ID de correlación que viaja con la request, y consultas en CloudWatch que responden preguntas en minutos.
Logs como datos, no como prosa
El cambio fundamental es dejar de escribir logs para humanos y empezar a escribirlos para máquinas. Un log de prosa ("Error procesando pedido 123 del usuario 456") es agradable de leer pero imposible de consultar en masa. Un log estructurado es un objeto JSON con campos consistentes:
logger.error('order_processing_failed', {
orderId: order.id,
userId: user.id,
errorCode: err.code,
correlationId: ctx.correlationId,
durationMs: Date.now() - start,
});
CloudWatch Logs Insights entiende JSON de forma nativa. Con logs estructurados, "¿cuántos pedidos fallaron esta semana con este código de error, y de qué usuarios?" deja de ser un grep heroico y pasa a ser una consulta:
fields @timestamp, orderId, userId
| filter level = 'error' and errorCode = 'PAYMENT_TIMEOUT'
| stats count(*) by userId
| sort count(*) desc
No uso un logger casero: AWS Lambda Powertools para TypeScript ya resuelve la serialización, los niveles, y añade automáticamente el contexto de la invocación (request ID, nombre de función, cold start). Reinventar eso es tiempo perdido.
El ID de correlación: el hilo que une el sistema
En una arquitectura orientada a eventos, el problema no es loguear: es correlacionar. La request del usuario dispara una Lambda que encola un mensaje en SQS que procesa otra Lambda que escribe en DynamoDB. Cuatro grupos de logs distintos, cuatro request IDs distintos, ninguna relación visible entre ellos.
Mi regla: el primer punto de entrada genera un correlationId (o adopta el que venga en el header X-Correlation-Id), y ese ID viaja con todo. En los mensajes SQS va dentro de los message attributes; en las llamadas entre servicios, en un header; en cada log, como campo obligatorio. Powertools permite inyectarlo una vez en el logger y olvidarte: todo log de esa invocación lo lleva.
Con eso, reconstruir la historia completa de una request es una sola consulta en Logs Insights cruzando todos los log groups implicados. Lo que antes era una tarde de arqueología ahora son treinta segundos.
💡 La observabilidad no se añade cuando hay un incidente: se diseña antes. El día que algo se rompe en producción, tus logs ya son los que son. Cada campo que no logueaste es una pregunta que no puedes responder.
Métricas sin martillo: EMF
Para métricas de negocio (pedidos procesados, matchings generados, colas que crecen) no uso llamadas a la API de CloudWatch, que añaden latencia y cuestan dinero por request. Uso Embedded Metric Format: escribes la métrica como parte del log JSON con un formato especial, y CloudWatch la extrae de forma asíncrona. Coste marginal cero en el camino caliente, y las métricas aparecen en dashboards y alarmas como cualquier otra.
metrics.addMetric('MatchingCompleted', MetricUnit.Count, 1);
metrics.addMetric('MatchingDurationMs', MetricUnit.Milliseconds, elapsed);
La disciplina que me impongo: cada Lambda emite al menos una métrica de éxito y una de fallo con nombre de negocio, no técnico. "Errors" no me dice nada a las 3 de la mañana; "PaymentTimeouts" sí.
Lo que decidí no hacer
No monté X-Ray en todo. Lo probé, y para mi escala el tracing distribuido completo era más ruido que señal: la mayoría de mis flujos tienen dos o tres saltos, y el correlation ID en logs estructurados los cubre de sobra. X-Ray lo reservo para los flujos con más saltos o cuando sospecho de latencias entre servicios que los logs no explican. Es un trade-off honesto: menos visibilidad automática a cambio de menos coste y menos configuración que mantener.
Tampoco centralizo logs en una herramienta externa. CloudWatch Logs Insights tiene una UX regular y consultas que se pagan por GB escaneado, pero añadir un tercero significa otro pipeline de envío, otro contrato, y otra factura. Mientras el volumen lo permita, prefiero el dolor conocido. El día que las consultas se vuelvan lentas o caras de verdad, esa decisión se revisa con datos: GB escaneados al mes y minutos perdidos por incidente.
El resultado
Nada de esto es glamuroso. Son tres hábitos: JSON en vez de prosa, un ID que viaja con la request, y métricas de negocio embebidas en los logs. Pero la diferencia operativa es enorme: los incidentes pasaron de "a ver si consigo reproducirlo" a "dame el correlation ID y te digo qué pasó". En serverless no puedes hacer SSH al servidor, así que más te vale que el sistema cuente su propia historia.