Skip to content

feat(observability): LLM 호출·도구 실행 OpenTelemetry span - #162

Merged
CocoRoF merged 3 commits into
PlateerLab:mainfrom
mumberrymountain:feat/otel-call-spans
Oct 15, 2026
Merged

CocoRoF merged 3 commits into
PlateerLab:mainfrom
mumberrymountain:feat/otel-call-spans

Conversation

@mumberrymountain

Copy link
Copy Markdown
Contributor

변경

  • observability/ 패키지 추가 — spans(span 수명) / attributes(속성 키) / genai(계측)
  • APIStage._traced_call: Stage 6 호출 1건(재시도 포함) → chat {model}
  • RegistryRouter.route: 도구 실행 1건 → execute_tool {tool}
    (모든 실행기와 CLI 도구 표면이 지나는 한 곳. 실제 라우팅은 _route 로 옮겼고 시그니처·반환값은 그대로)
  • 4.91.0, CHANGELOG

공개 API 변경은 없다(새 모듈과 내부 헬퍼만 추가).

동작 규칙

  • span 은 호출 시점의 현재 context 의 자식이다. 호스트가 턴 span 을 현재 context 로 두고
    run_turn/stream_turn 을 부르면 그 아래에 달리고, 안의 HTTP·MCP 호출은 각 span 아래에 중첩된다.
  • 토큰(gen_ai.usage.*)은 호출 span 에만 둔다. 호스트는 spans_enabled() 가 참이면 자기 쪽
    도구 span 과 턴 span 의 토큰을 끈다 (중복 span · 이중 합산 방지).
  • 프롬프트·응답·도구 입출력 본문은 남기지 않는다(길이·개수만). 취소는 오류로 표시하지 않는다.
  • opentelemetry-api 는 의존성으로 선언하지 않는다. 설치된 환경(xgen-workflow)에서만 켜지고,
    없거나 XGEN_AGENT_RUNTIME_OTEL=0 이면 no-op 이다. span 조작 실패는 실행을 깨지 않는다.
  • 비용: 꺼져 있으면 호출·도구당 추가 비용이 사실상 없다. 켜지면 호출 1건당 span 이 chat 1개,
    도구 1건당 1개 늘어난다.

테스트

  • tests/test_otel_spans.py 6건 — chat/execute_tool 이 호스트 턴 아래에 달림, HTTP span 이 각 호출
    아래에 중첩, 실패 도구의 오류 표시와 본문 미기록, 스위치 off, tracer 고장 시에도 턴 유지
  • opentelemetry-sdk 가 없는 환경에서는 이 테스트가 skip 된다 [dev extra 에 sdk 를 추가했다면 삭제]
  • Windows 로컬에서 변경 전후 tests/unit 실패 목록 비교: 동일 (셸·서브프로세스를 실행하는 기존 실패)
  • ruff check, ruff format --check, gen_event_docs --check 통과. CI 결과는 이 PR 에서 확인
  • 실제 Tempo trace 는 워크플로우 쪽 변경과 함께 배포한 뒤 확인 필요

함께 필요한 변경

xgen-workflow fix/geny-otel-trace [MR 링크로 교체]: 턴을 노드 실행 시작에서 열고 턴 context 를
전파한다. 이 PR 이 v4.91.0 으로 릴리스되면 워크플로우의 런타임 핀(pyproject.toml,
requirements.lock)을 올린다. 워크플로우 변경은 옛 런타임에서도 기존 방식으로 동작한다.

## 배경
dev Tempo 의 Geny 턴 trace 에 `chat` span 이 하나도 없었다. LLM 의
`POST /v1/chat/completions` 5건과 도구가 부른 HTTP·MCP 호출이 에이전트 턴이 아니라
워크플로우 span 바로 아래에 흩어져, 어느 호출이 병목인지 trace 에서 읽을 수 없었다.
호스트는 턴 끝의 사용량 합계와 도구 사건만 받으므로 호출 구간도 부모 관계도 되살릴 수 없다.
그래서 호출과 실행이 실제로 일어나는 런타임이 span 을 연다.

## 변경
- `observability/` 패키지 추가 (`spans` 수명 / `attributes` 키 / `genai` 계측)
- `APIStage._traced_call`: Stage 6 호출 1건(재시도 포함) → `chat {model}`
- `RegistryRouter.route`: 도구 실행 1건 → `execute_tool {tool}` (모든 실행기와 CLI 도구 표면이 지나는 한 곳)
- 4.91.0, CHANGELOG

## 동작 규칙
- span 은 호출 시점의 현재 context 의 자식이다. 호스트가 턴 span 을 현재 context 로
  두고 `run_turn`/`stream_turn` 을 부르면 그 아래에 달린다.
- 토큰(`gen_ai.usage.*`)은 호출 span 에만 둔다. 호스트는 `spans_enabled()` 가 참이면 자기 쪽
  도구 span 과 턴 span 의 토큰을 끈다 (중복 span · 이중 합산 방지).
- 프롬프트·응답·도구 입출력 본문은 남기지 않는다. 취소는 오류로 표시하지 않는다.
- `opentelemetry-api` 는 선택 의존성이다. 없거나 `XGEN_AGENT_RUNTIME_OTEL=0` 이면 no-op,
  span 조작 실패는 실행을 깨지 않는다.

## 테스트
- `tests/test_otel_spans.py` 6건: chat/execute_tool 이 호스트 턴 아래에 달림, HTTP span 이
  각 호출 아래에 중첩, 실패 도구의 오류 표시와 본문 미기록, 스위치 off, tracer 고장 시 턴 유지
- 단위 테스트 전체: 변경 전후 실패 목록 동일 (Windows 에서 셸·서브프로세스를 실행하는 기존 실패)
- 실제 Tempo trace 는 워크플로우 쪽 변경과 함께 배포한 뒤 확인 필요

## 함께 필요한 변경
xgen-workflow `fix/geny-otel-trace`: 턴을 노드 실행 시작에서 열고 턴 context 를 전파한다.
이 PR 의 릴리스 후 워크플로우의 런타임 핀(`pyproject.toml`, `requirements.lock`)을 v4.91.0 으로 올린다.
워크플로우 변경은 옛 런타임에서도 기존 방식으로 동작한다.
CI 의 `ruff format --check` 를 통과하도록 observability/genai.py 의 긴 `with` 줄과
dict 인자 줄바꿈을 맞춘다. 동작 변경 없음.
test(observability): dev extra 에 opentelemetry-sdk 추가
CI 는 `pip install -e ".[dev]"` 만 하므로 SDK 가 없어 tests/test_otel_spans.py 가
importorskip 으로 통째로 skip 되었다. 테스트 전용으로 dev extra 에만 넣는다.
런타임 의존성은 그대로다.
@CocoRoF
CocoRoF merged commit 6d10d8c into PlateerLab:main Oct 15, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants