[Observability] Datadog Alert 분석 SRE OpsAgent 구축(1)
Datadog, SRE, OpsAgent
Stan Cloud
---
Datadog 모니터가 울릴 때마다 대시보드 열고, 메트릭 확인하고, 로그 뒤지고, Slack에 상황 정리해서 공유하고. 이 과정을 하루에 몇 번씩 반복하다 보면 자연스럽게 드는 생각이 있다.
"이거 자동화하면 안 되나?"
그래서 만들었다. 알람이 오면 LLM이 Datadog 도구를 직접 호출해서 메트릭과 로그를 확인하고, 원인 가설과 조치 권고를 Slack에 보내주는 에이전트.
이름은 SRE-OpsAgent.
1. 문제 상황
SRE/인프라 운영을 해본 사람이면 공감할 텐데, Datadog 알람 대응은 대부분 이런 흐름이다.
1. Slack에 알람 노티가 뜬다
2. Datadog 대시보드로 이동해서 해당 메트릭 확인
3. 관련 로그를 검색해서 에러 패턴 파악
4. 최근 배포나 변경 이벤트가 있었는지 확인
5. 원인을 추정해서 Slack에 상황 공유
6. 필요하면 후속 조치
2. Datadog 대시보드로 이동해서 해당 메트릭 확인
3. 관련 로그를 검색해서 에러 패턴 파악
4. 최근 배포나 변경 이벤트가 있었는지 확인
5. 원인을 추정해서 Slack에 상황 공유
6. 필요하면 후속 조치
낮 시간이면 그나마 괜찮은데, 새벽 3시에 알람이 오면 얘기가 달라진다. 잠결에 노트북 열어서 같은 작업을 반복하는 건 체력적으로도, 정확도 면에서도 좋지 않다.
그리고 이 과정에서 사람이 실제로 판단하는 구간은 5번뿐이다. 1~4번은 정보 수집이고, 6번은 판단 이후의 실행이다. 정보 수집 단계를 자동화할 수 있으면 대응 속도가 확 줄어든다.
2. 해결 방향
목표는 단순하다.
알람이 오면 → 자동으로 관련 메트릭/로그를 조회하고 → 원인 가설을 정리해서 → Slack에 보내준다.
사람은 Slack에 올라온 분석 리포트를 보고, 실제 조치가 필요한지만 판단하면 된다.
전체 아키텍처는 이렇다.
Datadog 모니터 ──(webhook)──▶ Cloud Run (FastAPI)
│
▼
OpenAI Agents SDK 루프
├─ LLM: Gemini (OpenAI 호환)
└─ 도구: Datadog MCP (metrics/logs/...)
│
▼
Slack 리포트
Datadog 모니터가 트리거되면 웹훅으로 Cloud Run에 알람 페이로드를 보낸다. Cloud Run 위에 올라간 FastAPI 서버가 이걸 받아서 에이전트를 실행하고, 분석 결과를 Slack에 전송한다.
모델을 따로 학습시킨 건 아니다. Gemini API를 LLM으로 연결하고, Datadog MCP 서버를 도구로 붙여서, 에이전트 루프를 돌리는 구조다.
3. 기술 선택과 이유
왜 Gemini인가
LLM 선택지는 여러 개가 있었다. GPT-4o, Claude, Gemini 등.
Gemini를 선택한 이유는 두 가지다.
• 비용 — Gemini Flash 모델은 입출력 토큰 단가가 GPT-4o 대비 상당히 저렴하다. 알람마다 메트릭/로그를 여러 번 조회하면 컨텍스트가 커지는데, 이때 비용 차이가 누적된다.
• 속도 — Flash 모델은 응답 속도가 빠르다. SRE 알람 대응에서 분석 결과가 30초 만에 오는 것과 2분 걸리는 건 체감이 다르다.
• 속도 — Flash 모델은 응답 속도가 빠르다. SRE 알람 대응에서 분석 결과가 30초 만에 오는 것과 2분 걸리는 건 체감이 다르다.
그리고 Google이 Gemini에 OpenAI 호환 엔드포인트를 제공한다. 이게 다음 선택과 연결된다.
왜 OpenAI Agents SDK인가
이 프로젝트의 핵심은 LLM이 도구를 반복 호출하는 에이전트 루프다.
관찰 → 판단 → 도구 호출 → 결과 확인 → 다시 판단 → ... → 최종 결론
이 루프를 직접 구현하면 코드가 상당히 늘어난다. 도구 호출 파싱, 에러 핸들링, 재시도, 턴 관리 등을 다 처리해야 한다.
OpenAI Agents SDK는 이걸 Runner.run() 한 줄로 처리해준다.
result = await Runner.run(
agent,
input=f"다음 알람을 조사하라:\n\n{alert_context}",
max_turns=settings.max_turns,
)
문제는 이 SDK가 OpenAI API 형식을 기대한다는 점인데, Gemini가 OpenAI 호환 엔드포인트를 제공하니까 그대로 연결이 된다.
client = AsyncOpenAI(
base_url="https://generativelanguage.googleapis.com/v1beta/openai/",
api_key=settings.gemini_api_key,
)
model = OpenAIChatCompletionsModel(
model=settings.gemini_model, # gemini-2.5-flash
openai_client=client,
)
그래서 실제 추론은 Gemini가 하고, 에이전트 루프 관리는 OpenAI Agents SDK가 하는 구조가 됐다. 대안으로 LangChain이나 Google ADK도 있지만, 이 규모에서는 Agents SDK가 가장 가볍다.
왜 MCP 클라이언트를 직접 구현했나
Datadog이 MCP(Model Context Protocol) 서버를 제공한다. 메트릭 조회, 로그 검색, 모니터 상세 같은 API를 MCP 표준 인터페이스로 쓸 수 있다.
OpenAI Agents SDK에도 MCP 클라이언트가 내장되어 있는데, 이게 SSE(Server-Sent Events) 기반이다. 연결을 맺고 서버가 이벤트를 스트리밍으로 보내주는 방식.
문제는 Datadog MCP 서버가 GET SSE를 지원하지 않는다는 점이다. SDK의 MCP 클라이언트로 연결하면 anyio cancel scope 에러가 발생한다.
그래서 SSE 없이 POST only + JSON 응답 방식으로 동작하는 경량 클라이언트를 직접 구현했다.
class SimpleMCPClient:
"""MCP Streamable HTTP 클라이언트 (POST only, no SSE)."""
async def _send(self, method, params=None):
"""JSON-RPC request → response."""
msg = {
"jsonrpc": "2.0",
"method": method,
"id": self._next_id(),
}
if params is not None:
msg["params"] = params
resp = await self._client.post(
self.url, json=msg, headers=self._build_headers()
)
return resp.json()
MCP 프로토콜 자체는 JSON-RPC 2.0이라 구현이 복잡하지 않다. initialize로 세션 시작, tools/list로 도구 목록 조회, tools/call로 도구 실행. 이 세 가지면 된다.
전체 코드가 약 100줄이고, 외부 의존성은 httpx 하나뿐이다.
4. 전체 흐름
알람이 들어왔을 때 코드 실행 순서를 따라가 보자.
① 알람 수신 (main.py)
Datadog 모니터가 트리거되면 웹훅으로 JSON 페이로드가 날아온다.
# 입구 보안: 공유 시크릿으로 검증
if x_dd_secret != settings.webhook_secret:
raise HTTPException(status_code=401, detail="invalid secret")
payload = await request.json()
X-DD-Secret 헤더를 확인해서 정당한 요청인지 검증한 후, 페이로드를 파싱한다.
② 알람 사전 파싱 (agent.py)
Datadog이 보내는 JSON을 LLM에 그대로 넘기면 JSON 구조를 파악하는 데 턴을 쓴다. 그래서 코드에서 미리 핵심 정보를 추출한다.
# Before: LLM이 받는 입력
{"alert_title":"...","alert_type":"...","tags":"service:checkout,env:prod",...}
# After: 사전 파싱 후 LLM이 받는 입력
[알람 정보]
제목: checkout-service error rate 급증
서비스: checkout-service (prod)
메트릭: trace.http.request.errors (현재 12.4% / 임계값 5%)
발생 시각: 2026-06-04 09:15 KST
조회 권장 시간대: 09:00~09:15 (from="-15m")
서비스명, 환경, 메트릭, 시간범위까지 정리해서 넘기면 LLM이 바로 도구 호출로 들어갈 수 있다.
③ MCP 도구 로드
Datadog MCP 서버에 연결해서 사용 가능한 도구 목록을 받아온다. 전체 25개 중 SRE 분석에 필요한 6개만 필터링한다.
CORE_TOOLS = {
"get_datadog_metric", # 메트릭 추이
"search_datadog_logs", # 로그 검색
"search_datadog_events", # 배포/변경 이벤트
"search_datadog_monitors", # 모니터 상세
"search_datadog_services", # 서비스 정보
"get_datadog_trace", # 트레이스 상세
}
도구를 줄이는 이유는 간단하다. LLM에게 도구가 많으면 선택에 턴을 쓴다. 꼭 필요한 것만 주면 바로 본론으로 들어간다.
④ 에이전트 루프 실행
프롬프트에 Few-shot 예시를 포함시켜서, LLM이 어떤 순서로 도구를 호출하고 결과를 정리해야 하는지 보여준다.
# 프롬프트에 포함되는 예시 (발췌)
1턴 (병렬 호출):
- get_datadog_metric(query="avg:trace.http.request.errors{...}", from="-15m")
- search_datadog_logs(query="service:checkout-service status:error", from="-15m")
결과 확인 후 출력:
■ [요약] checkout-service OOM으로 5xx 급증
■ [원인 가설]
- 가설 1 (확신도: 상): Pod 메모리 부족 → OOM Kill
■ [조치 권고]
1. Pod 메모리 limit 증설 (512Mi → 1Gi 권장)
이 예시가 있으면 LLM이 1~2턴 안에 필요한 도구를 병렬 호출하고, 정해진 형식으로 결과를 정리한다. 예시가 없으면 턴이 3~5회로 늘어나거나 출력 형식이 들쭉날쭉해진다.
⑤ Slack 전송 (notify.py)
분석이 끝나면 Slack Bot API(chat.postMessage)로 결과를 보낸다.
payload = {
"channel": channel,
"blocks": [
{"type": "section", "text": {"type": "mrkdwn", "text": header}},
{"type": "divider"},
{"type": "section", "text": {"type": "mrkdwn", "text": text[:3000]}},
],
}
알람의 카테고리 태그(category:infra-error 등)에 따라 채널이 자동 결정된다.
정리
전체를 정리하면 이렇다.
| 단계 | 담당 | 하는 일 |
|---|---|---|
| 알람 수신 | main.py | Datadog 웹훅 수신, 시크릿 검증 |
| 사전 파싱 | agent.py | JSON → 정리된 텍스트로 변환 |
| 도구 연결 | mcp_client.py | Datadog MCP 세션 초기화, 도구 로드 |
| 분석 실행 | agent.py | Gemini + 도구 호출 루프 |
| 결과 전송 | notify.py | Slack Block Kit 메시지 발송 |
다음 글에서는 각 모듈의 구현 디테일(프롬프트 엔지니어링, MCP 세션 관리, 알람 라우팅 로직 등)을 다룰 예정이다.
댓글 쓰기
댓글 쓰기