Volver al blog
• 9 min de lectura

Observabilidad desde el día 1: logs útiles, métricas mínimas, trazas cuando duela

Si no lo puedes medir, no lo puedes operar. El bug más caro es el que no puedes reproducir. Observabilidad no es ver que falló, es entender por qué y cómo. Una guía práctica de logs estructurados, métricas que importan y trazas distribuidas.

Observabilidad Arquitectura Backend Operabilidad
Observabilidad desde el día 1: logs útiles, métricas mínimas, trazas cuando duela
Foto por Isaac Smith

Hay una regla que suena simple hasta que estás a las 2am con un incidente en producción:

Si no lo puedes medir, no lo puedes operar.

El bug más caro no es el que causa una excepción visible. Es el que llega como un reporte de usuario: “a veces no funciona”. Sin observabilidad, ese bug te puede costar días. Con observabilidad bien diseñada desde el inicio, lo resuelves en minutos.

Observabilidad no es solo ver que algo falló. Es tener la capacidad de entender el comportamiento de tu sistema a partir de sus salidas, sin necesitar ssh a producción ni agregar logs en caliente.


Por qué desde el día 1 y no después

La tentación es agregar observabilidad “cuando sea necesario”. El problema: cuando es necesario ya es tarde.

  • El sistema ya está en producción con usuarios reales.
  • Agregar logs estructurados ahora significa tocar cientos de archivos.
  • El tracing distribuido requiere instrumentación que atraviesa capas.
  • Los IDs de correlación tienen que existir antes del primer request.

Observabilidad retrofitada siempre es incompleta. La que se diseña desde el inicio es sistemática.


Los tres pilares (y cuándo usar cada uno)

1. Logs: tu historia de lo que pasó

Los logs son el pilar más accesible y el más fácil de arruinar.

El anti-patrón clásico:

ERROR: Something went wrong
INFO: User logged in
DEBUG: Processing request...

Bonitos, legibles, inútiles en producción. ¿Qué usuario? ¿Qué request? ¿Desde dónde? ¿En qué contexto?

El patrón correcto: logs estructurados

{
  "timestamp": "2026-03-21T14:32:01.123Z",
  "level": "ERROR",
  "message": "Payment authorization failed",
  "traceId": "abc123def456",
  "requestId": "req_789xyz",
  "userId": "usr_42",
  "operation": "PaymentService.authorize",
  "orderId": "ord_99",
  "provider": "stripe",
  "errorCode": "card_declined",
  "durationMs": 342
}

Ahora puedes responder: ¿quién? ¿qué operación? ¿en qué contexto? ¿cuánto tardó? ¿cuál fue el error exacto?

Los campos que no pueden faltar:

CampoPor qué
traceIdCorrelaciona todos los logs de un flujo distribuido
requestIdCorrelaciona logs de un request HTTP específico
userIdPermite decir “todo lo que hizo este usuario”
operationQué estaba haciendo el sistema (OrderService.create, PaymentGateway.charge)
durationMsLatencia de la operación
levelERROR, WARN, INFO, DEBUG — con semántica estricta

Semántica de niveles (la que importa):

  • ERROR: algo falló y requiere atención. Alguien debería mirar esto.
  • WARN: algo raro pasó pero el sistema se recuperó. Pista de un problema futuro.
  • INFO: evento de negocio relevante. OrderCreated, UserAuthenticated, PaymentProcessed.
  • DEBUG: información de diagnóstico. Solo en desarrollo o bajo demanda.

Si tu sistema en producción tiene miles de ERRORs por hora que “son normales”, ya perdiste la señal.


2. Métricas: el pulso de tu sistema

Los logs te dicen qué pasó. Las métricas te dicen cuánto y con qué frecuencia.

Las métricas mínimas que todo servicio debe exponer:

Latencia (histograma, no promedio)

http_request_duration_seconds{method="POST", path="/api/orders", status="200"}

El promedio miente. Un percentil 99 de 5s puede estar escondido en un promedio de 200ms. Usa histogramas y observa p50, p95, p99.

Tasa de errores

http_requests_total{method="POST", path="/api/orders", status="500"}
http_requests_total{method="POST", path="/api/orders", status="200"}

Quieres el ratio, no el número absoluto. Un sistema con 1000 errores que procesa 1,000,000 requests está mejor que uno con 10 errores en 20 requests.

Throughput

http_requests_total{...}  # rate por segundo o por minuto

Pico de tráfico + incremento de errores = problema de carga. Pico de tráfico sin incremento de errores = el sistema aguanta.

Métricas de dependencias externas:

external_calls_total{dependency="stripe", status="success"}
external_calls_total{dependency="stripe", status="timeout"}
external_call_duration_seconds{dependency="stripe"}

Cuando el sistema falla, necesitas saber si el problema es tuyo o de Stripe.

Colas y reintentos:

queue_depth{queue="payment_processing"}
retry_attempts_total{operation="send_notification", attempt="2"}

Una cola que crece sin procesar es un incidente esperando a ocurrir.

Recursos:

jvm_memory_used_bytes{area="heap"}
db_connection_pool_active{pool="main"}

El sistema puede estar respondiendo correctamente mientras acumula presión. Las métricas de recursos te avisan antes.


3. Trazas distribuidas: el mapa cuando hay microservicios

Las trazas son el tercer pilar y el más costoso de implementar. No las necesitas para todos los casos. Las necesitas cuando el dolor aparece.

¿Cuándo las trazas son imprescindibles?

  • Cuando un request atraviesa múltiples servicios y no sabes cuál es lento.
  • Cuando tienes colas entre servicios (un mensaje publicado en Kafka y consumido en otro servicio).
  • Cuando hay latencias intermitentes que no puedes atribuir a ningún componente específico.
  • Cuando el timeout de un request viene de una cadena de llamadas y necesitas saber dónde rompió.

El concepto clave: propagación de contexto

Una traza distribuida es una colección de spans (unidades de trabajo) conectados por el mismo traceId. Para que funcione, el contexto (traceId + spanId) tiene que propagarse entre servicios:

  • En HTTP: como headers (traceparent en el estándar W3C, o X-B3-TraceId en Zipkin).
  • En mensajes/eventos: como campo en el cuerpo o en los metadata del mensaje.
  • En llamadas a bases de datos: instrumentado por el SDK/agente.
Request → [Servicio A] → [Servicio B] → [Servicio C]
              span1         span2          span3
                    ← mismo traceId →

Con trazas puedes ver:

  • Cuánto tardó cada span.
  • Dónde está el cuello de botella real.
  • Si el timeout vino de B porque C tardó 4s.
  • Qué llamadas se hicieron en paralelo vs. en serie.

Cuándo NO invertir en trazas todavía:

  • Si tienes un monolito o 2-3 servicios bien controlados.
  • Si tus logs ya tienen traceId y requestId propagados — eso cubre la mayoría de los casos.
  • Si el volumen es bajo y el overhead de tracing completo no se justifica.

Empieza con logs bien correlacionados. Las trazas completas llegan cuando el dolor lo amerita.


Cómo implementarlo (sin sobreeingeniería)

Paso 1: genera IDs de correlación en el borde

El traceId debe generarse en el punto de entrada (API Gateway, servlet filter, middleware) y propagarse hacia adentro. Nunca debe generarse dentro de un servicio de negocio.

// Filter/Interceptor en el borde
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    String traceId = extractOrGenerate(((HttpServletRequest) request).getHeader("X-Trace-Id"));
    String requestId = UUID.randomUUID().toString();

    MDC.put("traceId", traceId);
    MDC.put("requestId", requestId);

    ((HttpServletResponse) response).setHeader("X-Trace-Id", traceId);

    chain.doFilter(request, response);

    MDC.clear();
}

Con MDC (Mapped Diagnostic Context en Logback/Log4j2) todos tus logs automáticamente incluyen traceId y requestId sin tocar el código de negocio.

Paso 2: configura logs estructurados en JSON

En Logback (logback-spring.xml):

<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
  <encoder class="net.logstash.logback.encoder.LogstashEncoder">
    <includeMdcKeyName>traceId</includeMdcKeyName>
    <includeMdcKeyName>requestId</includeMdcKeyName>
    <includeMdcKeyName>userId</includeMdcKeyName>
    <includeMdcKeyName>operation</includeMdcKeyName>
  </encoder>
</appender>

Resultado: cada log.info("Payment processed") sale como JSON con todos los campos de contexto automáticamente.

Paso 3: expone métricas con Micrometer (si usas Spring Boot)

@Configuration
public class MetricsConfig {
    @Bean
    MeterRegistryCustomizer<MeterRegistry> commonTags() {
        return registry -> registry.config()
            .commonTags("service", "payment-service", "env", "prod");
    }
}

Spring Boot Actuator + Micrometer + Prometheus te da latencia, throughput y error rate de tus endpoints sin código adicional. Para métricas de negocio:

@Service
public class PaymentService {
    private final Counter paymentSuccess;
    private final Counter paymentFailure;
    private final Timer paymentDuration;

    public PaymentService(MeterRegistry registry) {
        this.paymentSuccess = Counter.builder("payments.processed")
            .tag("status", "success").register(registry);
        this.paymentFailure = Counter.builder("payments.processed")
            .tag("status", "failure").register(registry);
        this.paymentDuration = Timer.builder("payments.duration").register(registry);
    }

    public PaymentResult process(PaymentRequest request) {
        return paymentDuration.record(() -> {
            try {
                PaymentResult result = doProcess(request);
                paymentSuccess.increment();
                return result;
            } catch (Exception e) {
                paymentFailure.increment();
                throw e;
            }
        });
    }
}

Paso 4: propaga el contexto en mensajes y eventos

Cuando publicas en una cola o topic, incluye el contexto de tracing:

// Al publicar
Map<String, String> headers = new HashMap<>();
headers.put("traceId", MDC.get("traceId"));
headers.put("requestId", MDC.get("requestId"));
kafkaTemplate.send(new ProducerRecord<>("orders", null, key, event, toHeaders(headers)));

// Al consumir
@KafkaListener(topics = "orders")
public void consume(ConsumerRecord<String, OrderEvent> record) {
    String traceId = extractHeader(record, "traceId");
    MDC.put("traceId", traceId);
    // ahora todos los logs de este consumidor tienen el traceId original
    processOrder(record.value());
    MDC.clear();
}

Con esto puedes correlacionar logs del productor con logs del consumidor usando el mismo traceId, aunque estén en servicios distintos y el mensaje haya tardado 30 segundos en procesarse.


Los anti-patrones que cuestan más caro

Logs sin IDs correlacionables

// Malo
log.error("Error processing payment: " + e.getMessage());

// Bien
log.error("Payment processing failed", Map.of(
    "orderId", request.getOrderId(),
    "userId", request.getUserId(),
    "operation", "PaymentService.process",
    "errorCode", e.getCode()
));

El primer log es decorativo. El segundo es operacional.

Métricas de conteo sin contexto

errors_total = 1503

¿Errores de qué? ¿De cuándo? ¿De qué endpoint? Sin labels/tags, las métricas son tan inútiles como los logs sin IDs.

Alertas sobre síntomas tardíos

Alertar cuando el disco está al 100% ya es tarde. Alertar cuando el error rate supera el 1% en los últimos 5 minutos es accionable. Diseña alertas sobre señales tempranas: crecimiento de cola, latencia p99 subiendo, retry rate aumentando.

Observabilidad solo en el “happy path”

Si solo logueas cuando todo va bien, no sabrás nada cuando algo falla. Las operaciones que más necesitan contexto son exactamente las que fallan: timeouts, reintentos, excepciones, validaciones rechazadas.


El mínimo viable de observabilidad

Si arrancas desde cero y quieres el mayor retorno por el menor esfuerzo:

  1. Logs JSON con traceId, requestId, userId, operation en cada log. MDC lo hace automático.
  2. Métricas HTTP de latencia (histograma), errores por código y throughput. Spring Boot Actuator + Micrometer lo da gratis.
  3. Métricas de negocio para las 2-3 operaciones más críticas: pagos procesados, órdenes creadas, notificaciones enviadas.
  4. Propagación de traceId en mensajes y eventos, aunque no tengas tracing completo todavía.

Con eso puedes responder la mayoría de incidentes sin trazas completas. Las trazas vienen cuando el dolor supera el costo de implementarlas.


Cierre

Observabilidad no es un lujo de empresas grandes. Es la diferencia entre un equipo que opera con confianza y uno que reza antes de cada deploy.

Empieza con logs estructurados y con IDs de correlación. Agrega métricas en tus endpoints y en tus operaciones de negocio críticas. Propaga contexto en tus mensajes. Y cuando los microservicios empiecen a doler, ya tendrás la base para agregar trazas distribuidas sin refactorizar todo.

El sistema observable no es el que nunca falla. Es el que, cuando falla, te dice exactamente dónde y por qué.