La lección 05-01 terminó con un éxito incómodo. Marta encontró el pedido de ocho segundos: 7.402 ms dentro de PostgreSQL, un patrón N+1 con 39 consultas. Pero para llegar ahí necesitó cuatro consultas de Logs Insights, una cronología montada a mano y, sobre todo, que alguien hubiera tenido la precaución de propagar la cabecera X-Peticion-Id por todos los componentes. Si mañana el problema está en la conexión a la base de datos, o en la pasarela de pago, o en un import de la Lambda que tarda dos segundos en arrancar, hay que empezar de cero y adivinar otra vez dónde mirar.

El problema de fondo no es de esfuerzo: es de modelo de datos. Las métricas agregan y pierden el caso individual. Los registros conservan el caso individual pero pierden la relación entre lo que pasó en la tienda y lo que pasó en la Lambda. Ninguno de los dos guarda la estructura de la petición: quién llamó a quién, en qué orden, cuánto tardó cada tramo y cuáles se solapan.

AWS X-Ray guarda exactamente eso. Su unidad de trabajo no es un número ni una línea de texto: es una petición completa, con su árbol de llamadas y su cronología. Del «algo va lento» al «esta llamada concreta tarda 6,2 segundos, y 5,9 de ellos están en una consulta a pedidos».

Esta lección instrumenta la tienda de MercadoFresco, activa el rastreo en la Lambda mercadofresco-estado-pedido y vuelve al pedido de 8,14 segundos para verlo, esta vez, en un solo gráfico.

Aviso de coste. X-Ray cobra por traza registrada y por traza recuperada o escaneada. Es barato si el muestreo está bien configurado y ruinoso si se traza el 100 % de un tráfico alto. Hay una sección entera sobre estrategia de muestreo económica.

Contenido

  1. Por qué las métricas y los registros no bastan
  2. El modelo de datos: traza, segmento y subsegmento
  3. El identificador de traza y su propagación
  4. Muestreo: por qué no se trazan todas las peticiones
  5. Reglas de muestreo de MercadoFresco
  6. Anotaciones frente a metadatos
  7. Una traza completa de un pedido, en un diagrama
  8. Permisos que necesita X-Ray
  9. Instrumentar la tienda con aws_xray_sdk
  10. Subsegmentos manuales y anotaciones útiles
  11. El demonio de X-Ray en EC2 y en contenedores
  12. Activar el rastreo en mercadofresco-estado-pedido
  13. Activar el rastreo en el ALB, en CloudFront y en API Gateway
  14. El mapa de servicios: cómo se lee
  15. Filtros de traza: el lenguaje de búsqueda
  16. Análisis de latencia: histogramas y percentiles
  17. El caso guiado: el pedido de 8,14 segundos
  18. X-Ray Insights
  19. Relación con CloudWatch ServiceLens
  20. OpenTelemetry y ADOT: la alternativa abierta
  21. Coste y estrategia de muestreo económica
  22. Limpieza

Por qué las métricas y los registros no bastan

Métricas (05-01) Registros (05-01) Trazas (X-Ray)
Unidad Serie temporal agregada Línea de texto Petición completa
Responde ¿Cuánto? ¿Va bien? ¿Qué pasó exactamente? ¿Dónde se fue el tiempo?
Conserva el caso individual No
Conserva la relación causal No No
Cardinalidad admitida Muy baja Alta Alta
Coste dominante Nº de series GB ingeridos Nº de trazas
Retención típica 15 meses Días o semanas 30 días
Detecta Que algo va mal Qué error salió Qué componente es el culpable

La columna que lo cambia todo es «conserva la relación causal». Un registro de la tienda dice llamada a lambda estado-pedido y otro registro, en otro grupo, dice REPORT Duration: 480 ms. Que esos dos eventos pertenezcan a la misma petición es algo que tienes que reconstruir. X-Ray lo sabe de nacimiento, porque el identificador viaja con la petición.

Y hay tres preguntas que solo una traza puede responder:

  • ¿Cuánto tiempo estuvo la petición esperando frente a trabajando? Un registro con «duración total 8.140 ms» no distingue entre calcular y esperar a otro.
  • ¿Qué llamadas se hicieron en paralelo y cuáles en serie? Es la diferencia entre 39 consultas de 190 ms secuenciales (7,4 s) y 39 consultas en paralelo (0,2 s).
  • ¿Qué pasó antes del código? El tiempo de arranque en frío de una Lambda, el establecimiento de la conexión TLS, la resolución de DNS. Nada de eso aparece en el log de tu aplicación, porque tu aplicación todavía no se estaba ejecutando.

El modelo de datos: traza, segmento y subsegmento

Tres conceptos y una jerarquía:

  • Traza (trace): todo lo que ocurre a raíz de una petición. Se identifica con un ID de traza y agrupa segmentos de todos los servicios implicados.
  • Segmento (segment): el trabajo de un servicio o recurso dentro de la traza. La tienda genera un segmento; la Lambda genera el suyo. Contiene nombre, hora de inicio y fin, y estado.
  • Subsegmento: un tramo dentro de un segmento. Una consulta SQL, una llamada HTTP a otro servicio, un bloque de cálculo que quieres medir.
flowchart TD
    T["TRAZA 1-68a2f4c1-3b8d9e2a5f7c1b4d8e0a2f6c<br/>duracion total: 8.140 ms"]
    T --> S1["SEGMENTO: mercadofresco-tienda<br/>0 ms - 8.140 ms"]
    S1 --> SS1["subsegmento: validar_stock<br/>27 ms - 84 ms"]
    S1 --> SS2["subsegmento: Invoke estado-pedido<br/>140 ms - 690 ms"]
    S1 --> SS3["subsegmento: SQL insertar lineas<br/>700 ms - 8.110 ms"]
    SS3 --> SS3a["39 consultas SELECT precio<br/>190 ms cada una, EN SERIE"]
    T --> S2["SEGMENTO: mercadofresco-estado-pedido<br/>145 ms - 685 ms"]
    S2 --> SS4["subsegmento: Initialization<br/>145 ms - 460 ms - arranque en frio"]
    S2 --> SS5["subsegmento: SNS Publish<br/>500 ms - 660 ms"]

Fíjate en el detalle que hace útil este modelo: el segmento de la Lambda (145-685 ms) está contenido en el subsegmento Invoke de la tienda (140-690 ms), y la diferencia de 10 ms es la latencia de red y de la propia API de Lambda. Esa cifra no aparece en ningún log de ninguno de los dos servicios.

Un segmento, en su representación JSON real —simplificada—, se ve así:

{
  "trace_id": "1-68a2f4c1-3b8d9e2a5f7c1b4d8e0a2f6c",
  "id": "6b1c2d3e4f5a6b7c",
  "name": "mercadofresco-tienda",
  "start_time": 1785142692.004,
  "end_time": 1785142700.144,
  "http": {
    "request": {
      "method": "POST",
      "url": "https://mercadofresco.example/api/pedidos/confirmar",
      "client_ip": "203.0.113.45",
      "user_agent": "Mozilla/5.0 ..."
    },
    "response": { "status": 200, "content_length": 412 }
  },
  "aws": {
    "ec2": { "instance_id": "i-0abc123def456", "availability_zone": "eu-west-1a" }
  },
  "annotations": {
    "pedido_id": "48213",
    "metodo_pago": "tarjeta",
    "num_lineas": 38,
    "entorno": "produccion"
  },
  "metadata": {
    "default": {
      "carrito": { "productos": ["tomate-rama", "lechuga-batavia", "..."] }
    }
  },
  "subsegments": [
    {
      "id": "7c2d3e4f5a6b7c8d",
      "name": "validar_stock",
      "start_time": 1785142692.031,
      "end_time": 1785142692.088
    }
  ]
}

Los campos error, fault y throttle marcan el resultado, y su distinción importa porque el mapa de servicios los pinta de colores distintos:

Campo Significa Código HTTP Color en el mapa
error Error del cliente 4xx Amarillo
fault Error del servidor 5xx Rojo
throttle Estrangulamiento 429 Morado
(ninguno) Correcto 2xx / 3xx Verde

El identificador de traza y su propagación

Un ID de traza tiene esta forma:

1-68a2f4c1-3b8d9e2a5f7c1b4d8e0a2f6c
│  │        └── 96 bits aleatorios en hexadecimal
│  └── marca de tiempo Unix del origen, en hexadecimal
└── version (siempre 1)

Que la marca de tiempo esté dentro del identificador no es decorativo: permite a X-Ray localizar la traza sin índice global, y es la razón de que no se puedan consultar trazas de más de 30 días.

La propagación se hace con la cabecera HTTP X-Amzn-Trace-Id:

X-Amzn-Trace-Id: Root=1-68a2f4c1-3b8d9e2a5f7c1b4d8e0a2f6c;Parent=6b1c2d3e4f5a6b7c;Sampled=1
Campo Qué es
Root El ID de la traza. Lo genera el primer componente instrumentado.
Parent El ID del segmento que hace la llamada. Es lo que construye el árbol.
Sampled 1 = trázala; 0 = no la trace. La decisión se toma una vez y se respeta.
Self Lo añade el ALB cuando ya venía una cabecera del cliente.
sequenceDiagram
    participant C as Cliente
    participant CF as CloudFront
    participant ALB as alb-mercadofresco-tienda
    participant T as Tienda EC2
    participant L as Lambda estado-pedido
    participant D as DynamoDB / RDS

    C->>CF: POST /api/pedidos/confirmar
    CF->>ALB: (reenvia)
    ALB->>T: X-Amzn-Trace-Id: Root=1-68a2...;Sampled=1
    Note over ALB: El ALB GENERA la cabecera<br/>si no venia
    T->>L: Invoke + Root=1-68a2...;Parent=6b1c...
    L->>D: Query + Root=1-68a2...;Parent=9d4e...
    D-->>L: resultado
    L-->>T: respuesta
    T-->>C: 200 OK

Tres reglas que hay que interiorizar:

  1. El primer componente instrumentado genera el Root. Si el ALB tiene rastreo activo, lo genera él. Si no, lo genera tu aplicación.
  2. La decisión de muestreo se toma una sola vez, en el origen, y todos la respetan. Si el Root llega con Sampled=0, la Lambda no enviará su segmento aunque tenga el rastreo activado. Esto evita trazas incompletas y es un comportamiento que confunde mucho al depurar: «he activado X-Ray en la Lambda y no veo nada» suele significar que la decisión de muestreo se tomó aguas arriba.
  3. Los SDK propagan la cabecera automáticamente en las llamadas salientes que interceptan. En una llamada que no interceptan —un cliente HTTP exótico, una cola propia— tienes que propagarla tú.

Muestreo: por qué no se trazan todas las peticiones

MercadoFresco recibe, en un viernes normal, del orden de 2 millones de peticiones diarias. Trazar todas costaría, a 5 USD por millón de trazas registradas, unos 300 USD al mes solo en registro, sin contar las recuperaciones. Y el 99,9 % de esas trazas serían idénticas y aburridas: peticiones de 80 ms que funcionan.

El muestreo decide qué fracción se traza. Una regla de muestreo tiene dos partes:

  • reservoir (depósito): un número fijo de peticiones por segundo que se trazan siempre. Es el suelo: garantiza que siempre tengas ejemplos, incluso con tráfico bajísimo.
  • fixed_rate: el porcentaje del resto que se traza. Es el techo proporcional: garantiza representatividad cuando el tráfico sube.

Con reservoir: 1 y fixed_rate: 0.05:

Peticiones/segundo Del depósito Del 5 % restante Total trazado
1 1 0 1 (100 %)
10 1 0,45 ~1,45 (14,5 %)
100 1 4,95 ~5,95 (6 %)
1.000 1 49,95 ~51 (5,1 %)

Ese es el diseño: a volumen bajo trazas casi todo (y te cuesta nada); a volumen alto trazas un porcentaje estable (y el coste crece de forma controlada).

Las reglas se evalúan por prioridad, de menor a mayor número, y gana la primera que coincide. Los criterios de coincidencia son: service_name, service_type, host, http_method, url_path, y atributos de la petición.

Reglas de muestreo de MercadoFresco

La estrategia de Marta: trazar poco lo barato y frecuente, trazar mucho lo caro e importante.

Prioridad Nombre Coincide con Depósito Tasa fija Motivo
100 muestreo-mercadofresco-checkout POST /api/pedidos/* 2/s 100 % Es el dinero
200 muestreo-mercadofresco-admin /admin/* 1/s 50 % Poco tráfico, mucho valor
300 muestreo-mercadofresco-estaticos /static/*, /favicon.ico 0/s 0 % Ruido puro
400 muestreo-mercadofresco-salud /salud 0/s 0 % El health check cada 15 s
9000 muestreo-mercadofresco-defecto Todo lo demás 1/s 5 % Representatividad
# La regla que importa: trazar el 100% de las confirmaciones de pedido.
aws xray create-sampling-rule --cli-input-json '{
  "SamplingRule": {
    "RuleName": "muestreo-mercadofresco-checkout",
    "Priority": 100,
    "FixedRate": 1.0,
    "ReservoirSize": 2,
    "ServiceName": "mercadofresco-tienda",
    "ServiceType": "*",
    "Host": "*",
    "HTTPMethod": "POST",
    "URLPath": "/api/pedidos/*",
    "Version": 1,
    "ResourceARN": "*",
    "Attributes": {}
  }
}' --profile mercadofresco-dev --region eu-west-1

# Y la que ahorra mas dinero: NO trazar el health check.
aws xray create-sampling-rule --cli-input-json '{
  "SamplingRule": {
    "RuleName": "muestreo-mercadofresco-salud",
    "Priority": 400,
    "FixedRate": 0.0,
    "ReservoirSize": 0,
    "ServiceName": "*",
    "ServiceType": "*",
    "Host": "*",
    "HTTPMethod": "GET",
    "URLPath": "/salud",
    "Version": 1,
    "ResourceARN": "*"
  }
}' --profile mercadofresco-dev --region eu-west-1

Ese /salud merece un cálculo. El ALB comprueba /salud cada 15 segundos contra cada instancia (03-03). Con 4 instancias en el pico: 4 × 4 = 16 peticiones por minuto, 691.200 al mes. Al 5 % serían 34.560 trazas mensuales de una respuesta de 3 ms que dice OK. No aportan absolutamente nada y ensucian los histogramas de latencia con miles de puntos en 3 ms que desplazan los percentiles. Excluirlos es la primera optimización que hay que hacer siempre.

Las reglas se gestionan de forma centralizada: el SDK las descarga cada 10 segundos desde la API de X-Ray. Cambias una regla en la consola y todas las instancias del ASG la aplican en segundos, sin desplegar código. Esto permite algo muy útil: subir el muestreo al 100 % durante un incidente y bajarlo cuando termina.

Anotaciones frente a metadatos

Es la distinción más práctica de X-Ray y la que decide si podrás encontrar algo o no:

Anotación Metadato
¿Indexada? No
¿Se puede filtrar por ella? No
Límite 50 por traza Tamaño del segmento (64 KB)
Tipos Cadena, número, booleano Cualquier JSON
Uso pedido_id, metodo_pago, provincia El carrito completo, la respuesta de la pasarela

La regla es simple: si vas a buscar por ello, es una anotación; si solo quieres verlo cuando abras la traza, es un metadato.

Y aquí está la ventaja frente a CloudWatch que conviene subrayar. En 05-01 vimos que poner PedidoId como dimensión de métrica costaría 190.000 USD al mes. Como anotación de X-Ray es gratis y además buscable: annotation.pedido_id = "48213". Es exactamente el hueco que las métricas no pueden cubrir.

Las anotaciones de MercadoFresco, elegidas para responder preguntas reales:

Anotación Ejemplo Pregunta que responde
pedido_id "48213" «El cliente dice que su pedido tardó mucho»
cliente_hash "a3f8c1e9" «¿Le pasa siempre a este cliente?»
metodo_pago "tarjeta" «¿Es lento solo con una pasarela?»
num_lineas 38 «¿La lentitud crece con el tamaño del pedido?»
provincia "Madrid" «¿Es un problema de una zona de reparto?»
version_app "2.14.3" «¿Empezó con el último despliegue?»
entorno "produccion" Separar producción de pruebas

Aviso de privacidad. Las anotaciones y los metadatos se almacenan en X-Ray y son visibles para cualquiera con permiso de lectura. Nunca pongas ahí correos, nombres, direcciones, números de tarjeta ni identificadores directos de persona. Por eso MercadoFresco usa cliente_hash, un hash del identificador interno, y no el correo. Si manejas datos personales bajo el RGPD, esta decisión debe revisarla quien lleve el cumplimiento en tu organización.

Una traza completa de un pedido, en un diagrama

gantt
    title Traza 1-68a2f4c1 - confirmacion del pedido 48213 - total 8.140 ms
    dateFormat X
    axisFormat %L ms

    section Tienda EC2
    Segmento tienda           :active, t1, 0, 8140
    validar_stock             :done,   t2, 27, 57
    Invoke estado-pedido      :done,   t3, 140, 550
    SQL insertar cabecera     :done,   t4, 700, 40
    SQL 39 SELECT precio      :crit,   t5, 745, 7410
    responder                 :done,   t6, 8110, 30

    section Lambda estado-pedido
    Segmento lambda           :active, l1, 145, 540
    Initialization arranque frio :crit, l2, 145, 315
    SNS Publish               :done,   l3, 500, 160

    section RDS pedidos
    Consultas PostgreSQL      :crit,   r1, 745, 7402

Un gráfico así —que en la consola de X-Ray se llama vista de cascada y se genera solo— cuenta la historia entera en dos segundos de lectura: hay una barra roja de 7,4 segundos y todo lo demás es irrelevante en comparación. Compáralo con la hoja de cálculo que Marta montó a mano en 05-01.

Permisos que necesita X-Ray

El rol rol-mercadofresco-tienda necesita poder enviar segmentos y descargar las reglas de muestreo. AWS tiene una política gestionada para esto:

aws iam attach-role-policy \
  --role-name rol-mercadofresco-tienda \
  --policy-arn arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess \
  --profile mercadofresco-dev

Si prefieres el mínimo privilegio explícito, que es lo que enseñamos en 04-01, el contenido efectivo es este:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "EnviarTrazas",
      "Effect": "Allow",
      "Action": [
        "xray:PutTraceSegments",
        "xray:PutTelemetryRecords"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DescargarReglasDeMuestreo",
      "Effect": "Allow",
      "Action": [
        "xray:GetSamplingRules",
        "xray:GetSamplingTargets",
        "xray:GetSamplingStatisticSummaries"
      ],
      "Resource": "*"
    }
  ]
}

Ninguna de esas acciones admite ARN de recurso —X-Ray no tiene recursos por traza—, así que "Resource": "*" es correcto aquí; es el mismo caso que cloudwatch:PutMetricData en 05-01.

Para el rol de la Lambda, rol-lambda-miniaturas y el de mercadofresco-estado-pedido, basta con la política gestionada AWSXRayDaemonWriteAccess, o la aún más específica AWSXRayWriteOnlyAccess.

Y para leer trazas, el grupo de personas que hace guardia necesita:

{
  "Effect": "Allow",
  "Action": [
    "xray:GetTraceSummaries",
    "xray:BatchGetTraces",
    "xray:GetServiceGraph",
    "xray:GetTraceGraph",
    "xray:GetTimeSeriesServiceStatistics",
    "xray:GetGroups",
    "xray:GetInsightSummaries",
    "xray:GetInsight"
  ],
  "Resource": "*"
}

Instrumentar la tienda con aws_xray_sdk

Instalación:

pip install aws-xray-sdk

Y la instrumentación mínima de una aplicación Flask, que es lo que corre en las instancias del ASG:

"""Instrumentacion de X-Ray en la tienda de MercadoFresco."""
from flask import Flask, request, jsonify
from aws_xray_sdk.core import xray_recorder, patch_all
from aws_xray_sdk.ext.flask.middleware import XRayMiddleware

# 1. Configurar el grabador ANTES de crear la aplicacion.
xray_recorder.configure(
    service="mercadofresco-tienda",       # nombre del nodo en el mapa
    daemon_address="127.0.0.1:2000",      # donde escucha el demonio
    context_missing="LOG_ERROR",          # NO lanzar excepcion si falta contexto
    sampling=True,                        # usar las reglas centralizadas
    plugins=("EC2Plugin",),               # añade instance_id y AZ al segmento
)

# 2. patch_all instrumenta automaticamente las librerias soportadas:
#    boto3, botocore, requests, httplib, sqlite3, psycopg2, pymysql, aiohttp...
patch_all()

app = Flask(__name__)

# 3. El middleware crea un segmento por cada peticion HTTP entrante,
#    lee la cabecera X-Amzn-Trace-Id y respeta la decision de muestreo.
XRayMiddleware(app, xray_recorder)

Cuatro decisiones que hay que entender, porque cada una evita un problema real:

  • context_missing="LOG_ERROR" es probablemente el parámetro más importante del fichero. Con el valor por defecto (RUNTIME_ERROR), cualquier código instrumentado que se ejecute fuera de una petición HTTP —una tarea programada, un script de mantenimiento, un hilo de fondo, el publicador de métricas por lotes de 05-01— lanza una excepción y tumba la tarea. Con LOG_ERROR, escribe un error en el log y sigue. La observabilidad no debe tumbar la aplicación, nunca.
  • plugins=("EC2Plugin",) hace que cada segmento incluya el instance_id y la zona de disponibilidad. Cuando una sola instancia del ASG esté degradada, esa anotación es la que lo revela. Hay equivalentes ECSPlugin y ElasticBeanstalkPlugin.
  • patch_all() incluye psycopg2, el controlador de PostgreSQL. Eso significa que cada consulta a mercadofresco-pedidos genera un subsegmento automáticamente, con el texto de la consulta sanitizado. Es exactamente lo que va a delatar el N+1.
  • daemon_address: el SDK no habla con la API de X-Ray. Escribe paquetes UDP a un demonio local, que se encarga de agrupar y enviar. Por eso una llamada instrumentada añade microsegundos, no milisegundos.

Si prefieres instrumentar más finamente, patch() acepta la lista de módulos: patch(("boto3", "psycopg2", "requests")). Evita instrumentar sqlite3 en producción si lo usas para cachés locales de alta frecuencia: generarías miles de subsegmentos inútiles.

Subsegmentos manuales y anotaciones útiles

La instrumentación automática cubre las llamadas a servicios externos. Los tramos de tu código hay que marcarlos tú:

"""Confirmacion de pedido, instrumentada."""
import hashlib
from aws_xray_sdk.core import xray_recorder


@app.route("/api/pedidos/confirmar", methods=["POST"])
def confirmar_pedido():
    datos = request.get_json()
    pedido_id = datos["pedido_id"]

    # ANOTACIONES: indexadas y filtrables. Lo primero que se hace,
    # para que esten presentes aunque la peticion falle despues.
    segmento = xray_recorder.current_segment()
    segmento.put_annotation("pedido_id", str(pedido_id))
    segmento.put_annotation("metodo_pago", datos["metodo_pago"])
    segmento.put_annotation("num_lineas", len(datos["lineas"]))
    segmento.put_annotation("provincia", datos["direccion"]["provincia"])
    segmento.put_annotation("version_app", app.config["VERSION"])
    # Hash, NUNCA el correo ni el identificador directo del cliente.
    segmento.put_annotation(
        "cliente_hash",
        hashlib.sha256(datos["cliente_id"].encode()).hexdigest()[:8],
    )

    # METADATOS: no indexados, pero visibles al abrir la traza.
    segmento.put_metadata("carrito", datos["lineas"], "negocio")
    segmento.put_metadata("importe_total", datos["total_eur"], "negocio")

    # SUBSEGMENTO como gestor de contexto: se cierra solo, incluso si hay excepcion.
    with xray_recorder.in_subsegment("validar_stock") as sub:
        disponible = validar_stock(datos["lineas"])
        sub.put_annotation("stock_ok", disponible)
        if not disponible:
            # error = problema del cliente (4xx), amarillo en el mapa.
            sub.add_error_flag()
            return jsonify({"error": "sin stock"}), 409

    with xray_recorder.in_subsegment("calcular_portes"):
        portes = calcular_portes(datos["direccion"])

    # Esta llamada NO necesita subsegmento manual: patch_all ha instrumentado
    # boto3, y la invocacion de Lambda aparece sola en la traza.
    respuesta = cliente_lambda.invoke(
        FunctionName="mercadofresco-estado-pedido",
        Payload=json.dumps({"pedido_id": pedido_id}),
    )

    # Tampoco esta: psycopg2 esta instrumentado y cada consulta
    # genera su propio subsegmento con el SQL sanitizado.
    with xray_recorder.in_subsegment("persistir_pedido"):
        guardar_pedido(pedido_id, datos["lineas"], portes)

    return jsonify({"estado": "confirmado", "pedido_id": pedido_id}), 200

Y el decorador, para funciones que se llaman desde varios sitios:

from aws_xray_sdk.core import xray_recorder


@xray_recorder.capture("calcular_portes")
def calcular_portes(direccion):
    """Cada llamada a esta funcion crea su propio subsegmento."""
    ...

Trabajo en hilos y en tareas asíncronas

El fallo de instrumentación más común en producción. El SDK guarda el segmento actual en una variable de contexto por hilo. Si lanzas un hilo nuevo, ese hilo no tiene contexto y todo lo instrumentado que ejecute fallará o se perderá:

import threading
from aws_xray_sdk.core import xray_recorder

def procesar_en_paralelo(entidad):
    # Capturar el segmento en el hilo padre...
    segmento_padre = xray_recorder.current_subsegment() or xray_recorder.current_segment()

    def trabajo():
        # ...y restaurarlo en el hilo hijo.
        xray_recorder.context.put_segment(segmento_padre)
        with xray_recorder.in_subsegment("trabajo_paralelo"):
            hacer_algo(entidad)

    threading.Thread(target=trabajo).start()

Si esto te parece frágil, tienes razón: lo es. Es uno de los argumentos serios a favor de OpenTelemetry, que resuelve la propagación de contexto de forma más limpia. Lo vemos al final de la lección.

El demonio de X-Ray en EC2 y en contenedores

El demonio de X-Ray es un proceso ligero que escucha en el puerto 2000/UDP, agrupa los segmentos que le mandan los SDK y los envía a la API de X-Ray por lotes.

flowchart LR
    A["Aplicacion<br/>aws_xray_sdk"] -->|"UDP 2000<br/>microsegundos"| D["Demonio X-Ray<br/>proceso local"]
    D -->|"HTTPS por lotes<br/>cada segundo"| X["API de X-Ray<br/>eu-west-1"]

¿Por qué esta arquitectura? Porque desacopla la aplicación de la red. Si la API de X-Ray está lenta o caída, la aplicación no se entera: sigue escribiendo paquetes UDP que, en el peor caso, se pierden. Una traza perdida no es un problema; una tienda bloqueada esperando a la API de trazas, sí.

Instalación en las instancias del ASG, añadida al user data de lt-mercadofresco-tienda:

#!/bin/bash
set -euo pipefail

# Amazon Linux 2023. AWS publica el paquete en un bucket por region.
curl -fsSL -o /tmp/xray.rpm \
  "https://s3.dualstack.eu-west-1.amazonaws.com/aws-xray-assets.eu-west-1/xray-daemon/aws-xray-daemon-3.x.rpm"
dnf install -y /tmp/xray.rpm

cat > /etc/amazon/xray/cfg.yaml <<'YAML'
TotalBufferSizeMB: 16
Concurrency: 8
Region: eu-west-1
Socket:
  UDPAddress: 127.0.0.1:2000
  TCPAddress: 127.0.0.1:2000
LocalMode: false
LogLevel: warn
YAML

systemctl enable --now xray
systemctl is-active xray || exit 1

Notas de campo:

  • UDPAddress: 127.0.0.1:2000, no 0.0.0.0. Solo debe escuchar en la interfaz local; nadie de fuera tiene por qué enviarle segmentos.
  • TotalBufferSizeMB: si el buffer se llena, el demonio descarta segmentos y lo anota en su log. Ver SegmentsRejectedCount creciendo significa que hay que subirlo o bajar el muestreo.
  • No hace falta abrir nada en sg-mercadofresco-tienda: el tráfico es local. La salida hacia la API de X-Ray sale por el NAT (03-01), o mejor aún, por un endpoint de interfaz de VPC para com.amazonaws.eu-west-1.xray si quieres que ni siquiera pase por internet.

En contenedores (módulo 10), el demonio se despliega como un contenedor sidecar en la misma definición de tarea, y la aplicación lo alcanza por localhost en ECS con red awsvpc. Se detalla en 10-01.

Activar el rastreo en mercadofresco-estado-pedido

En Lambda no hay demonio que instalar: AWS lo ejecuta por ti dentro del entorno de ejecución. Basta una casilla y un permiso.

# 1. El permiso
aws iam attach-role-policy \
  --role-name rol-mercadofresco-estado-pedido \
  --policy-arn arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess \
  --profile mercadofresco-dev

# 2. La casilla: modo de rastreo activo
aws lambda update-function-configuration \
  --function-name mercadofresco-estado-pedido \
  --tracing-config Mode=Active \
  --profile mercadofresco-dev --region eu-west-1

Los dos modos:

Modo Comportamiento
PassThrough (defecto) Solo traza si la petición ya venía con Sampled=1
Active La función decide el muestreo si no venía decidido

Para mercadofresco-estado-pedido, que la llama la tienda, PassThrough sería suficiente y más barato. Para mercadofresco-generar-miniaturas, que la dispara un evento de S3 y no tiene origen instrumentado, hace falta Active o no se trazará nunca nada.

Con solo eso ya obtienes el segmento con la duración, el arranque en frío y los errores. Para ver dentro de la función, hay que instrumentar igual que en EC2:

"""mercadofresco-estado-pedido, instrumentada."""
import json
import os
import boto3
from aws_xray_sdk.core import xray_recorder, patch_all

# En Lambda el demonio esta en la direccion de la variable de entorno,
# que el propio entorno de ejecucion define. No hay que configurar nada.
patch_all()

sns = boto3.client("sns")
TEMA = os.environ["ARN_TEMA_ALERTAS"]


def handler(evento, contexto):
    pedido_id = evento["pedido_id"]

    # En Lambda, el segmento raiz lo crea el entorno de ejecucion y es
    # de solo lectura: las anotaciones van en un SUBSEGMENTO.
    subsegmento = xray_recorder.begin_subsegment("procesar_estado")
    try:
        subsegmento.put_annotation("pedido_id", str(pedido_id))
        subsegmento.put_annotation("origen", evento.get("origen", "tienda"))

        estado = calcular_estado(pedido_id)
        subsegmento.put_annotation("estado_resultante", estado)

        # boto3 esta parcheado: esta llamada aparece sola como subsegmento
        # y ademas dibuja el nodo de SNS en el mapa de servicios.
        sns.publish(
            TopicArn=TEMA,
            Subject=f"Pedido {pedido_id}: {estado}",
            Message=json.dumps({"pedido_id": pedido_id, "estado": estado}),
        )
        return {"estado": estado}

    except Exception as e:
        subsegmento.add_exception(e, [])   # marca fault: rojo en el mapa
        raise
    finally:
        xray_recorder.end_subsegment()

El detalle que hace perder tiempo a todo el mundo la primera vez: en Lambda no puedes anotar el segmento raíz. Lo crea y lo controla el entorno de ejecución. xray_recorder.current_segment() existe, pero put_annotation sobre él se ignora silenciosamente. Las anotaciones van siempre en un subsegmento propio.

Otro detalle valioso: el subsegmento Initialization que ves en las trazas de Lambda es el arranque en frío. Si tu p99 de Duration es malo pero el p50 es bueno, mira ese subsegmento: casi siempre es un import pesado que se puede mover dentro del handler o reducir. Es el diagnóstico que en 02-05 solo podíamos intuir.

Activar el rastreo en el ALB, en CloudFront y en API Gateway

Servicio Qué aporta Cómo se activa
ALB Genera X-Amzn-Trace-Id si no viene Automático, siempre activo
API Gateway Segmento propio con latencia de integración Casilla por etapa
CloudFront No genera segmentos de X-Ray Se correlaciona por x-amz-cf-id
SQS / SNS Propagan la cabecera Automático (07-01, 07-02)
Step Functions Segmento por estado Casilla (07-04)

El ALB genera la cabecera automáticamente y no hay nada que activar: es la razón por la que la tienda de MercadoFresco recibe un Root sin que nadie lo haya programado. Lo que el ALB no hace es generar un segmento propio, así que su tiempo de proceso no aparece como nodo en el mapa. Esa latencia se sigue viendo en la métrica TargetResponseTime de CloudWatch (05-01), y ese es un buen ejemplo de que las dos herramientas se complementan.

Si algún día MercadoFresco pone una API detrás de API Gateway:

aws apigateway update-stage \
  --rest-api-id abc123def4 --stage-name produccion \
  --patch-operations op=replace,path=/tracingEnabled,value=true \
  --profile mercadofresco-dev --region eu-west-1

CloudFront es la pieza que falta, y conviene ser honesto: CloudFront no participa en las trazas de X-Ray. Su identificador propio es x-amz-cf-id, que aparece en sus registros de acceso. Para correlacionar el tiempo de la CDN con la traza, MercadoFresco registra ese valor como anotación:

cf_id = request.headers.get("X-Amz-Cf-Id")
if cf_id:
    segmento.put_annotation("cf_id", cf_id)

Con eso, dada una traza puedes buscar la línea correspondiente en los registros de CloudFront en S3, y al revés. No es tan cómodo como un nodo en el mapa, pero cierra el círculo.

El mapa de servicios: cómo se lee

El mapa de servicios es el gráfico que X-Ray construye agregando todas las trazas de la ventana temporal seleccionada. No es un diagrama de arquitectura dibujado por nadie: es lo que realmente pasa, deducido del tráfico.

flowchart LR
    C(("Cliente")) --> T["mercadofresco-tienda<br/>1.240 t/min<br/>lat. media 0,18 s<br/>errores 0,2%"]
    T --> L["mercadofresco-estado-pedido<br/>Lambda<br/>lat. media 0,48 s"]
    T --> R[("mercadofresco-pedidos<br/>PostgreSQL<br/>lat. media 2,10 s<br/>ROJO")]
    T --> S3[("mercadofresco-catalogo-fotos<br/>S3")]
    L --> SN["alertas-mercadofresco<br/>SNS"]
    L --> R

Cómo se lee, elemento a elemento:

Elemento Significado
Círculo Un nodo: un servicio, un recurso o el cliente
Tamaño del círculo Volumen de tráfico
Anillo verde Peticiones correctas
Anillo amarillo error — fallos 4xx (culpa del cliente)
Anillo rojo fault — fallos 5xx (culpa tuya)
Anillo morado throttle — estrangulamiento (429)
Flecha Llamada de un nodo a otro
Nodo de cliente El origen: no es un servicio tuyo

Los tipos de nodo también importan: X-Ray distingue nodos de servicio (algo que tú instrumentas, con su propio segmento) de nodos de recurso descendente (una base de datos, un bucket de S3, que no están instrumentados pero cuyo tiempo se mide desde el llamante). Un nodo de RDS rojo significa que las llamadas a RDS fallan o tardan, no que el servidor de PostgreSQL esté informando de nada.

Tres patrones que se reconocen de un vistazo en un mapa real:

  1. Un nodo rojo aislado con todo lo demás verde: el culpable está claro.
  2. Todo rojo aguas abajo de un nodo: hay un fallo en cascada; el culpable es el más profundo.
  3. Un nodo que aparece de repente y no debería estar ahí: una dependencia que alguien introdujo sin decirlo. Es una de las razones por las que este mapa es útil incluso sin incidentes.

Los grupos permiten tener un mapa filtrado, por ejemplo solo del flujo de compra:

aws xray create-group \
  --group-name grupo-mercadofresco-pedidos \
  --filter-expression 'service("mercadofresco-tienda") AND annotation.entorno = "produccion" AND http.url CONTAINS "/api/pedidos"' \
  --insights-configuration InsightsEnabled=true,NotificationsEnabled=true \
  --profile mercadofresco-dev --region eu-west-1

Un grupo con InsightsEnabled genera además métricas propias en CloudWatch, sobre las que se pueden crear alarmas.

Filtros de traza: el lenguaje de búsqueda

Aquí está la potencia real de X-Ray. El lenguaje de expresiones de filtro:

Expresión Encuentra
service("mercadofresco-tienda") Trazas que pasan por ese servicio
service("mercadofresco-pedidos") { fault } Trazas donde ese nodo falló
responsetime > 5 Trazas de más de 5 segundos
duration > 3 AND duration < 10 Entre 3 y 10 segundos
http.status = 500 Por código de respuesta
http.url CONTAINS "/api/pedidos" Por ruta
annotation.pedido_id = "48213" Un pedido concreto
annotation.cliente_hash = "a3f8c1e9" Todas las peticiones de un cliente
annotation.num_lineas > 25 Pedidos grandes
annotation.version_app = "2.14.3" Solo la versión nueva
error = true Fallos 4xx
fault = true Fallos 5xx
throttle = true Estrangulamientos
service("estado-pedido") { fault } AND responsetime > 2 Combinaciones con AND, OR, NOT
edge("mercadofresco-tienda", "mercadofresco-pedidos") Trazas que recorren esa arista concreta

Desde la CLI:

# Todas las confirmaciones lentas de las ultimas 3 horas
aws xray get-trace-summaries \
  --start-time $(date -d '3 hours ago' +%s) \
  --end-time $(date +%s) \
  --filter-expression 'http.url CONTAINS "/api/pedidos/confirmar" AND responsetime > 5' \
  --query 'TraceSummaries[].[Id,Duration,ResponseTime,Http.HttpStatus]' \
  --output table \
  --profile mercadofresco-dev --region eu-west-1

# Y el detalle completo de una traza concreta
aws xray batch-get-traces \
  --trace-ids 1-68a2f4c1-3b8d9e2a5f7c1b4d8e0a2f6c \
  --profile mercadofresco-dev --region eu-west-1

Un script que Marta usa cuando llega una queja concreta, y que sustituye a las cuatro consultas de Logs Insights de 05-01:

"""Buscar la traza de un pedido concreto y desglosar donde se fue el tiempo."""
import boto3
import time

xray = boto3.client("xray", region_name="eu-west-1")


def investigar_pedido(pedido_id, horas=24):
    ahora = int(time.time())
    resumenes = xray.get_trace_summaries(
        StartTime=ahora - horas * 3600,
        EndTime=ahora,
        FilterExpression=f'annotation.pedido_id = "{pedido_id}"',
    )

    if not resumenes["TraceSummaries"]:
        print(f"Sin trazas para el pedido {pedido_id}.")
        print("Causas posibles: no fue muestreado, o han pasado mas de 30 dias.")
        return

    for resumen in resumenes["TraceSummaries"]:
        traza_id = resumen["Id"]
        print(f"\nTraza {traza_id}: {resumen['Duration']:.3f} s")

        detalle = xray.batch_get_traces(TraceIds=[traza_id])
        for traza in detalle["Traces"]:
            for segmento in traza["Segments"]:
                import json
                doc = json.loads(segmento["Document"])
                dur = doc["end_time"] - doc["start_time"]
                print(f"  [{dur*1000:8.1f} ms] {doc['name']}")
                for sub in doc.get("subsegments", []):
                    d = sub["end_time"] - sub["start_time"]
                    marca = " <-- AQUI" if d > 1.0 else ""
                    print(f"      [{d*1000:8.1f} ms] {sub['name']}{marca}")


investigar_pedido("48213")

Análisis de latencia: histogramas y percentiles

La consola de X-Ray ofrece, para cada nodo del mapa y para cada búsqueda, un histograma de latencia en escala logarítmica. Es la herramienta que revela lo que un percentil resume.

Un histograma unimodal —una sola joroba— significa que todas las peticiones se comportan parecido: si es lento, es lento para todos, y el problema es estructural.

Un histograma bimodal —dos jorobas— significa que hay dos poblaciones distintas de peticiones, y ese es el hallazgo más valioso que ofrece X-Ray. El de mercadofresco-tienda en el pico del viernes:

Latencia Peticiones Interpretación
60-200 ms 94 % Peticiones normales
200 ms - 2 s 4 % Fichas de producto con muchas fotos
5-9 s 2 % La segunda joroba: el problema

Un p95 de 1,8 s no habría mostrado nada raro. El p99 de 8,2 s sí. Pero lo que de verdad convence es ver que hay dos jorobas separadas: no es una degradación general, es un subconjunto concreto de peticiones que se comporta de otra manera. Y en X-Ray puedes seleccionar la segunda joroba en el histograma con el ratón y ver solo esas trazas. Eso lleva directamente al caso guiado.

Una comparación útil que hay que aprender a hacer, entre dos versiones de la aplicación:

# Latencia p95 antes del despliegue de la version 2.14.3
aws xray get-time-series-service-statistics \
  --start-time $(date -d '2026-07-29 00:00' +%s) \
  --end-time   $(date -d '2026-07-30 00:00' +%s) \
  --group-name grupo-mercadofresco-pedidos \
  --entity-selector-expression 'service("mercadofresco-tienda")' \
  --period 3600 \
  --profile mercadofresco-dev --region eu-west-1

Y el filtro que aísla exactamente el efecto de un despliegue, que es la pregunta que más veces se hace tras publicar una versión:

annotation.version_app = "2.14.3" AND responsetime > 2

El caso guiado: el pedido de 8,14 segundos

Volvemos al pedido 48213, esta vez con X-Ray. Compara el esfuerzo con las cuatro consultas de 05-01.

Paso 1. Encontrar la traza. Una sola búsqueda:

annotation.pedido_id = "48213"

Aparece una traza, 8,14 s. En 05-01 esto costó dos consultas de Insights y saber el hash del cliente.

Paso 2. Abrir la vista de cascada. Se ve inmediatamente:

Tramo Duración % del total
validar_stock 57 ms 0,7 %
Invoke mercadofresco-estado-pedido 550 ms 6,8 %
Initialization (arranque en frío) 315 ms 3,9 %
calcular_portes 12 ms 0,1 %
persistir_pedido 40 ms 0,5 %
SELECT × 39 sobre productos 7.410 ms 91,0 %
Resto 71 ms 0,9 %

Paso 3. Ver la naturaleza del problema. Al desplegar el subsegmento de la base de datos aparecen 39 subsegmentos consecutivos, cada uno de unos 190 ms, con el mismo SQL sanitizado:

SELECT precio, iva, promocion FROM productos WHERE id = ?

Esto es lo que un log no puede enseñar: la forma. Treinta y nueve barras idénticas, una detrás de otra, sin solaparse. El diagnóstico se lee en el gráfico antes de que nadie lea el SQL. Es el patrón N+1: el código recorre las líneas del pedido y consulta el precio de cada producto por separado.

Paso 4. Confirmar que es sistemático y no un caso aislado. El filtro:

annotation.num_lineas > 25 AND responsetime > 4

Devuelve 187 trazas en 24 horas. Y el complementario:

annotation.num_lineas < 10 AND responsetime > 4

Devuelve 2. La lentitud crece con el número de líneas del pedido: confirmado. Esta es exactamente la pregunta que las anotaciones estaban ahí para responder, y por eso num_lineas era una anotación y no un metadato.

Paso 5. Cuantificar el impacto en el negocio. Con el filtro anterior sobre 30 días: 5.610 pedidos afectados, con más de 4 segundos de espera en la confirmación. Son los pedidos más grandes, es decir, los de mayor importe. Ese dato es el que convierte una tarea técnica en una prioridad.

Paso 6. Arreglar. Una sola consulta en lugar de 39:

SELECT id, precio, iva, promocion FROM productos WHERE id = ANY(%s);

Paso 7. Verificar con datos, no con impresiones. Se despliega como versión 2.14.4 y se compara:

annotation.version_app = "2.14.4" AND annotation.num_lineas > 25
Métrica Antes (2.14.3) Después (2.14.4)
p50 de confirmación 1,9 s 0,21 s
p95 6,8 s 0,44 s
p99 8,4 s 0,61 s
Subsegmentos SQL por pedido 39-42 3
DatabaseConnections en el pico 185 96

Fíjate en la última fila, que es un efecto secundario que nadie había previsto: al eliminar el N+1, la presión sobre mercadofresco-pedidos cae a la mitad. La alarma mercadofresco-rds-conexiones-altas de 05-01 deja de estar al borde todos los viernes. Un problema de latencia era también un problema de capacidad.

Y el paso 8, el que cierra el ciclo con 05-01: ahora que sabemos cuál es el problema, se crea una alarma para que no vuelva sin avisar. El grupo grupo-mercadofresco-pedidos con Insights activado publica métricas en CloudWatch, y la métrica TiempoConfirmacionPedido que creamos en 05-01 ya vale para detectar la regresión. X-Ray diagnostica; CloudWatch vigila. Ninguno sustituye al otro.

X-Ray Insights

Insights analiza continuamente las trazas de un grupo, construye una línea base de comportamiento normal y detecta anomalías sin que tú definas umbrales. Cuando encuentra una, abre un «insight» con:

  • El momento de inicio y el estado (activo o resuelto).
  • La causa raíz probable: el nodo y el tipo de error que más contribuye.
  • El impacto: número de peticiones y usuarios afectados.
  • Un gráfico de anomalía frente a la línea base.
aws xray update-group \
  --group-name grupo-mercadofresco-pedidos \
  --insights-configuration InsightsEnabled=true,NotificationsEnabled=true \
  --profile mercadofresco-dev --region eu-west-1

aws xray get-insight-summaries \
  --start-time $(date -d '7 days ago' +%s) --end-time $(date +%s) \
  --states ACTIVE CLOSED \
  --group-name grupo-mercadofresco-pedidos \
  --profile mercadofresco-dev --region eu-west-1

Con NotificationsEnabled=true, cada insight genera un evento de EventBridge (07-03), que se puede encaminar a alertas-mercadofresco. Ese es el enlace que convierte X-Ray de herramienta de diagnóstico en herramienta de detección.

Dos limitaciones honestas:

  • Insights detecta anomalías respecto a lo habitual, no problemas. Si tu servicio siempre ha tardado 8 segundos, nunca lo marcará.
  • Necesita volumen. Con poco tráfico muestreado, la línea base es ruidosa y produce falsos positivos. Es una razón más para no muestrear al 1 %.

Relación con CloudWatch ServiceLens

ServiceLens es la vista de CloudWatch que une las tres señales en una sola pantalla. Es literalmente el mapa de servicios de X-Ray, pero con las métricas de CloudWatch y un acceso directo a los registros correlacionados.

Desde ServiceLens puedes… Y así…
Ver el mapa con métricas de CloudWatch superpuestas Latencia, tasa de error y peticiones por nodo
Pinchar un nodo y ver sus métricas Sin cambiar de consola
Pinchar un nodo y ver sus registros Filtrados por la ventana temporal del pico
Pasar de un pico de latencia a las trazas de ese pico En dos clics
Ver los contribuyentes principales al error Qué URL, qué instancia

Requisito: X-Ray activo y, para la correlación de registros, que los grupos estén asociados. El flujo de trabajo que ServiceLens habilita, y que es el que Marta acaba usando a diario:

flowchart LR
    A["Alarma CloudWatch<br/>latencia p95 alta"] --> B["ServiceLens:<br/>que nodo esta rojo?"]
    B --> C["X-Ray: trazas<br/>de ese pico"]
    C --> D["Cascada:<br/>que subsegmento?"]
    D --> E["Logs Insights:<br/>que decia el log<br/>en ese instante"]
    E --> F["Diagnostico"]

Métrica → mapa → traza → subsegmento → log. Ese es el camino completo, y ahora MercadoFresco lo tiene entero.

OpenTelemetry y ADOT: la alternativa abierta

Sería deshonesto enseñar X-Ray sin decir que hoy el estándar de la industria es OpenTelemetry (OTel), un proyecto de la CNCF que define una API, un SDK y un protocolo (OTLP) neutrales respecto al proveedor.

AWS Distro for OpenTelemetry (ADOT) es la distribución de AWS de OpenTelemetry, soportada oficialmente, que puede enviar trazas a X-Ray y a cualquier otro destino a la vez.

X-Ray SDK OpenTelemetry / ADOT
Estándar Propietario de AWS Abierto (CNCF)
Señales Trazas Trazas, métricas y registros
Destinos X-Ray X-Ray, Prometheus, Jaeger, Grafana, Datadog…
Lenguajes 6 oficiales Docenas
Instrumentación automática Buena en AWS Muy amplia, con agente automático en Java/Python/Node
Propagación de contexto Propia (X-Amzn-Trace-Id) W3C Trace Context (traceparent) + AWS
Madurez en AWS Muy alta, años Alta y creciendo
Complejidad inicial Baja Media (hay un colector que configurar)
Riesgo de dependencia Alto Bajo
Recomendación de AWS hoy Soportado Preferido para proyectos nuevos

Qué recomendar, con criterio:

  • Proyecto nuevo, o con intención de no atarse a AWS: OpenTelemetry con ADOT. El coste inicial extra —configurar el colector— se paga solo la primera vez que quieras enviar las mismas trazas a otro sitio, o migrar.
  • Proyecto existente ya instrumentado con el SDK de X-Ray, todo en AWS: no hay urgencia. Funciona, está soportado y migrar tiene coste.
  • Caso de MercadoFresco: Marta ha instrumentado con el SDK de X-Ray porque es lo más rápido de poner en marcha y todo su sistema está en AWS. Ha anotado en el registro de decisiones técnicas que la migración a ADOT es la evolución natural cuando la aplicación crezca o si algún día se plantea multinube. No es deuda técnica oculta: es una decisión consciente con fecha de revisión.

Una nota de compatibilidad que ahorra un mal rato: X-Ray propaga X-Amzn-Trace-Id y OTel propaga traceparent (W3C). Si mezclas ambos en un mismo sistema, hay que configurar el propagador xray en OTel para que las trazas no se rompan en la frontera.

Un ejemplo mínimo de ADOT en Python, para que veas la diferencia de forma:

pip install aws-opentelemetry-distro

# La instrumentacion automatica no toca tu codigo
OTEL_PYTHON_DISTRO=aws_distro \
OTEL_PYTHON_CONFIGURATOR=aws_configurator \
OTEL_TRACES_EXPORTER=otlp_proto_http \
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:4318/v1/traces \
OTEL_PROPAGATORS=xray \
OTEL_RESOURCE_ATTRIBUTES="service.name=mercadofresco-tienda" \
opentelemetry-instrument python app.py

Fíjate en que no hay una sola línea de código de la aplicación: opentelemetry-instrument envuelve el proceso e instrumenta las librerías conocidas. Eso, para una aplicación grande y existente, es una ventaja enorme.

Coste y estrategia de muestreo económica

Concepto Precio de referencia Capa gratuita mensual
Traza registrada 5,00 USD por millón 100.000
Traza recuperada o escaneada 0,50 USD por millón 1.000.000
X-Ray Insights 1,00 USD por millón de trazas analizadas
Almacenamiento Incluido (30 días)

«Traza recuperada» es cada vez que alguien abre una traza o ejecuta un filtro. Consultar es barato; registrar es lo caro.

Cálculo para MercadoFresco. Tráfico: unos 2 millones de peticiones al día, 60 millones al mes.

Escenario A: sin reglas, muestreo por defecto (1/s + 5 %).

Concepto Trazas/mes Coste
Registradas ~3,3 millones 16,50 USD
Recuperadas ~200.000 0,00 USD (capa gratuita)
Total ~16,50 USD

Escenario B: con las reglas de MercadoFresco.

Regla Peticiones/mes Muestreo Trazas
/salud 691.200 0 % 0
Estáticos 34.000.000 0 % 0
/api/pedidos/* 660.000 100 % 660.000
/admin/* 40.000 50 % 20.000
Resto 24.600.000 1/s + 5 % ~1.400.000
Total registradas ~2,08 millones
Concepto Coste
Registradas: (2.080.000 − 100.000) × 5 / 1.000.000 9,90 USD
Recuperadas: ~500.000 0,00 USD
Insights sobre el grupo de pedidos ~0,70 USD
Total ~10,60 USD/mes

Menos coste que el escenario A y con el 100 % de las confirmaciones de pedido trazadas, que es lo único que de verdad importa. Eso es lo que hace una buena estrategia de muestreo: no traza menos, traza mejor.

Escenario C: el error, muestreo al 100 % de todo.

60 millones de trazas: 300 USD/mes, más el impacto en el rendimiento del demonio y en la usabilidad de la consola, que se llena de trazas de /salud. Nunca lo hagas fuera de una ventana de depuración acotada.

Las cinco reglas de la estrategia económica:

  1. Excluir siempre /salud y los estáticos. Es el 57 % del tráfico de MercadoFresco y su valor diagnóstico es cero.
  2. Trazar al 100 % las rutas críticas de negocio. Son pocas peticiones y son las que importan.
  3. Depósito de al menos 1/s en la regla por defecto, para tener ejemplos incluso de madrugada.
  4. Subir el muestreo temporalmente durante un incidente y bajarlo al terminar. Las reglas son centralizadas y se aplican en 10 segundos, sin desplegar.
  5. Vigilar el coste con un presupuesto filtrado por servicio = X-Ray (módulo 11).

Y una advertencia de rendimiento, no de coste: cada subsegmento tiene un precio en tamaño. Un segmento no puede pasar de 64 KB, y un patch_all() sobre una aplicación que hace miles de consultas pequeñas por petición genera segmentos enormes que el demonio acaba descartando. Si ves SegmentsRejectedCount en el log del demonio, revisa qué estás instrumentando.

Limpieza

# Desactivar el rastreo en Lambda
aws lambda update-function-configuration \
  --function-name mercadofresco-estado-pedido \
  --tracing-config Mode=PassThrough \
  --profile mercadofresco-dev --region eu-west-1

# Borrar las reglas de muestreo
for R in muestreo-mercadofresco-checkout muestreo-mercadofresco-admin \
         muestreo-mercadofresco-estaticos muestreo-mercadofresco-salud; do
  aws xray delete-sampling-rule --rule-name "$R" \
    --profile mercadofresco-dev --region eu-west-1
done

# Borrar el grupo
aws xray delete-group --group-name grupo-mercadofresco-pedidos \
  --profile mercadofresco-dev --region eu-west-1

# Quitar los permisos
aws iam detach-role-policy --role-name rol-mercadofresco-tienda \
  --policy-arn arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess \
  --profile mercadofresco-dev

# Y en las instancias, detener el demonio
sudo systemctl disable --now xray

Importante: las trazas ya registradas no se pueden borrar y expiran solas a los 30 días. No hay almacenamiento que siga costando. Lo que sí hay que hacer es quitar la instrumentación del código si no vas a seguir usándola, o al menos dejar context_missing="LOG_ERROR" para que un SDK sin demonio no genere errores en el log.

Errores Comunes y Consejos

1. «He activado X-Ray y no veo nada». Por orden de probabilidad: (a) el demonio no está corriendo o no escucha en 2000/UDP; (b) falta xray:PutTraceSegments en el rol; (c) la petición llegó con Sampled=0 y la decisión se respeta aguas abajo; (d) estás mirando la región equivocada.

2. Poner anotaciones en el segmento raíz de una Lambda. Se ignoran en silencio: el segmento raíz lo controla el entorno de ejecución. Las anotaciones van en un subsegmento creado por ti.

3. Dejar context_missing en su valor por defecto. Cualquier código instrumentado fuera de una petición HTTP —tareas programadas, hilos, scripts— lanzará excepciones y tumbará el proceso. Pon siempre LOG_ERROR.

4. Confundir anotación con metadato. Si no puedes filtrar por algo, es que lo pusiste como metadato. Solo las anotaciones están indexadas, y hay un máximo de 50 por traza.

5. Poner datos personales en anotaciones. Correo, nombre, dirección, tarjeta: nunca. Usa un hash. Las trazas las ve todo el que tenga permiso de lectura, y viven 30 días.

6. Trazar el health check. El /salud cada 15 segundos genera decenas de miles de trazas inútiles al mes, ensucia los histogramas y desplaza los percentiles. Es la primera exclusión que hay que crear.

7. Muestrear al 100 % «para no perderse nada». 300 USD al mes en el caso de MercadoFresco, una consola inservible y el demonio descartando segmentos. Trazar mejor no es trazar más.

8. Perder el contexto en hilos. El segmento vive en una variable por hilo. Si lanzas un hilo, tienes que pasarle el segmento explícitamente. Es el fallo más frecuente en aplicaciones con paralelismo.

9. Buscar trazas de hace dos meses. No existen: X-Ray retiene 30 días y punto. Si necesitas análisis histórico, exporta lo que te interese a S3 mientras esté disponible.

10. Esperar que CloudFront aparezca en el mapa. No aparece. Correlaciona con la anotación cf_id y los registros de la CDN en S3.

11. Instrumentar sin propagar en las llamadas propias. Si tu código llama a otro servicio con un cliente HTTP que el SDK no parchea, la cabecera X-Amzn-Trace-Id no viaja y la traza se corta ahí. Se propaga a mano.

12. Usar X-Ray como sustituto de los registros. No lo es. Las trazas están muestreadas: la petición que buscas puede no estar. Los registros lo tienen todo. Se complementan: la traza te dice dónde mirar, el registro te dice qué decía.

Consejo final: la inversión que más rendimiento da en X-Ray no es la instrumentación, sino elegir bien cinco o seis anotaciones. pedido_id, cliente_hash y num_lineas son las que han resuelto este caso. Piensa qué preguntas te van a hacer —«a este cliente le pasa siempre», «empezó con el despliegue del martes», «solo con pedidos grandes»— y anota exactamente lo que hace falta para responderlas con un filtro.

Ejercicios

Ejercicio 1: diseñar la estrategia de muestreo de un servicio nuevo

MercadoFresco lanza una API pública para que las tiendas de barrio asociadas consulten stock y creen pedidos al por mayor. Tráfico previsto:

Ruta Peticiones/día Latencia típica Criticidad
GET /api/v1/stock/{sku} 4.000.000 25 ms Media
GET /api/v1/catalogo 120.000 300 ms Media
POST /api/v1/pedidos-mayoristas 3.000 1,8 s Máxima
POST /api/v1/facturas 800 4,0 s Máxima
GET /salud 5.760 2 ms Nula
GET /api/v1/docs 400 40 ms Nula

Requisitos: cada pedido y cada factura deben poder investigarse individualmente; hay que poder diagnosticar la latencia de /stock sin arruinarse; el presupuesto de X-Ray es de 25 USD al mes.

Diseña el conjunto completo de reglas de muestreo con prioridades, depósitos y tasas. Calcula el número de trazas mensuales y el coste. Justifica cada regla y di qué anotaciones pondrías en la API.

Ejercicio 2: leer una cascada e identificar tres problemas

Esta es la cascada de una traza de 6,45 segundos de POST /api/pedidos/confirmar:

[0 ms]      mercadofresco-tienda                                  6.450 ms
[12 ms]     ├─ subsegmento: cargar_configuracion                     890 ms
[905 ms]    ├─ subsegmento: SecretsManager GetSecretValue            310 ms
[1.220 ms]  ├─ subsegmento: validar_stock                             45 ms
[1.270 ms]  ├─ subsegmento: SELECT ... FROM productos WHERE id=?      95 ms
[1.370 ms]  ├─ subsegmento: SELECT ... FROM productos WHERE id=?      92 ms
[1.465 ms]  ├─ subsegmento: SELECT ... FROM productos WHERE id=?      88 ms
[1.560 ms]  ├─ (repetido 11 veces mas, en serie)                   1.030 ms
[2.595 ms]  ├─ subsegmento: Invoke mercadofresco-estado-pedido     3.780 ms
[2.610 ms]  │   └─ SEGMENTO Lambda                                 3.750 ms
[2.615 ms]  │       ├─ Initialization                              2.980 ms
[5.600 ms]  │       ├─ SNS Publish                                    95 ms
[5.700 ms]  │       └─ SELECT ... FROM pedidos                        60 ms
[6.380 ms]  └─ subsegmento: responder                                 70 ms

Identifica al menos tres problemas distintos, ordénalos por impacto en milisegundos, propón una solución concreta para cada uno y estima la latencia resultante. Indica además qué anotación o subsegmento añadirías para poder detectar cada uno de estos problemas de forma automática en el futuro.

Ejercicio 3: el caso que X-Ray no resuelve solo

Un lunes, Sara avisa de que el 3 % de los pedidos de la última semana no han llegado a generar la factura. Los clientes tienen su pedido confirmado y su reparto en marcha, pero no hay factura. No hay errores en CloudWatch, ninguna alarma ha saltado, mercadofresco-estado-pedido no registra Errors, y en X-Ray las trazas de esos pedidos aparecen completas y en verde.

Arquitectura implicada: la tienda confirma el pedido, publica en el tema alertas-mercadofresco, y un proceso de facturación suscrito a ese tema genera el PDF y lo sube a mercadofresco-informes-analitica. Ese proceso de facturación no está instrumentado con X-Ray.

Explica: por qué X-Ray no lo ha detectado, qué le falta a la observabilidad de MercadoFresco, qué combinación de las herramientas de 05-01 y 05-02 usarías para diagnosticarlo, y qué instrumentación y qué alarma dejarías montadas para que la próxima vez se detecte en minutos. Sé concreto con los comandos.

Soluciones

Solución 1

Análisis previo. El tráfico total es de 4.124.960 peticiones/día ≈ 124 millones/mes. Al muestreo por defecto (5 %) serían 6,2 millones de trazas: 30,50 USD. Nos pasamos del presupuesto, y encima gastaríamos casi todo en trazas de /stock idénticas.

Reglas propuestas:

Prioridad Nombre Coincide Depósito Tasa Trazas/mes
100 muestreo-api-facturas POST /api/v1/facturas 1/s 100 % 24.000
110 muestreo-api-pedidos-mayoristas POST /api/v1/pedidos-mayoristas 1/s 100 % 90.000
200 muestreo-api-catalogo GET /api/v1/catalogo 1/s 10 % ~400.000
300 muestreo-api-stock GET /api/v1/stock/* 1/s 0,5 % ~600.000
800 muestreo-api-salud GET /salud 0/s 0 % 0
810 muestreo-api-docs GET /api/v1/docs 0/s 0 % 0
9000 muestreo-api-defecto Resto 1/s 5 % ~50.000

Cálculo detallado de /stock, que es donde está el 97 % del tráfico y donde se decide todo:

  • 4.000.000/día ÷ 86.400 s = 46,3 peticiones/segundo.
  • Depósito: 1/s × 2.592.000 s/mes = 2.592.000… que ya es demasiado. Cuidado con el depósito cuando el volumen es alto: el depósito es un suelo, no un techo, y con 46 peticiones por segundo siempre hay una que trazar.

Rehacemos: con depósito 1/s el suelo mensual es de 2,59 millones de trazas solo de /stock (12,95 USD). Es demasiado para lo que aporta. Depósito 0 y tasa fija del 0,05 %:

  • 124.000.000 × 0,0005 = 62.000 trazas/mes. Suficiente para un histograma de latencia representativo de una ruta que responde en 25 ms.

Tabla corregida:

Prioridad Nombre Depósito Tasa Trazas/mes
100 muestreo-api-facturas 1/s 100 % 24.000
110 muestreo-api-pedidos-mayoristas 1/s 100 % 90.000
200 muestreo-api-catalogo 1/s 10 % ~360.000
300 muestreo-api-stock 0/s 0,05 % ~62.000
800 muestreo-api-salud 0/s 0 % 0
810 muestreo-api-docs 0/s 0 % 0
9000 muestreo-api-defecto 1/s 5 % ~50.000
Total ~586.000

Coste: (586.000 − 100.000) × 5 / 1.000.000 = 2,43 USD/mes en registro, más recuperaciones dentro de la capa gratuita. Muy por debajo de los 25 USD, y con el 100 % de facturas y pedidos mayoristas trazado.

Con el margen sobrante se puede subir /catalogo al 25 % y activar Insights sobre el grupo de la API.

La lección del ejercicio: el depósito es peligroso en rutas de altísimo volumen, porque garantiza un suelo de una traza por segundo que en un mes son 2,6 millones. Para el tráfico masivo, depósito 0 y tasa muy baja; para el tráfico escaso y valioso, depósito alto y tasa 100 %.

Anotaciones propuestas para la API:

Anotación Motivo
tienda_id Cada tienda asociada es un cliente; hay que poder aislarla
factura_id / pedido_mayorista_id Requisito explícito: investigación individual
num_lineas Lo mismo que en la tienda: la lentitud crece con el tamaño
version_api v1, v2: comparar versiones
plan_tarifa ¿Los clientes premium tienen mejor latencia?
sku No: 40.000 valores. Iría como metadato o en el log

Solución 2

Problema 1 — Arranque en frío de la Lambda: 2.980 ms (46 % del total).

El subsegmento Initialization de casi 3 segundos es un arranque en frío patológico. Lo normal en Python es 200-600 ms; 2.980 ms indica import muy pesados —típicamente pandas, numpy, un SDK completo— o una conexión establecida en el ámbito del módulo.

Soluciones, por orden de coste-beneficio:

  • Revisar los import y mover al handler los que solo se usan en algunos caminos.
  • Usar boto3.client() a nivel de módulo (eso está bien: se reutiliza entre invocaciones) pero no abrir conexiones a la base de datos ahí.
  • Reducir el tamaño del paquete de despliegue; usar capas para las dependencias grandes.
  • Si tras eso sigue siendo alto y la latencia importa: concurrencia aprovisionada (02-05), que elimina el arranque en frío a cambio de pagar por capacidad reservada.

Ahorro estimado: de 2.980 a ~400 ms. −2.580 ms.

Problema 2 — El N+1, otra vez: 1.305 ms (20 %).

Catorce SELECT ... FROM productos WHERE id=? en serie, de unos 93 ms cada uno. El mismo patrón del caso guiado, en otra ruta.

Solución: WHERE id = ANY(...) en una sola consulta. Ahorro: de 1.305 a ~100 ms. −1.205 ms.

Nota: 93 ms por un SELECT por clave primaria también es alto. Merece revisar si hay un índice adecuado, si la conexión se está estableciendo en cada consulta (no hay agrupación de conexiones), o si la instancia de RDS está saturada. Un subsegmento conectar_bd separado lo aclararía.

Problema 3 — Configuración cargada en cada petición: 890 ms (14 %).

cargar_configuracion de 890 ms al principio de cada petición es configuración leída de disco, de red o de Parameter Store en caliente. Debería cargarse una vez al arrancar el proceso y guardarse en memoria.

Solución: cargar al inicio del proceso y refrescar cada N minutos en un hilo de fondo. Ahorro: de 890 a ~1 ms. −889 ms.

Problema 4 — Secrets Manager en la ruta crítica: 310 ms (5 %).

GetSecretValue en cada petición. Como vimos en 04-03, el secreto rota cada 30 días: no hay ninguna razón para leerlo en cada compra. Se cachea con un TTL de 5-15 minutos y se reintenta al recibir un error de autenticación.

Ahorro: de 310 a ~0 ms en el 99,9 % de las peticiones. −310 ms.

Problema 5 — La llamada a la Lambda es síncrona y bloqueante.

Los 3.780 ms del Invoke están enteramente en la ruta crítica del cliente. Pero, ¿de verdad hace falta esperar a que se calcule el estado del pedido antes de responder «confirmado»? Casi con seguridad no: es un buen candidato a invocación asíncrona o a una cola (07-01).

Ahorro potencial: los 3.780 ms enteros desaparecen de lo que el cliente percibe.

Resumen ordenado por impacto:

# Problema Ahorro
1 Arranque en frío de la Lambda −2.580 ms
2 N+1 de productos −1.205 ms
3 Configuración en cada petición −889 ms
4 Secreto sin caché −310 ms
5 Invocación síncrona innecesaria −3.780 ms (arquitectural)

Latencia resultante aplicando 1-4: 6.450 − 4.984 = ~1.470 ms. Aplicando además el 5: ~430 ms. De 6,45 s a menos de medio segundo.

Instrumentación para detectarlo automáticamente en el futuro:

Qué añadir Detecta
Subsegmento conectar_bd separado de la consulta Falta de agrupación de conexiones
Anotación num_consultas_sql por petición El N+1, con un filtro annotation.num_consultas_sql > 10
Anotación cache_config_hit (booleano) Configuración recargada indebidamente
Anotación arranque_frio en la Lambda Trazas afectadas por arranque en frío
Alarma sobre el p99 de Duration de la Lambda La regresión de arranque en frío
X-Ray Insights en el grupo Cambios de comportamiento no previstos

La anotación num_consultas_sql merece un comentario: es una anotación derivada, calculada por la propia aplicación contando las consultas de la petición. Con ella, un filtro annotation.num_consultas_sql > 10 encuentra todos los N+1 del sistema, presentes y futuros, sin saber de antemano dónde están. Es el tipo de anotación que distingue una instrumentación pensada de una copiada.

Solución 3

Por qué X-Ray no lo detectó, y es importante entenderlo bien.

X-Ray traza lo que está instrumentado. El proceso de facturación no lo está, así que para X-Ray no existe. Las trazas de la tienda terminan en SNS Publish con estado 200 —el mensaje se publicó correctamente— y todo aparece verde. El fallo está después del último punto instrumentado, y ese es el punto ciego estructural de cualquier sistema de trazas.

Y hay un segundo motivo, más sutil: aunque instrumentaras el facturador, el patrón es asíncrono. La tienda publica y se olvida. La traza de la tienda termina al publicar; la del facturador sería una traza distinta, disparada por el mensaje. X-Ray las relaciona si el SDK propaga la cabecera a través de SNS (lo hace, en los atributos del mensaje), pero la ausencia de una traza no genera ninguna señal. Nadie está contando cuántos mensajes deberían haberse procesado.

Lo que le falta a la observabilidad de MercadoFresco: no tiene ninguna comprobación de que las dos mitades de un proceso asíncrono cuadren. Es el hueco clásico de las arquitecturas basadas en eventos, y se resuelve con reconciliación, no con trazas.

Diagnóstico, paso a paso:

1. Cuantificar y acotar en el tiempo. Consulta directa a la base de datos para saber cuántos y cuándo:

SELECT date_trunc('hour', p.confirmado_en) AS hora,
       count(*) FILTER (WHERE f.id IS NULL) AS sin_factura,
       count(*) AS total
FROM pedidos p
LEFT JOIN facturas f ON f.pedido_id = p.id
WHERE p.confirmado_en > now() - interval '7 days'
GROUP BY 1 ORDER BY 1;

Si los fallos se concentran en unas horas concretas, hay una causa puntual; si están repartidos uniformemente al 3 %, hay un fallo probabilístico —tiempo de espera agotado, condición de carrera, límite de concurrencia—.

2. Comprobar el eslabón de SNS. Las métricas de SNS en CloudWatch dicen si el mensaje salió y si la entrega falló:

aws cloudwatch get-metric-statistics \
  --namespace AWS/SNS --metric-name NumberOfNotificationsFailed \
  --dimensions Name=TopicName,Value=alertas-mercadofresco \
  --start-time $(date -d '7 days ago' -u +%FT%TZ) \
  --end-time $(date -u +%FT%TZ) \
  --period 3600 --statistics Sum \
  --profile mercadofresco-dev --region eu-west-1

Si NumberOfNotificationsFailed es cero, el mensaje llegó al facturador y el problema está dentro de él. Si no es cero, el problema es de entrega y hay que mirar la política de reintentos de SNS (07-02) y si hay cola de mensajes fallidos.

3. Mirar los registros del facturador con Logs Insights. Aquí es donde 05-01 hace el trabajo:

fields @timestamp, @message, pedido_id, error
| filter ispresent(pedido_id)
| stats count() as eventos by pedido_id
| filter eventos < 2
| limit 50

Es decir: pedidos que entraron al facturador pero no llegaron a registrar la línea de finalización. Y la comprobación cruzada definitiva, buscando un pedido concreto sin factura en todos los grupos a la vez:

aws logs start-query \
  --log-group-names /mercadofresco/tienda/aplicacion \
                    /mercadofresco/facturacion/aplicacion \
  --start-time $(date -d '3 days ago' +%s) --end-time $(date +%s) \
  --query-string 'fields @timestamp, @log, @message
                  | filter @message like /48213/
                  | sort @timestamp asc' \
  --profile mercadofresco-dev --region eu-west-1

Si el pedido aparece en la tienda y no aparece nunca en facturación, el mensaje se perdió. Si aparece y se corta a mitad, el proceso murió: memoria, tiempo de espera agotado, excepción no capturada.

Diagnóstico probable —y el más frecuente en este escenario—: el facturador tarda más de lo que permite su tiempo de espera al generar PDF de pedidos grandes, muere, y SNS no reintenta indefinidamente. Sin cola de mensajes fallidos, el mensaje desaparece en silencio. Es un 3 % estable: los pedidos más grandes.

Lo que hay que dejar montado, en cuatro capas:

a) Instrumentar el facturador con X-Ray. Es lo primero y lo más obvio:

from aws_xray_sdk.core import xray_recorder, patch_all
xray_recorder.configure(service="mercadofresco-facturacion",
                        context_missing="LOG_ERROR")
patch_all()

Con anotaciones pedido_id y factura_id. A partir de ahí, la traza de un pedido incluye su facturación y el filtro service("mercadofresco-facturacion") { fault } encuentra los fallos.

b) Una cola de mensajes fallidos. Sin ella no hay forma de saber qué se perdió. MercadoFresco ya tiene el patrón montado con mercadofresco-miniaturas-fallidas (02-05); aquí hace falta el equivalente, y con una alarma sobre ApproximateNumberOfMessagesVisible > 0. El patrón completo —colas, reintentos, idempotencia— es la lección 07-05.

c) La métrica de reconciliación, que es la solución de fondo. Un proceso que cada 15 minutos compara pedidos confirmados con facturas emitidas y publica la diferencia:

"""Reconciliacion: publica cuantos pedidos llevan mas de 30 minutos sin factura."""
import boto3

cw = boto3.client("cloudwatch", region_name="eu-west-1")

pendientes = contar_pedidos_sin_factura(antiguedad_minutos=30)

cw.put_metric_data(
    Namespace="MercadoFresco/Tienda",
    MetricData=[{
        "MetricName": "PedidosSinFactura",
        "Dimensions": [{"Name": "Entorno", "Value": "produccion"}],
        "Value": pendientes,
        "Unit": "Count",
    }],
)

Y su alarma:

aws cloudwatch put-metric-alarm \
  --alarm-name mercadofresco-pedidos-sin-factura \
  --alarm-description "Hay pedidos confirmados sin factura tras 30 minutos" \
  --namespace MercadoFresco/Tienda --metric-name PedidosSinFactura \
  --dimensions Name=Entorno,Value=produccion \
  --statistic Maximum --period 900 --evaluation-periods 2 --datapoints-to-alarm 2 \
  --threshold 5 --comparison-operator GreaterThanThreshold \
  --treat-missing-data breaching \
  --alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

Con --treat-missing-data breaching: si el propio proceso de reconciliación deja de ejecutarse, eso también es un incidente.

d) La regla general que hay que extraer. En un sistema asíncrono, las trazas no detectan lo que no ocurrió. Solo detectan lo que ocurrió mal. Para lo que no ocurrió hace falta una métrica de reconciliación que compare las dos mitades del proceso: pedidos frente a facturas, mensajes enviados frente a procesados, ficheros subidos frente a miniaturas generadas.

Es exactamente el mismo razonamiento que en 05-01 llevó a poner --treat-missing-data breaching en mercadofresco-sin-pedidos: la ausencia de una señal es una señal, pero solo si alguien la está contando.

Conclusión

MercadoFresco ha pasado del «algo va lento» al «esta llamada concreta tarda 6,2 segundos, y 5,9 de ellos están en una consulta». Sabes por qué las métricas y los registros no bastan: las métricas agregan y pierden el caso individual, los registros conservan el caso pero no la relación causal entre lo que pasó en la tienda y lo que pasó en la Lambda. Solo una traza guarda la estructura completa de una petición.

Dominas el modelo: traza, segmento por servicio y subsegmento por tramo, con error (amarillo, 4xx), fault (rojo, 5xx) y throttle (morado). Conoces el ID de traza con su marca de tiempo incrustada —y por qué eso implica los 30 días de retención— y la cabecera X-Amzn-Trace-Id con sus campos Root, Parent y Sampled, con la regla que más despista: la decisión de muestreo se toma una vez en el origen y todos la respetan, así que «he activado X-Ray en la Lambda y no veo nada» casi siempre significa que llegó con Sampled=0.

Sabes configurar el muestreo con su depósito y su tasa fija, y por qué esa combinación traza casi todo cuando hay poco tráfico y un porcentaje estable cuando hay mucho. Has montado las reglas de MercadoFresco: 100 % de /api/pedidos/*, 0 % de /salud y de los estáticos, 5 % por defecto. Y has aprendido que el depósito es traicionero en rutas de altísimo volumen, donde 1/s son 2,6 millones de trazas al mes.

Tienes clara la distinción que decide si encontrarás algo: anotaciones indexadas y filtrables (máximo 50, cardinalidad libre) frente a metadatos que solo se ven al abrir la traza. Y sabes que pedido_id como anotación de X-Ray es gratis y buscable, mientras que como dimensión de métrica en CloudWatch costaría 190.000 USD al mes: es exactamente el hueco que X-Ray cubre. Con la advertencia de privacidad correspondiente: cliente_hash, nunca el correo.

Has instrumentado la tienda con aws_xray_sdkxray_recorder.configure con context_missing="LOG_ERROR" para que la observabilidad no tumbe nunca la aplicación, patch_all() que instrumenta boto3 y psycopg2 de forma que cada consulta genera su subsegmento, y el EC2Plugin que añade la instancia y la AZ—, has creado subsegmentos manuales con in_subsegment, y sabes que en hilos hay que pasar el contexto a mano. Has desplegado el demonio en las instancias del ASG, entendiendo por qué existe: el SDK escribe UDP local y no espera nunca a la red. Y has activado el rastreo en mercadofresco-estado-pedido con una política y Mode=Active, sabiendo que en Lambda las anotaciones van en un subsegmento porque el segmento raíz es de solo lectura, y que el subsegmento Initialization es el arranque en frío que en 02-05 solo podíamos intuir.

Sabes leer el mapa de servicios —tamaño, colores, nodos de cliente y de recurso, y los tres patrones que se reconocen de un vistazo—, y buscar con el lenguaje de filtros: service(), fault, responsetime > 5, edge(), annotation.pedido_id = "48213". Y sabes leer un histograma bimodal, que es donde de verdad se ven las dos poblaciones de peticiones que un percentil resume en un número.

Y has cerrado el caso que arrastrábamos desde el módulo 4. El pedido 48213: una sola búsqueda por anotación, la cascada, y ahí estaba —39 subsegmentos consecutivos de 190 ms, el 91 % del tiempo total, un N+1—. Confirmado como sistemático con annotation.num_lineas > 25 AND responsetime > 4, cuantificado en 5.610 pedidos afectados en 30 días, corregido con WHERE id = ANY(...), y verificado: p95 de 6,8 s a 0,44 s, y de regalo DatabaseConnections de 185 a 96 en el pico del viernes. Compara eso con las cuatro consultas y la hoja de cálculo de 05-01.

Conoces X-Ray Insights con sus notificaciones vía EventBridge, ServiceLens como la vista que une métrica → mapa → traza → subsegmento → log, y OpenTelemetry con ADOT como el estándar abierto que hoy AWS recomienda para proyectos nuevos, con la decisión de MercadoFresco anotada y con fecha de revisión en lugar de escondida. Y sabes que todo esto cuesta unos 10,60 USD al mes gracias a una estrategia de muestreo que traza mejor, no más, frente a los 300 USD de trazarlo todo.

Queda una pregunta del módulo 4 sin responder, y es la que ni las métricas ni las trazas pueden contestar. CloudWatch sabe lo que la aplicación dice de sí misma. X-Ray sabe por dónde pasó una petición. Ninguno de los dos sabe quién llamó a la API de AWS. Nadie sabe todavía quién descifró la última copia de la base de datos con alias/mercadofresco-datos, ni quién leyó el secreto mercadofresco/produccion/rds/mfadmin, ni desde qué dirección IP, ni si alguna de esas llamadas falló con AccessDenied porque alguien estaba probando puertas.

Ese registro existe, se llama AWS CloudTrail y lleva grabándolo todo desde el primer día sin que nadie lo haya mirado. En la lección 05-03, «AWS CloudTrail», veremos la diferencia esencial entre registrar llamadas a la API y registrar lo que dice tu aplicación, el historial gratuito de 90 días frente a un trail persistente, cómo crear trail-mercadofresco hacia un bucket cifrado con validación de integridad —y por qué ese bucket debe ser imposible de borrar—, la anatomía comentada de un evento real de kms:Decrypt, la diferencia de coste entre eventos de gestión y eventos de datos, y cómo investigar con Athena y SQL quién asumió un rol, quién leyó un secreto y qué llamadas fallaron con AccessDenied.

Curso de AWS

Módulo 1: Introducción a AWS

Módulo 2: Servicios principales de AWS

Módulo 3: Redes y entrega de contenido

Módulo 4: Seguridad e identidad

Módulo 5: Monitorización y gestión

Módulo 6: Bases de datos

Módulo 7: Integración de aplicaciones

Módulo 8: Herramientas para desarrolladores

Módulo 9: Infraestructura como código y gobierno de cuentas

Módulo 10: Contenedores en AWS

Módulo 11: Mejores prácticas y gestión de costos

© Copyright 2026. Todos los derechos reservados