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.












