본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 21. 14:07

CloudWatch를 사용하여 AI 에이전트 모니터링하기

요약

CloudWatch의 MetricFilter와 EMF를 활용하여 AI 에이전트의 성능과 실패 모드를 효율적으로 모니터링하는 방법을 다룹니다. 로그 라인을 메트릭으로 변환하여 예산 초과나 Bedrock Throttling 같은 특정 오류를 실시간으로 감지하는 가이드를 제공합니다.

핵심 포인트

  • MetricFilter를 사용하여 로그 라인을 저비용 메트릭으로 변환 가능
  • EMF(Embedded Metric Format)를 통해 에이전트/모델별 차원(Dimension) 설정
  • 예산 소진 및 Bedrock Throttling 등 주요 AI 실패 모드 감지 방법
  • 데이터베이스 쿼리 없이 실시간 대시보드 및 알람 구축

작업을 가능하게 한 두 가지 아이디어:

  1. 실패 모드는 이미 로그 라인입니다. 코드가 아닌 MetricFilter를 사용하여 이를 메트릭 (Metrics)으로 변환하세요.
  2. 에이전트별/모델별 차원 (Dimensions)에는 EMF가 필요합니다. PutMetricData가 아니라, JSON 한 줄을 작성하면 CloudWatch가 메트릭을 추출합니다.

시작점: 데이터베이스의 한 행은 텔레메트리 (Telemetry)가 아니다

각 에이전트 호출은 한 행을 기록했습니다:

# 00-starting-point/usage_log.py (축약본)
row = AgentUsageLog(
    agent_name=self.name,        # "summarizer", "classifier", ...
...

데이터는 완전하지만, 새벽 2시에는 무용지물입니다. "지금 summarizer가 느린가?"라는 질문에 답하려면 SSH처럼 데이터베이스에 접속하여 백분위수 (Percentiles) 쿼리를 작성해야 합니다. 알람도 없고, 대시보드 (Dashboard)도 없으며, SQL 없이 슬라이스할 수 있는 차원 (Dimension)도 없습니다. 더 나은 점은, 실제로 경고를 주는 실패 모드들(예산 소진, Bedrock 스로틀링 (Throttling), 서비스 중단)이 별도로 기록되지 않거나 전혀 감시되지 않았다는 것입니다.

두 가지 공백, 두 가지 서로 다른 도구.

1단계: 실패 모드는 이미 로그 라인이다

AWS에서 가장 저렴한 메트릭은 이미 작성하고 있는 로그 라인에서 파생되는 메트릭입니다. CloudWatch Logs의 메트릭 필터 (Metric Filters)는 로그 그룹을 스캔하고 패턴이 일치할 때 메트릭을 증가시킵니다. IAM 없이, API 호출 없이 가능합니다.

우리는 알림을 보낼 가치가 있는 세 가지 특정 AI 실패 모드를 가지고 있었습니다.
두 가지는 이미 기록되고 있었고, 하나는 한 줄의 수정이 필요했습니다.

예산 소진. 월간 비용 상한선이 Bedrock 계정을 보호합니다. 이 상한선이 활성화되면, 모든 에이전트가 1일까지 503 오류를 반환하며, 이는 우리가 겪는 가장 영향력이 큰 AI 중단 상황입니다. 이는 이미 기록되고 있었습니다:

# cost_guard.py, 이미 존재함
logger.error("monthly_cost_cap_reached", spent_usd=spent, cap_usd=cap)
raise HTTPException(status_code=503, detail="...budget reached...")

Bedrock의 Throttling (제한). 이것은 보이지 않는 문제였습니다. 우리의 Bedrock 래퍼(wrapper)는 모든 boto 에러를 잡아 BedrockError라는 재시도 가능한(retryable) 형태로 감싸고, tenacity가 재시도하도록 두었습니다. 올바른 동작이었지만, Throttling 폭풍(부하 급증 또는 계정 모델별 TPS 할당량 초과)이 발생하면 다른 일반적인 깜빡임(flicker)과 똑같이 보였습니다.

Throttling이 다르게 기록되도록 분류기(classifier)를 추가했습니다:

# 01-log-metric-filters/bedrock_throttle.py
_THROTTLE_CODES = {
    "ThrottlingException", "TooManyRequestsException",
...

이제 CDK에서 세 가지 패턴, 세 가지 메트릭(metrics)을 사용합니다:

// 01-log-metric-filters/metric_filters.ts
new logs.MetricFilter(this, "CostCapFilter", {
  logGroup: intelLogGroup,
...

필터 패턴은 단순히 따옴표 사이에 이벤트 이름을 넣는 것입니다.
structlog가 이를 로그 라인에 렌더링하면, CloudWatch가 해당 하위 문자열(substring)을 매칭합니다.
이것은 우리의 주력 패턴(workhorse pattern)이며, 대부분의 알람(alarms)은 로그에서 파생됩니다.

메트릭 필터(Metric Filters)가 할 수 없는 것: 차원(dimensions).
AgentFailed모든 에이전트의 실패를 하나의 숫자로 집계합니다.
각 에이전트 이름별로 필터를 지정하지 않고서는 "어떤 에이전트인가?"라고 물을 수 없으며, 에이전트 이름을 미리 알 수도 없습니다. 에이전트와 모델별로 데이터를 세분화(slice)하려면 다른 도구가 필요합니다.

2단계: EMF를 통한 에이전트별 차원 구현 (PutMetricData 방식 아님)

당연한 선택지는 Dimensions를 포함한 cloudwatch.put_metric_data(...)를 사용하는 것이었습니다. 하지만 우리는 그렇게 하지 않았습니다. 세 가지 이유가 있습니다:

  • 이는 이미 느린 요청 경로 상에서 발생하는 **동기식 네트워크 호출(synchronous network call)**입니다 (Bedrock이 지배적이지만, 메트릭을 발행하기 위해 30-80ms를 추가하는 것은 불합리합니다).
  • IAM (cloudwatch:PutMetricData) 권한이 필요하며, CloudWatch 자체가 Throttling 상태일 때를 위한 에러 핸들링이 필요합니다.
  • 실패 모드를 관찰하는 것이 목적인 시스템에서 또 다른 실패 모드가 될 수 있습니다.

대안은 **EMF (Embedded Metric Format)**입니다. stdout에 특수한 형태의 JSON 한 줄을 작성하면 됩니다. CloudWatch Logs는 이를 인식하여 _차원 (dimensions)_이 포함된 메트릭을 자동으로 추출합니다. 작업에는 이미 logs:PutLogEvents 권한이 있습니다. API 호출도, IAM 설정도, 지연 시간(latency)도 필요 없습니다.

전체 헬퍼(helper):

# 02-emf-metrics/emf.py
import json, sys, time

...

각 값(LatencyMs, CostUsd, …)은 동일한 JSON 객체 내에서 차원(AgentName, ModelUsed) 필드와 공존합니다. Dimensions 배열은 어떤 차원 _집합 (sets)_을 구체화할지 나열합니다: []는 집계(모든 에이전트에 걸친 단일 숫자, 글로벌 알람에 유용)를 제공하고, ["AgentName"]은 에이전트별로, ["AgentName","ModelUsed"]는 에이전트-모델별로 제공합니다. CloudWatch는 단 한 줄로부터 이 세 가지를 모두 생성합니다.

호출 위치는 이미 존재하는 에이전트 래퍼(wrapper)이며, 단 한 곳에 위치하여 모든 에이전트가 이를 상속받습니다:

# 02-emf-metrics/execute_hook.py
await self.log_usage(context, result)     # 기존 데이터베이스 행
emit_agent_metrics(                        # 신규: EMF 라인
...

이제 myapp/Agents에는 CostUsd, LatencyMs, Invocations, Errors, 토큰이 포함되며, 각각 에이전트 및 모델별로 세분화(slice)하여 콘솔에서 조회하고 알람을 설정할 수 있습니다. 이 모든 과정에 새로운 인프라는 전혀 필요하지 않습니다.

3단계: AI 부하를 위해 중요한 알람들

일반적인 인프라 알람(CPU, 5xx, 작업 수)은 이미 가지고 계실 것입니다.
이것들은 AI 특화 알람이며, 임계값(thresholds) 설정이 흥미로운 부분입니다.

// 03-alarms/ai-alarms.ts (형태; 전체 파일은 폴더에 있음)

// 모든 에이전트 다운, 예산 소진. 한 번의 발생으로 알림.
...

동일한 코드베이스의 다른 부분에서 뼈아픈 경험을 통해 배운 임계값에 관한 두 가지 교훈입니다:

  • 단일 발생(Una sola ocurrencia) vs. 버스트(ráfaga). "예산 소진(Presupuesto agotado)"은 첫 번째 이벤트 발생 시 알림을 보내며, 이는 이진적(binary)이고 전체적인 문제입니다. 반면 "스로틀링 (Throttling)"은 그래서는 안 됩니다. 스로틀링은 정상적인 현상이며 재시도(retry)가 이루어집니다. 버스트(예: 5분 내 10회 이상)에 대해 알림을 설정하십시오. 그렇지 않으면 알림을 무시하도록 스스로를 훈련하게 될 것입니다.
  • 이질적인 에이전트들 사이의 p95는 조잡합니다. 검색 에이전트는 300ms 내에 응답하지만, 문서 분석 에이전트는 20초가 걸립니다. 전역 p95는 오직 시스템적(systemic) 느려짐만을 포착하며, 이는 단일 알림으로 모든 것을 커버하고 싶을 때 딱 적합합니다. 하지만 특정 에이전트를 미세 조정하려면 집계된 값이 아닌 ["AgentName"] 차원(dimension)을 사용하십시오.

4단계: 이제 대시보드는 거의 무료입니다

메트릭(metrics)이 이미 존재하므로, 대시보드를 만드는 것은 CDK 몇 줄이면 충분합니다. 에이전트별 비용, p95 지연 시간(latency), 호출 횟수(invocations), 에러율(error rate) 등을 포함할 수 있습니다. 대시보드의 목적은 알림(알림은 당신에게 알려주는 역할입니다)이 아닙니다. 대시보드의 목적은 사고 후 질문인 "특정 에이전트의 문제였는가, 아니면 모두의 문제였는가?"에 대해 SQL 세션을 여는 대신 한눈에 답을 얻는 것입니다.

함정 (Trampas)

  • EMF 라인은 자체적인 한 줄에 가공되지 않은 JSON 형태여야 합니다. 로거(logger)가 타임스탬프나 로그 레벨 접두사를 앞에 붙이면 EMF가 파싱하지 못합니다. EMF 라인이 깔끔하게 유지되고 structlog 라인이 영향을 받지 않도록(structlog 라인은 별도의 로그 이벤트로 처리됨), structlog를 건너뛰고 JSON을 sys.stdout으로 직접 보냅니다.
  • 헬스 체크(health checks)가 속도 제한기(rate limiter)를 트리거할 것입니다. ALB는 /health에 시간당 약 360회 요청을 보냅니다. 우리의 전역 제한인 시간당 30회는 모든 상태 확인(probe)을 429 에러로 만들었고, 컨테이너는 약 30분의 재시작 사이클 내에 비정상(unhealthy) 상태로 표시되었습니다. 헬스 체크 엔드포인트를 명시적으로 제외하십시오.
  • 스로틀링을 메시지가 아닌 에러 코드로 분류하십시오. Bedrock은 다양한 botocore 코드(ThrottlingException, ServiceQuotaExceededException, …)를 통해 용량 문제를 나타냅니다. 메시지의 하위 문자열이 아닌 response.Error.Code를 매칭하십시오.
  • 텔레메트리(telemetry)가 호출을 중단시켜서는 안 됩니다. emit_agent_metrics는 모든 예외를 삼켜야 합니다. 메트릭 버그가 에이전트를 다운시켜서는 안 됩니다.

의도적으로 건너뛴 것들

  • X-Ray / 분산 트레이싱 (Distributed Tracing). request_id가 이미 로그 상에서 백엔드 → 지능 (Intelligence) → Bedrock까지 연결되어 있습니다. 현재 우리의 규모에서 분산 트레이싱을 도입하는 것은 미미한 이득에 비해 실제 작업 공수가 너무 큽니다. 호출 그래프 (Call Graph)가 더 깊어지면 그때 검토하겠습니다.
  • 프롬프트 버전별 메트릭 (Metrics by prompt version). 학습 루프를 위해 데이터베이스에 prompt_version_id를 캡처하고 있습니다. 이를 CloudWatch의 차원 (Dimension)으로 승격시키는 것은 아직 필요하지 않은 카디널리티 (Cardinality)를 발생시킵니다.
  • APM 제공업체. 로그와 EMF (Embedded Metric Format)에서 파생된 메트릭은 $0의 비용으로 운영 모니터링을 충족합니다. 유료 도구를 추가하기 위한 기준은 "이 질문이 인시던트 비용을 발생시키고 있는가"이며, 아직은 그렇지 않습니다.

전체적인 구조

장애 모드 (비용 상한, 스로틀링, 에이전트 실패)  -> 로그 라인 -> MetricFilter -> 알람
에이전트별 비용 / 지연 시간 / 토큰              -> EMF 라인  -> 자동 메트릭 -> 대시보드 + 알람
서비스 다운                                    -> ECS RunningTaskCount -> 알람

세 가지 메커니즘, 하나의 원칙: 로그 라인을 방출하고, CloudWatch가 이를 메트릭으로 변환하게 하십시오. 에이전트 코드 중 그 어떤 것도 AWS 메트릭 API를 직접 호출하지 않습니다. 요청 경로(Request path) 상의 그 무엇도 텔레메트리 (Telemetry)를 기다리며 대기하지 않으며, 이 모든 것에 대한 월간 비용은 0원입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0