
AI PR의 정체가 보이지 않아 OpenTelemetry로 관제 로그를 설계했다
요약
AI 에이전트가 생성하는 PR의 처리 과정을 추적하기 위해 OpenTelemetry를 활용한 관측성(Observability) 설계 방법을 다룹니다. Webhook부터 최종 판단까지의 흐름을 Trace와 Metric으로 구분하여 설계하는 실무적인 가이드를 제공합니다.
핵심 포인트
- AI 에이전트 PR 처리 과정의 병목과 판단 흐름을 추적하기 위한 가관측성 확보
- OpenTelemetry를 사용하여 Webhook 이벤트를 하나의 Trace로 구성
- Trace에는 상세 식별 정보(PR 번호 등)를, Metric에는 집계용 속성만 포함하여 고카디널리티 문제 방지
무슨 일이 일어났는가
AI 에이전트를 병렬로 실행하면 처음에는 기분이 좋습니다.
한 개체는 인증 관련, 다른 한 개체는 API, 또 다른 한 개체는 UI를 담당합니다. 인간이 기다리는 동안 PR이 늘어갑니다. 작업 디렉토리를 나누면 로컬에서는 서로 충돌하지 않습니다. CI도 각 PR에서는 초록색(성공)이 됩니다.
하지만, 한동안 운용하다 보면 다른 방식으로 막히게 됩니다.
PR이 main에 착지하기 직전의 정체가 보이지 않습니다.
GitHub의 webhook은 들어오고 있습니다. GitHub App 측의 로그도 있습니다. PR에는 체크(check)도 코멘트도 달립니다. 그런데 나중에 "왜 이 PR은 Wait가 되었지?", "어느 단계가 느렸지?", "ACK 이후에 판단이 바뀌었나?"를 추적하려고 하면 로그, DB, GitHub 화면을 왔다 갔다 해야 합니다.
이것은 애플리케이션의 에러 모니터링이라기보다, 판단의 흐름에 대한 가관측성 (Observability) 문제였습니다.
그래서 AI 에이전트의 PR 관제를 수행하는 GitHub App에 OpenTelemetry를 추가한다면, 무엇을 trace로 하고 무엇을 metric으로 해야 할지를 정리했습니다. 구현 대상은 Veripsa Core이지만, 사고방식은 "PR에 체크를 내보내는 GitHub App"이라면 대부분 응용할 수 있습니다.
OpenTelemetry로 보고 싶은 것
OpenTelemetry의 trace는 "요청이 애플리케이션 내부를 어떻게 통과했는가"를 보기 위한 것입니다. span은 그 안의 작업 단위입니다.
PR 관제 GitHub App이라면, 하나의 webhook을 하나의 trace로 보는 것이 자연스러웠습니다.
github.webhook.pull_request
├─ github.payload.verify
├─ github.pr.load_context
...
여기서 보고 싶은 것은 단순히 "성공했는가"가 아닙니다.
- 어떤 event로부터 시작되었는가
- 어떤 repo / PR / commit에 대한 처리인가
- 몇 건의 변경 사항을 보았는가
- 최종 판단이
clear
/heads_up
/wait
/unknown
중 어느 것인가 - GitHub의 check run까지 반영되었는가
- 어떤 span이 느린가
이것을 1 trace로 볼 수 있다면, PR 화면에 나타난 "Wait in line"이 어떤 webhook으로부터, 어떤 판단을 거쳐, 어떤 check run이 되었는지를 추적할 수 있게 됩니다.
우선 추가한다면, 이 최소 형태
Python이라면 최소 형태는 상당히 솔직합니다.
from opentelemetry import metrics, trace
tracer = trace.get_tracer("veripsa.github_app")
meter = metrics.get_meter("veripsa.github_app")
...
webhook 입구에서 trace를 생성합니다.
def handle_pull_request(payload: dict) -> None:
repo = payload["repository"]
pr = payload["pull_request"]
...
포인트는 trace attribute와 metric attribute를 분리하는 것입니다.
trace에는 repository_id, pr_number, head_sha를 붙여두면 조사가 쉽습니다. 1건의 처리를 깊게 파고들기 위한 정보이기 때문입니다.
반면 metric에는 PR 번호나 commit SHA를 넣지 않는 것이 좋습니다. metric은 집계하여 대시보드나 알람(alert)에 사용하므로, 고카디널리티 (High Cardinality) 값을 라벨로 사용하면 한꺼번에 다루기 어려워집니다. metric 측은 decision, reason, event 정도로 억제하는 것이 안전했습니다.
"판단"은 error가 아니라 attribute로 한다
처음에 고민했던 것이 wait나 unknown을 span status의 Error로 할 것인가 하는 점입니다.
결론적으로, 하지 않기로 했습니다.
wait는 앱의 실패가 아닙니다. GitHub App이 "이 PR은 지금 바로 착지하지 않는 편이 좋다"라고 정상적으로 판단할 수 있었던 상태입니다. unknown...
적어도 제 설계에서는 "처리에 실패했다"가 아니라 "판단하기에 충분한 재료가 없다"라는 출력입니다.
따라서 span status는 transport / runtime의 실패에 사용하고, 업무상의 판단은 attribute에 몰아줍니다.
span.set_attribute("veripsa.decision", "wait")
span.set_attribute("veripsa.reason", "same_place")
GitHub API가 500을 반환했거나, DB에 접속할 수 없었거나, payload의 서명 검증에 실패한 경우. 그런 상황에서는 span status를 error로 설정합니다.
from opentelemetry.trace import Status, StatusCode
try:
publish_check_run(payload)
...
이렇게 하면 대시보드상의 error rate는 "앱이 고장 난 비율"이 되며, wait의 빈도는 별도의 metric으로 취급할 수 있습니다.
event는 span event로 한다
또 하나 효과적인 방법은 판단의 분기점을 span event로 만드는 것입니다.
attribute는 최종 상태를 설명하는 데 적합합니다. span event는 "그 시점에 무엇이 일어났는가"를 남기는 데 적합합니다.
with tracer.start_as_current_span("veripsa.route.evaluate") as span:
span.add_event(
"route.input_loaded",
...
이를 넣어두면 나중에 trace를 보았을 때 "입력을 읽었다", "판단했다", "GitHub에 반영했다"가 시계열로 나열됩니다.
로그로도 비슷한 일을 할 수 있습니다. 다만, PR 한 건의 처리가 여러 함수와 여러 외부 API에 걸쳐 있다면 trace 쪽이 훨씬 읽기 편합니다. 특히 GitHub webhook은 동일한 repo에서 짧은 시간 동안 여러 건이 들어오기 때문에, trace_id로 한 건씩 묶을 수 있다는 점이 유용합니다.
content-free와 telemetry는 별개로 생각한다
Veripsa Core는 코드 본문을 저장하지 않는 설계로 하고 있습니다. PR 관제에 필요한 최소한의 운영 메타데이터만을 다루기 위해서입니다.
하지만 여기서 한 번 다시 생각해봐야 할 점이 있습니다.
앱 본체가 본문을 저장하지 않더라도, telemetry backend에 무엇을 보낼지는 별개의 문제입니다.
예를 들어 file path는 코드 본문이 아닙니다. 하지만 프라이빗 리포지토리(private repository)에서는 path만으로도 기능명이나 고객명이 드러나는 경우가 있습니다. PR title이나 comment body는 더욱 위험합니다. 편리하다고 해서 trace attribute에 전부 넣다 보면, "본체는 저장하지 않았지만 모니터링 기반에는 흘려보내고 있었다"는 상황이 됩니다.
그래서 telemetry에 실을 값은 다음과 같은 기준으로 정했습니다.
OK:
- installation_id
- repository_id
...
file path가 필요한 경우라도 우선은 path_count나 hash로 대체합니다. 조사를 위해 꼭 필요하다면, trace를 통해 GitHub의 PR 번호로 돌아가 권한이 있는 사람이 GitHub 측에서 확인하도록 합니다.
이 분리는 매우 중요했습니다. OpenTelemetry를 도입하면 나도 모르게 "조사에 편리한 것"을 전부 attribute에 넣고 싶어집니다. 하지만 모니터링 기반은 검색이 쉬운 만큼, 정보가 유출되었을 때의 영향도 큽니다. PR 관제의 telemetry는 처음부터 절제하는 편이 안전합니다.
metric은 "관제의 건강 상태"를 본다
trace는 한 건을 깊게 파기 위한 것. metric은 전체적인 경향을 보기 위한 것.
PR 관제에서는 다음 metric이 유용합니다.
veripsa.route_decisions
attributes:
decision = clear | heads_up | wait | unknown
...
여기서도 repo_id / pr_number / sha는 넣지 않습니다. 조사 대상인 한 건을 보고 싶다면 trace, 경향을 보고 싶다면 metric이라는 역할 분담입니다.
실제로 보고 싶은 대시보드는 예를 들어 다음과 같습니다.
-
unknown이 갑자기 증가하고 있지는 않은가 -
route_eval.duration -
route_eval.duration이 늘어나고 있지는 않은가 -
GitHub API 반영 실패가 늘어나고 있지는 않은가
-
wait가 많은 시간대는 언제인가 -
ACK 이후에 판단이 바뀌는 케이스가 늘어나고 있지는 않은가
이것은 일반적인 "CPU가 높다", "HTTP 500이 많다"와는 조금 다릅니다. AI 에이전트 운용의 트래픽(Traffic)과 관제 판단의 상태를 보는 대시보드(Dashboard)입니다.
PR 코멘트에 trace_id를 노출할 것인가
고민 끝에, PR 코멘트 본문에는 노출하지 않는 방향으로 결정했습니다.
trace_id를 노출하면 PR 화면에서 모니터링 화면으로 쉽게 이동할 수 있습니다. 하지만 사용자에게는 노이즈가 될 수 있습니다. 게다가 백엔드(Backend)를 변경했을 때 링크가 깨질 수도 있습니다.
대신, 체크 런(Check run)의 상세 URL(Details URL)이나 내부 로그에서 trace_id를 조회할 수 있도록 해두는 정도가 적당했습니다. PR 코멘트는 인간과 에이전트가 읽는 곳이므로, 그곳에는 판단 결과만을 출력합니다.
PR comment:
- Clear to land / Heads up / Wait / Unknown
- 무엇을 기다려야 하는가
...
"읽는 사람에게 필요한 정보"와 "운영자가 파헤쳐야 할 정보"를 분리하는 것이 코멘트와 모니터링 모두 깨지지 않고 유지하기에 좋습니다.
설계하며 알게 된 점
처음에는 GitHub App의 로그를 늘리면 해결될 것이라고 생각했습니다.
하지만 PR 관제에서 원했던 것은 로그의 양이 아니라, 하나의 PR 판단이 어떻게 흘러갔는가였습니다.
- Webhook을 받았다
- 입력을 검증했다
- 변경 범위를 읽었다
- 다른 오픈된(Open) PR의 상태를 확인했다
- 판단했다
- 체크 런(Check run)과 코멘트에 반영했다
이 일련의 흐름을 트레이스(Trace)로 만들면, "왜 이런 결과가 나왔는가?"에 대한 조사가 상당히 쉬워집니다.
반대로, 메트릭(Metric)은 압축하는 것이 좋습니다. PR 번호나 커밋 SHA(Commit SHA)를 넣고 싶은 마음은 들지만, 그 부분은 트레이스(Trace)에 맡깁니다. 메트릭은 decision과 reason만으로도 충분히 유용합니다.
요약
AI 에이전트 시대의 관측 가능성(Observability)은 LLM의 토큰(Token) 수나 지연 시간(Latency)만으로는 부족합니다.
에이전트가 코드를 작성하고, PR을 만들고, main 브랜치에 안착시킨다면, 그곳에는 "어떤 순서로 넣을 것인가", "어디서 기다릴 것인가", "무엇을 보고 판단했는가"라는 관제의 레이어(Layer)가 생깁니다.
그 레이어에는 일반적인 애플리케이션 모니터링과는 조금 다른 텔레메트리(Telemetry)가 필요했습니다.
- 1 Webhook = 1 Trace로 구성한다
- 스팬(Span)은 "페이로드(Payload) 검증", "상태 읽기", "판단", "GitHub 반영"으로 나눈다
wait/unknown은 에러(Error)가 아니라 결정 속성(Decision attribute)으로 처리한다- 메트릭(Metric)은
decision/reason으로 집계하며, PR 번호나 SHA는 넣지 않는다 - 본체가 콘텐츠 프리(Content-free)이더라도, 텔레메트리(Telemetry)에 무엇을 보낼지는 별도로 설계한다
OpenTelemetry는 "처리 시간을 보는 것"이라고 생각했지만, 실제로는 판단의 흐름을 나중에 설명할 수 있는 형태로 만드는 것으로서 효과적입니다.
여러 AI 에이전트로 인해 PR이 늘어나면서 "main에 들어가기 직전의 교통 정리"가 보이지 않게 되었다면, 우선 Webhook 1건을 트레이스(Trace)로 만드는 것부터 시작해 보세요. 풍경이 상당히 달라질 것입니다.
참조:
Discussion

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