Lab práctico · Semana 9: Coste, rendimiento y monitorización de aplicaciones de IA generativa

Observabilidad de Bedrock con invocation logging, dashboards y alarmas

⏱ 90-120 minDificultad: mediaTask statements: 4.3

Qué vas a construir

Un sistema de observabilidad mínimo pero completo para una aplicación que usa Amazon Bedrock:

  • Model invocation logging hacia CloudWatch Logs, con metadatos propios por petición (requestMetadata).
  • Consultas de CloudWatch Logs Insights para saber quién consume tokens y qué peticiones son las más caras.
  • Un metric filter que convierte los logs en una métrica de negocio.
  • Un dashboard con las métricas AWS/Bedrock (invocaciones, latencia, TTFT, tokens, throttling).
  • Dos alarmas: throttling y consumo anómalo de tokens con anomaly detection, notificadas por SNS.
  • Un vistazo a la vista Model Invocations de CloudWatch generative AI observability.

Antes de empezar

  • Región eu-central-1, modelo Nova Micro vía perfil EU (eu.amazon.nova-micro-v1:0).
  • Abre AWS CloudShell en eu-central-1. Necesitas permisos de IAM (crear un rol), CloudWatch, CloudWatch Logs, SNS y Bedrock.
  • Si ya tienes configurado invocation logging en esta región (por ejemplo, del módulo 8), anota la configuración actual con aws bedrock get-model-invocation-logging-configuration para restaurarla al final: la configuración es única por cuenta y región.

Paso 1: variables y grupo de logs

export AWS_REGION=eu-central-1
export CUENTA=$(aws sts get-caller-identity --query Account --output text)
export GRUPO=/lab17/bedrock/invocaciones
export ROL=Lab17BedrockLogsRole
export MODELO=eu.amazon.nova-micro-v1:0

aws logs create-log-group --log-group-name $GRUPO
aws logs put-retention-policy --log-group-name $GRUPO --retention-in-days 1

La retención de 1 día limita el coste y, en un caso real, la definirías según tus requisitos de cumplimiento.

Paso 2: rol de IAM para que Bedrock escriba los logs

cat > confianza.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Service": "bedrock.amazonaws.com"},
    "Action": "sts:AssumeRole",
    "Condition": {
      "StringEquals": {"aws:SourceAccount": "$CUENTA"},
      "ArnLike": {"aws:SourceArn": "arn:aws:bedrock:$AWS_REGION:$CUENTA:*"}
    }
  }]
}
EOF

cat > permisos.json <<EOF
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
    "Resource": "arn:aws:logs:$AWS_REGION:$CUENTA:log-group:$GRUPO:log-stream:aws/bedrock/modelinvocations"
  }]
}
EOF

aws iam create-role --role-name $ROL --assume-role-policy-document file://confianza.json
aws iam put-role-policy --role-name $ROL --policy-name escribir-logs --policy-document file://permisos.json

Las condiciones aws:SourceAccount y aws:SourceArn evitan el problema del confused deputy: solo Bedrock actuando para tu cuenta puede asumir el rol.

Paso 3: activa model invocation logging

aws bedrock put-model-invocation-logging-configuration --logging-config "{
  \"cloudWatchConfig\": {
    \"logGroupName\": \"$GRUPO\",
    \"roleArn\": \"arn:aws:iam::$CUENTA:role/$ROL\"
  },
  \"textDataDeliveryEnabled\": true,
  \"imageDataDeliveryEnabled\": false,
  \"embeddingDataDeliveryEnabled\": false
}"

aws bedrock get-model-invocation-logging-configuration

Si falla con un error de validación del rol, espera un minuto (propagación de IAM) y repite.

Paso 4: genera tráfico con metadatos

Crea trafico.py:

import random
import boto3

brt = boto3.client("bedrock-runtime", region_name="eu-central-1")
MODELO = "eu.amazon.nova-micro-v1:0"
EQUIPOS = ["soporte", "ventas", "marketing"]
PREGUNTAS = [
    "Resume en una frase qué es Amazon S3.",
    "Escribe un eslogan de cinco palabras para una cafetería.",
    "Explica qué es un token en un modelo de lenguaje, en dos frases.",
    "Enumera tres ventajas del streaming de respuestas.",
    "Redacta un correo de 150 palabras disculpándote por un retraso en un pedido.",
]

for i in range(40):
    equipo = random.choice(EQUIPOS)
    pregunta = random.choice(PREGUNTAS)
    if i % 2 == 0:
        r = brt.converse(
            modelId=MODELO,
            messages=[{"role": "user", "content": [{"text": pregunta}]}],
            inferenceConfig={"maxTokens": 400},
            requestMetadata={"equipo": equipo, "funcion": "chat"},
        )
        print(i, equipo, r["usage"]["outputTokens"], r["stopReason"])
    else:
        # ConverseStream publica también la métrica TimeToFirstToken
        r = brt.converse_stream(
            modelId=MODELO,
            messages=[{"role": "user", "content": [{"text": pregunta}]}],
            inferenceConfig={"maxTokens": 400},
            requestMetadata={"equipo": equipo, "funcion": "chat-stream"},
        )
        for evento in r["stream"]:
            if "metadata" in evento:
                print(i, equipo, evento["metadata"]["usage"]["outputTokens"], "stream")
python3 trafico.py

Paso 5: explora los logs

Espera un par de minutos y mira un registro:

aws logs filter-log-events --log-group-name $GRUPO --limit 1 --query "events[0].message" --output text | python3 -m json.tool | head -40

Localiza identity.arn, requestMetadata, input.inputTokenCount, output.outputTokenCount, modelId y los cuerpos completos. Ahora, consumo por equipo con Logs Insights:

QID=$(aws logs start-query --log-group-name $GRUPO \
  --start-time $(date -d '-1 hour' +%s) --end-time $(date +%s) \
  --query-string 'fields requestMetadata.equipo as equipo, input.inputTokenCount as tin, output.outputTokenCount as tout
| stats count() as llamadas, sum(tin) as entrada, sum(tout) as salida by equipo
| sort salida desc' --query queryId --output text)
sleep 5
aws logs get-query-results --query-id $QID --query results

Y las cinco peticiones con más tokens de salida:

QID=$(aws logs start-query --log-group-name $GRUPO \
  --start-time $(date -d '-1 hour' +%s) --end-time $(date +%s) \
  --query-string 'fields @timestamp, requestMetadata.funcion, output.outputTokenCount
| sort output.outputTokenCount desc
| limit 5' --query queryId --output text)
sleep 5
aws logs get-query-results --query-id $QID --query results

Esto es forensic traceability: de una cifra agregada llegas a la petición concreta, con su prompt, su respuesta y quién la hizo.

Paso 6: una métrica de negocio desde los logs

Un metric filter publica una métrica cada vez que un log cumple un patrón. Aquí contamos las respuestas largas (más de 200 tokens):

aws logs put-metric-filter --log-group-name $GRUPO \
  --filter-name respuestas-largas \
  --filter-pattern '{ $.output.outputTokenCount > 200 }' \
  --metric-transformations metricName=RespuestasLargas,metricNamespace=Lab17/GenAI,metricValue=1,defaultValue=0

Vuelve a ejecutar python3 trafico.py para que el filtro empiece a contar (solo procesa logs nuevos).

Paso 7: descubre la dimensión del modelo

aws cloudwatch list-metrics --namespace AWS/Bedrock --metric-name Invocations \
  --query "Metrics[].Dimensions[?Name=='ModelId'].Value" --output text

Guarda el valor que corresponda a Nova Micro:

export DIM=<valor que ha salido>

Paso 8: dashboard

cat > dashboard.json <<EOF
{
  "widgets": [
    {"type": "metric", "x": 0, "y": 0, "width": 12, "height": 6,
     "properties": {"title": "Invocaciones, errores y throttling", "region": "$AWS_REGION", "stat": "Sum", "period": 60,
       "metrics": [["AWS/Bedrock", "Invocations", "ModelId", "$DIM"],
                   [".", "InvocationClientErrors", ".", "."],
                   [".", "InvocationServerErrors", ".", "."],
                   [".", "InvocationThrottles", ".", "."]]}},
    {"type": "metric", "x": 12, "y": 0, "width": 12, "height": 6,
     "properties": {"title": "Latencia y TTFT (p90)", "region": "$AWS_REGION", "stat": "p90", "period": 60,
       "metrics": [["AWS/Bedrock", "InvocationLatency", "ModelId", "$DIM"],
                   [".", "TimeToFirstToken", ".", "."]]}},
    {"type": "metric", "x": 0, "y": 6, "width": 12, "height": 6,
     "properties": {"title": "Tokens", "region": "$AWS_REGION", "stat": "Sum", "period": 300,
       "metrics": [["AWS/Bedrock", "InputTokenCount", "ModelId", "$DIM"],
                   [".", "OutputTokenCount", ".", "."]]}},
    {"type": "metric", "x": 12, "y": 6, "width": 12, "height": 6,
     "properties": {"title": "Respuestas largas (negocio)", "region": "$AWS_REGION", "stat": "Sum", "period": 300,
       "metrics": [["Lab17/GenAI", "RespuestasLargas"]]}}
  ]
}
EOF

aws cloudwatch put-dashboard --dashboard-name Lab17-GenAI --dashboard-body file://dashboard.json

Ábrelo en CloudWatch → Dashboards → Lab17-GenAI. Compara la latencia total con el TTFT: la diferencia es el tiempo de generación.

Paso 9: alarmas con SNS

export TOPIC=$(aws sns create-topic --name lab17-alertas --query TopicArn --output text)
aws sns subscribe --topic-arn $TOPIC --protocol email --notification-endpoint tu-correo@ejemplo.com

Confirma la suscripción desde tu correo. Alarma de throttling:

aws cloudwatch put-metric-alarm --alarm-name lab17-throttling \
  --namespace AWS/Bedrock --metric-name InvocationThrottles \
  --dimensions Name=ModelId,Value=$DIM \
  --statistic Sum --period 300 --evaluation-periods 1 \
  --threshold 0 --comparison-operator GreaterThanThreshold \
  --treat-missing-data notBreaching --alarm-actions $TOPIC

Alarma de consumo anómalo de tokens de salida con anomaly detection (banda de 2 desviaciones):

aws cloudwatch put-metric-alarm --alarm-name lab17-tokens-anomalos \
  --evaluation-periods 2 --comparison-operator GreaterThanUpperThreshold \
  --threshold-metric-id ad1 --treat-missing-data notBreaching --alarm-actions $TOPIC \
  --metrics "[
    {\"Id\": \"m1\", \"ReturnData\": true, \"MetricStat\": {\"Metric\": {\"Namespace\": \"AWS/Bedrock\", \"MetricName\": \"OutputTokenCount\", \"Dimensions\": [{\"Name\": \"ModelId\", \"Value\": \"$DIM\"}]}, \"Period\": 300, \"Stat\": \"Sum\"}},
    {\"Id\": \"ad1\", \"ReturnData\": true, \"Expression\": \"ANOMALY_DETECTION_BAND(m1, 2)\", \"Label\": \"Banda esperada\"}
  ]"

El modelo de anomalías necesita histórico para aprender (hasta dos semanas de datos); en un lab de una hora verás la banda, pero no será representativa. Es normal que la alarma quede en INSUFFICIENT_DATA al principio.

Paso 10: CloudWatch generative AI observability

En la consola de CloudWatch, abre GenAI Observability → Model Invocations. Como ya envías los invocation logs a CloudWatch Logs, verás uso, tokens y latencia por modelo y la tabla de invocaciones. La pestaña de agentes (Bedrock AgentCore) se llenará cuando instrumentes un agente con AgentCore Observability (módulo 5).

Comprueba que funciona

  • get-model-invocation-logging-configuration muestra tu grupo de logs y tu rol.
  • La consulta de Logs Insights devuelve llamadas y tokens por equipo.
  • El dashboard muestra invocaciones, latencia, TTFT y tokens.
  • Las dos alarmas existen (aws cloudwatch describe-alarms --alarm-name-prefix lab17 --query "MetricAlarms[].[AlarmName,StateValue]").

Limpieza

En orden:

aws bedrock delete-model-invocation-logging-configuration
aws cloudwatch delete-alarms --alarm-names lab17-throttling lab17-tokens-anomalos
aws cloudwatch delete-dashboards --dashboard-names Lab17-GenAI
aws logs delete-metric-filter --log-group-name $GRUPO --filter-name respuestas-largas
aws sns delete-topic --topic-arn $TOPIC
aws logs delete-log-group --log-group-name $GRUPO
aws iam delete-role-policy --role-name $ROL --policy-name escribir-logs
aws iam delete-role --role-name $ROL
rm -f confianza.json permisos.json dashboard.json trafico.py

Si tenías una configuración de invocation logging anterior, vuelve a aplicarla con put-model-invocation-logging-configuration en lugar de borrarla.

Preguntas para pensar como arquitecto

  1. El responsable de finanzas quiere ver en Cost Explorer el gasto de Bedrock de cada equipo. ¿Sirve el requestMetadata de este lab?
Respuesta

No. requestMetadata solo aparece en los invocation logs. Para Cost Explorer, crea un application inference profile por equipo con etiquetas, activa esas etiquetas como cost allocation tags y haz que cada equipo invoque con el ARN de su perfil. requestMetadata sigue siendo útil para análisis detallado en Logs Insights.

  1. Auditoría pide conservar siete años los prompts y respuestas, pero el coste de CloudWatch Logs se dispara. ¿Qué cambias?
Respuesta

Envía los invocation logs también (o solo) a S3, con cifrado KMS, políticas de ciclo de vida hacia clases de almacenamiento de archivo y, si hace falta inmutabilidad, S3 Object Lock. Deja en CloudWatch Logs una retención corta para operación y consultas. Consulta los históricos con Athena.

  1. La alarma de anomalías de tokens salta todos los lunes a las 9:00. ¿Es un problema del modelo?
Respuesta

Probablemente no: es el pico semanal de uso. La detección de anomalías de CloudWatch aprende la estacionalidad diaria y semanal con suficiente histórico (hasta dos semanas). Si salta igualmente, ajusta el ancho de la banda o combina la alarma con otra métrica (por ejemplo, tokens por invocación) en una alarma compuesta.

  1. Un agente en producción tarda 20 segundos en algunas peticiones y la métrica InvocationLatency del modelo es normal. ¿Dónde buscas?
Respuesta

En las trazas de extremo a extremo (AgentCore Observability u OpenTelemetry/X-Ray con Transaction Search): la latencia estará en una herramienta lenta, en reintentos por throttling, en la recuperación o en demasiadas vueltas del bucle del agente. Las métricas del modelo solo ven cada llamada individual.


Volver al módulo