Las dos ideas que hicieron el trabajo:
-
Los modos de falla ya son líneas de log. Conviértelos en métricas con
MetricFilter, no con código. -
Las dimensiones por agente/por modelo necesitan EMF, no
PutMetricData, escribes una línea JSON y CloudWatch extrae la métrica.
El punto de partida: una fila en la base de datos no es telemetría
Cada llamada de agente escribía una fila:
# 00-starting-point/usage_log.py (abreviado)
row = AgentUsageLog(
agent_name=self.name, # "summarizer", "classifier", ...
model_used=result.model_used,
input_tokens=result.input_tokens,
output_tokens=result.output_tokens,
cost_usd=result.cost_usd,
latency_ms=result.latency_ms,
success=result.success,
error=result.error,
)
db.add(row)
Datos completos, e inútiles a las 02:00. Para contestar "¿está lento el summarizer ahorita?" tienes que meterte tipo SSH a una base de datos y escribir una consulta de percentiles. No hay alarma, no hay dashboard, no hay dimensión que puedas rebanar sin SQL. Peor: los modos de falla que de verdad te avisan (presupuesto agotado, throttling de Bedrock, servicio caído) o no se registraban de manera distinta o no se vigilaban para nada.
Dos huecos, dos herramientas distintas.
Fase 1: los modos de falla ya son líneas de log
La métrica más barata en AWS es una que derivas de una línea de log que ya estás escribiendo. Los filtros de métrica de CloudWatch Logs escanean un grupo de logs e incrementan una métrica cuando un patrón coincide. Sin IAM, sin llamada a la API.
Teníamos tres modos de falla específicos de IA que valía la pena avisar.
Dos ya se registraban; uno necesitó un cambio de una línea.
Presupuesto agotado. Un tope de costo mensual protege la cuenta de Bedrock. Cuando se activa, cada agente regresa 503 hasta el día 1, la caída de IA de mayor impacto que tenemos. Ya se registraba:
# cost_guard.py, ya estaba
logger.error("monthly_cost_cap_reached", spent_usd=spent, cap_usd=cap)
raise HTTPException(status_code=503, detail="...budget reached...")
Throttling de Bedrock. Este era invisible. Nuestro envoltorio de Bedrock atrapaba cada error de boto, lo envolvía como un BedrockError reintentable, y dejaba que tenacity reintentara. Comportamiento correcto, pero una tormenta de throttling (un pico de carga, o rebasar la cuota de TPS por modelo de la cuenta) se veía idéntica a cualquier otro parpadeo.
Agregamos un clasificador para que los throttles se registren distinto:
# 01-log-metric-filters/bedrock_throttle.py
_THROTTLE_CODES = {
"ThrottlingException", "TooManyRequestsException",
"ServiceQuotaExceededException", "ModelTimeoutException",
"ServiceUnavailableException",
}
def _is_throttle(exc) -> bool:
code = getattr(exc, "response", {}).get("Error", {}).get("Code", "")
return code in _THROTTLE_CODES
# en el bloque except, antes de re-lanzar:
if _is_throttle(exc):
logger.warning("bedrock_throttled", model=model_id)
Ahora tres patrones, tres métricas, en CDK:
// 01-log-metric-filters/metric-filters.ts
new logs.MetricFilter(this, "CostCapFilter", {
logGroup: intelLogGroup,
filterPattern: logs.FilterPattern.literal('"monthly_cost_cap_reached"'),
metricNamespace: "myapp/Intelligence",
metricName: "MonthlyCostCapReached",
metricValue: "1",
defaultValue: 0,
});
// ...la misma forma para "bedrock_throttled" -> BedrockThrottled
// ...y "agent_failed" -> AgentFailed
El patrón del filtro es nada más el nombre del evento entre comillas.
structlog lo renderiza en la línea; CloudWatch hace match de la subcadena.
Este es el patrón caballito de batalla, la mayoría de nuestras alarmas se derivan de logs.
Lo que los filtros de métrica no pueden hacer: dimensiones.
AgentFailed cuenta las fallas de todos los agentes en un solo número.
No puedes preguntar "¿cuál agente?" sin un filtro por cada nombre de agente y no conoces los nombres de antemano. Para rebanar por agente y por modelo necesitas una herramienta distinta.
Fase 2: dimensiones por agente con EMF (no PutMetricData)
El movimiento obvio es cloudwatch.put_metric_data(...) con Dimensions. No lo hicimos. Tres razones:
- Es una llamada de red sincrónica sobre una ruta de petición que ya es lenta (Bedrock domina, pero agregar 30-80ms para emitir métricas es una tontería).
- Necesita IAM (
cloudwatch:PutMetricData) y manejo de errores para cuando el mismo CloudWatch está bajo throttling. - Es otro modo de falla en la cosa cuyo trabajo es observar modos de falla.
La alternativa es EMF, Embedded Metric Format. Escribes una línea JSON con una forma especial a stdout. CloudWatch Logs la reconoce y extrae métricas con dimensiones, de manera automática. La tarea ya tiene logs:PutLogEvents. Sin llamada a la API, sin IAM, sin latencia.
El helper completo:
# 02-emf-metrics/emf.py
import json, sys, time
def emit_agent_metrics(*, agent_name, model_used, success,
latency_ms, input_tokens, output_tokens, cost_usd):
doc = {
"_aws": {
"Timestamp": int(time.time() * 1000),
"CloudWatchMetrics": [{
"Namespace": "myapp/Agents",
# [] = agregado; conjuntos nombrados = por-agente / por-agente-modelo
"Dimensions": [[], ["AgentName"], ["AgentName", "ModelUsed"]],
"Metrics": [
{"Name": "Invocations", "Unit": "Count"},
{"Name": "Errors", "Unit": "Count"},
{"Name": "LatencyMs", "Unit": "Milliseconds"},
{"Name": "InputTokens", "Unit": "Count"},
{"Name": "OutputTokens", "Unit": "Count"},
{"Name": "CostUsd", "Unit": "None"},
],
}],
},
"AgentName": agent_name,
"ModelUsed": model_used or "unknown",
"Invocations": 1,
"Errors": 0 if success else 1,
"LatencyMs": latency_ms,
"InputTokens": input_tokens,
"OutputTokens": output_tokens,
"CostUsd": round(cost_usd, 6),
}
sys.stdout.write(json.dumps(doc) + "\n")
sys.stdout.flush()
Cada valor (LatencyMs, CostUsd, …) coexiste con los campos de
dimensión (AgentName, ModelUsed) en el mismo objeto JSON. El arreglo Dimensions lista qué conjuntos de dimensiones materializar: [] te da un agregado (un solo número a través de todos los agentes, bueno para una
alarma global), ["AgentName"] te da por agente, ["AgentName","ModelUsed"] te da por agente-por-modelo. CloudWatch crea los tres desde una sola línea.
El sitio de la llamada es el envoltorio de agente que ya existe, un solo lugar, cada agente lo hereda:
# 02-emf-metrics/execute_hook.py
await self.log_usage(context, result) # la fila de base de datos existente
emit_agent_metrics( # nuevo: la línea EMF
agent_name=self.name,
model_used=result.model_used,
success=result.success,
latency_ms=result.latency_ms,
input_tokens=result.input_tokens,
output_tokens=result.output_tokens,
cost_usd=result.cost_usd,
)
Ahora myapp/Agents tiene CostUsd, LatencyMs, Invocations,
Errors, tokens, cada uno rebanable por agente y modelo, consultable en la consola, con alarma, y con cero infraestructura nueva.
Fase 3: las alarmas que importan para una carga de IA
Las alarmas de infra genéricas (CPU, 5xx, conteo de tareas) ya las tienes.
Estas son las específicas de IA, y los umbrales son la parte interesante.
// 03-alarms/ai-alarms.ts (formas; el archivo completo está en la carpeta)
// Todos los agentes caídos,presupuesto agotado. Una ocurrencia avisa.
new cw.Alarm(this, "CostCapReached", {
metric: costCapMetric, threshold: 1, evaluationPeriods: 1,
// ...GREATER_THAN_OR_EQUAL_TO_THRESHOLD
});
// Tormenta de throttling, un solo throttle se reintenta solo y está bien.
new cw.Alarm(this, "BedrockThrottling", {
metric: throttleMetric, threshold: 10, // >=10 en 5 min
});
// Latencia sistémica, p95 agregado (sin dimensión) a través de todos los agentes.
new cw.Alarm(this, "AgentLatencyP95", {
metric: new cw.Metric({
namespace: "myapp/Agents", metricName: "LatencyMs", statistic: "p95",
}),
threshold: 30_000, // 30s de spinner
});
Dos lecciones de umbrales que aprendimos a la mala en otra parte del mismo codebase:
-
Una sola ocurrencia vs. ráfaga. "Presupuesto agotado" avisa al primer evento, es binario y total. "Throttling" no debe: un throttle es normal y se reintenta. Alarma sobre la ráfaga (
>=10 en 5 min), o te entrenas a ti mismo a ignorar la alarma. -
El p95 a través de agentes heterogéneos es burdo. Un agente de
búsqueda contesta en 300ms; un agente de análisis de documentos tarda 20s. Un p95 global solo atrapa la lentitud sistémica, que es justo lo que quieres para una sola alarma cúbrelo todo, pero para afinar un agente específico, usa la dimensión
["AgentName"], no el agregado.
Fase 4: el dashboard ahora es casi gratis
Con las métricas ya existiendo, un dashboard son unas cuantas líneas de CDK, costo por agente, latencia p95, invocaciones, tasa de error. El punto del dashboard no es la alarma (la alarma te avisa); es la pregunta post-incidente "¿fue un agente o fueron todos?", contestada de un solo vistazo en lugar de una sesión de SQL.
Trampas
-
Las líneas EMF tienen que ser JSON crudo en su propia línea. Si tu logger antepone un prefijo de timestamp/nivel, EMF no parsea. Mandamos el JSON directo a
sys.stdout, brincándonos structlog, para que la línea EMF quede limpia y las líneas de structlog no se vean afectadas son eventos de log separados. -
Los health checks van a disparar tu limitador de tasa. El ALB pega a
/health~360×/hora; nuestro limitador global de30/horaconvertía cada sondeo en un 429 y el contenedor se veía no saludable en un ciclo de reinicio de ~30 min. Exenta el endpoint de salud explícitamente. -
Clasifica el throttling por código de error, no por mensaje. Bedrock saca los problemas de capacidad bajo varios códigos de botocore (
ThrottlingException,ServiceQuotaExceededException, …). Haz match delresponse.Error.Code, no de una subcadena del mensaje. -
La telemetría nunca debe romper la llamada.
emit_agent_metricsse traga todo, un bug de métricas no puede tirar un agente.
Lo que saltamos a propósito
-
X-Ray / trazado distribuido. El
request_idya hila backend → inteligencia → Bedrock en los logs. El trazado es trabajo de verdad por una ganancia marginal a nuestra escala; lo revisamos cuando el grafo de llamadas se profundice. -
Métricas por versión de prompt. Capturamos
prompt_version_iden la base de datos para el ciclo de aprendizaje; promoverlo a una dimensión de CloudWatch es cardinalidad que todavía no necesitamos. - Un proveedor de APM. Las métricas derivadas de logs + EMF cubren el monitoreo operativo a $0. La barra para agregar una herramienta de paga es "esta pregunta nos está costando incidentes", y no es así, todavía.
La forma de todo esto
modos de falla (tope de costo, throttle, agente fallido) -> línea de log -> MetricFilter -> alarma
costo / latencia / tokens por agente -> línea EMF -> métrica auto -> dashboard + alarma
servicio caído -> ECS RunningTaskCount -> alarma
Tres mecanismos, un principio: emite una línea, deja que CloudWatch la convierta en una métrica. Ningún código de agente llama a una API de métricas de AWS; nada en la ruta de la petición espera por la telemetría; la cuenta mensual de todo esto es cero.
Top comments (0)