Problema

En pipelines de datos, ETL o jobs serverless, los logs crecen rápidamente y mezclan mensajes de INFO, WARN y ERROR. Un operador necesita saber cuándo un mensaje marca un incidente real que requiere acción y cuándo es simplemente ruido que puede ignorarse. El reto es que:

  • Un mensaje con la palabra ERROR no siempre implica que el job falló (por ejemplo, “ERROR: retrying step 2”).
  • Algunas excepciones críticas aparecen sin la etiqueta ERROR (por ejemplo, OutOfMemoryError dentro de un WARN).
  • Los patrones de fallo varían entre versiones de Spark, Glue o librerías de terceros.
  • Los picos de reintentos pueden ser normales en workloads con alta latencia, pero también pueden indicar un problema subyacente.

Sin una estrategia clara, los dashboards de observabilidad se llenan de falsos positivos y los equipos terminan ignorando alertas útiles.

Causa

Los falsos positivos y la ambigüedad surgen por tres motivos principales:

  1. Dependencia exclusiva de palabras clave
    Filtrar solo por “ERROR”, “Exception” o “FAILED” ignora el contexto. Un mensaje como “ERROR: task timed out, retrying” no implica fallo definitivo.

  2. Falta de correlación con métricas de ejecución
    Los logs no se cruzan con el estado del job (SUCCESS/FAILED), número de reintentos, duración o uso de recursos. Cuando el job termina exitosamente, la mayoría de los “errores” son recuperables.

  3. Ausencia de aprendizaje histórico
    Cada pipeline tiene su propio “perfil de ruido”. Sin comparar contra un histórico de ejecución, no se detectan desviaciones significativas (picos de OOM, aumentos de latencia, etc.).

Solución

Una solución robusta combina filtrado estructurado, correlación con métricas y detección de anomalías. El flujo recomendado es:

  1. Normalizar los logs

    • Usa un formato estructurado (JSON) para que cada registro incluya campos como timestamp, level, message, job_id, task_id, resource_id.
    • Si el origen no permite JSON, aplica un parser (por ejemplo, Logstash o Fluent Bit) que extraiga esos campos.
  2. Clasificar el nivel de gravedad con reglas de contexto

    • Define una tabla de reglas que evalúe tanto el level como patrones de texto y métricas asociadas.
    • Ejemplo de regla:
      - name: "Retryable error"
        level: "ERROR"
        pattern: "retrying"
        outcome: "ignore"
      - name: "Fatal OOM"
        level: "ERROR"
        pattern: "OutOfMemoryError"
        outcome: "incident"
      - name: "Access denied"
        level: "WARN"
        pattern: "AccessDenied"
        outcome: "incident"
      
  3. Correlacionar con el estado del job

    • Consulta la API de Glue (o la herramienta equivalente) para obtener JobRunState.
    • Si JobRunState == FAILED y el log contiene al menos una regla marcada como incident, genera una alerta.
    • Si JobRunState == SUCCEEDED, descarta cualquier regla cuyo outcome sea ignore, pero registra los incident para análisis posterior.
  4. Aplicar detección de anomalías sobre métricas auxiliares

    • Métricas recomendadas: número de reintentos (RetryCount), uso de memoria (MaxMemory), duración (RunTime).
    • Usa un algoritmo simple como Z‑score o EWMA para detectar desviaciones > 3σ respecto al histórico de 30 días.
    • Cuando una métrica supera el umbral, eleva la gravedad de cualquier log asociado, incluso si la regla original era ignore.
  5. Enriquecer la alerta con contexto

    • Adjunta al evento: job_id, run_id, last 5 log lines, metric snapshot.
    • Esto permite a SREs diagnosticar sin abrir múltiples pestañas.

Implementación práctica (AWS)

  1. Exportar logs a CloudWatch con formato JSON
    Configura el job de Glue para que use --conf spark.driver.extraJavaOptions=-Dlog4j.configuration=log4j-json.properties.

  2. Crear un metric filter en CloudWatch

    aws logs put-metric-filter \
      --log-group-name /aws/glue/jobs \
      --filter-name IncidentFilter \
      --filter-pattern '{ $.level = "ERROR" && $.message = /OutOfMemoryError|AccessDenied/ }' \
      --metric-transformations metricName=GlueIncidents,metricNamespace=DevOps,metricValue=1
    
  3. Configurar una alarma basada en el metric y en el estado del job

    aws cloudwatch put-composite-alarm \
      --alarm-name GlueIncidentAlarm \
      --alarm-rule "ALARM(GlueIncidents) AND ALARM(GlueJobFailed)" \
      --actions-enabled
    
  4. Pipeline de correlación en Lambda

    • Trigger: CloudWatch Alarm.
    • Lambda consulta GetJobRun y, según la regla, publica en SNS o abre un ticket.

Cuándo aplicar esta solución

Aplica cuando:

  • Los jobs generan cientos o miles de líneas por ejecución.
  • Existe un historial suficiente (al menos 2 semanas) para calcular métricas de referencia.
  • Se dispone de acceso a la API del orquestador (Glue, Airflow, etc.) para obtener estados y métricas.

No aplica si:

  • Los logs son escasos y cada línea ya se revisa manualmente.
  • No hay posibilidad de estructurar los logs (por ejemplo, aplicaciones de terceros sin control de formato).
  • El entorno no permite ejecutar código adicional (sin Lambda, sin metric filters).

Código

# 1. Crear grupo de logs si no existe
aws logs create-log-group --log-group-name /aws/glue/jobs

# 2. Definir filtro de incidentes (ERROR + patrones críticos)
aws logs put-metric-filter \
  --log-group-name /aws/glue/jobs \
  --filter-name IncidentFilter \
  --filter-pattern '{ $.level = "ERROR" && $.message = /OutOfMemoryError|AccessDenied|Timeout/ }' \
  --metric-transformations metricName=GlueIncidents,metricNamespace=DevOps,metricValue=1

# 3. Alarmas combinadas: incidentes + job failure
aws cloudwatch put-composite-alarm \
  --alarm-name GlueIncidentAlarm \
  --alarm-rule "ALARM(GlueIncidents) AND ALARM(GlueJobFailed)" \
  --actions-enabled \
  --alarm-actions arn:aws:sns:us-east-1:123456789012:DevOpsAlerts

Verificación

  1. Generar un fallo controlado

    • Ejecuta un job de Glue que lance OutOfMemoryError.
    • Verifica que el metric GlueIncidents incrementa en 1.
  2. Revisar la alarma

    • En la consola de CloudWatch, confirma que GlueIncidentAlarm pasa a estado ALARM.
    • Comprueba que el mensaje enviado a SNS contiene job_id, run_id y las últimas 5 líneas del log.
  3. Caso de éxito con error recuperable

    • Ejecuta un job que produzca ERROR: retrying step.
    • El filtro no coincide (no contiene patrones críticos) y la alarma permanece en OK aunque el job termine SUCCEEDED.

Notas adicionales

  • Mantenimiento de reglas: revisa el archivo de reglas cada 30‑45 días. Los patrones que aparecen frecuentemente como ignore pueden migrar a incident si la tasa de reintentos crece.
  • Persistencia de histórico: almacena métricas diarias en una tabla de DynamoDB o en un bucket S3; facilita el cálculo de desviaciones sin depender de CloudWatch Retention.
  • Escalado a otros servicios: la misma lógica funciona para logs de EMR, Lambda o Kubernetes (usando Fluent Bit → CloudWatch). Solo cambia el log-group-name y adapta los patrones a los mensajes propios del runtime.
  • Alert fatigue: combina la alerta de incidentes con un “cool‑down” de 10 minutos para evitar disparos repetidos del mismo job en bucles de reintento.