맹목적으로 비행하는 것을 멈추기: AI 게이트웨이에 OpenTelemetry 트레이싱 추가하기
요약
본 문서는 LLM 기반 애플리케이션의 관측 가능성 문제를 다루며, AI 게이트웨이에 OpenTelemetry (OTel) 트레이싱을 추가하는 방법을 안내합니다. 표준 APM 도구로는 놓치기 쉬운 의미론적 속성(프롬프트, 토큰 수 등)을 포착하여, LLM 워크플로우의 지연 원인과 문제점을 정확히 진단할 수 있게 합니다.
핵심 포인트
- AI 게이트웨이는 애플리케이션 코드와 여러 모델 제공업체 사이에 위치하는 핵심 컴포넌트입니다.
- 표준 트레이싱은 HTTP 요청 시간만 측정하며, LLM의 의미론적 속성(토큰, 프롬프트)을 놓칩니다.
- OpenTelemetry를 사용하여 RAG 단계부터 최종 생성까지 전체 워크플로우를 계층적으로 추적할 수 있습니다.
- Python 구현 시 비동기 환경에서 컨텍스트 전파와 PII 마스킹 처리가 중요합니다.
비전 없는 운영을 멈추세요: AI 게이트웨이에 OpenTelemetry 트레이싱 추가하기
AI Gateway를 구축하는 것은 실험적인 LLM 플레이그라운지에서 프로덕션급 애플리케이션으로 전환하는 조직에게 표준 관행이 되었습니다. 이 게이트웨이는 애플리케이션 코드와 여러 모델 제공업체(OpenAI, Anthropic, 로컬 LLM) 사이에 위치하여 라우팅, 캐싱, 속도 제한(rate limiting), 비용 제어 등을 처리합니다. 그러나 깊은 수준의 관측 가능성(observability) 없이는 팀이 본질적으로 비전 없이 날고 있는 것과 같습니다. 사용자가 채팅 응답이 "느리다"거나 "이상하다"고 불평할 때, 네트워크 시간 초과인지, 특정 모델의 환각 현상(hallucination) 때문인지, 아니면 중복되고 비용이 많이 드는 추론을 유발한 캐시 미스인지를 구별할 수 있습니까?
표준 APM 도구들은 AI 워크로드에 어려움을 겪습니다. 왜냐하면 대규모 언어 모델(LLM) 상호 작용에 대한 의미적 이해가 부족하기 때문입니다. 이들은 HTTP 요청만 볼 뿐, 중요한 의미적 "스팬(span)"—실제 전송된 프롬프트, 소비된 토큰 수, 종료 이유(finish reason), 그리고 검색 증강 생성(RAG) 단계와 최종 생성 간의 의미적 관계—을 놓칩니다. 본 튜토리얼은 AI 게이트웨이를 위한 강력한 OpenTelemetry (OTel) 트레이싱 파이프라인 구현 과정을 안내합니다. 우리는 의미론적 속성 규칙(semantic attribute conventions), 비동기 경계를 가로지르는 컨텍스트 전파(context propagation), 그리고 이 데이터를 Jaeger나 Grafana Tempo 같은 백엔드로 내보내는 방법을 다룰 것입니다.
목차
-
- 표준 트레이싱이 AI 게이트웨이에 실패하는 이유
-
- 아키텍처: AI 게이트웨이 관측 가능성 파이프라인
-
- 의미론적 규칙: 기록해야 할 것들
-
- 구현: Python으로 게이트웨이 계측(Instrumenting)하기
-
- 비동기 Python에서 컨텍스트 전파 처리하기
-
- PII 및 민감 데이터 마스킹(Redacting)하기
-
- 데이터 시각화: Jaeger와 Grafana
-
- 자주 묻는 질문
1. 표준 트레이싱이 AI 게이트웨이에 실패하는 이유
기존의 분산 트레이싱(distributed tracing)은 지연 시간(latency)이 '어디서' 발생하는지에 초점을 맞춥니다 (DB 쿼리, API 호출). 하지만 AI 시스템에서는 종종 모델이 특정 방식으로 동작한 '이유'가 더 중요합니다. 일반적인 HTTP 트레이스는 /api/chat 요청에 3.2초가 걸렸다고 알려줄 뿐입니다. 이 중 2.5초가 토큰을 생성하는 LLM 추론 엔진(LLM inference engine)에서 소요되었는지, 또는 프롬프트 길이가 4,000 토큰으로 길어 비용이 높았는지는 알려주지 않습니다.
게다가 AI 워크플로우는 종종 여러 단계로 이루어져 있습니다. 일반적인 RAG (검색 증강 생성, Retrieval-Augmented Generation) 흐름은 다음과 같습니다:
- 사용자 쿼리를 임베딩(Embed)합니다.
- 벡터 데이터베이스를 검색합니다.
- 결과를 재순위화(Rerank)합니다.
- 컨텍스트와 쿼리를 LLM에 전송합니다.
이러한 작업들을 단일 트레이스로 연결하지 않으면, '벡터 DB가 관련 없는 결과를 반환하여 LLM이 환각(hallucinate)을 일으키게 했다'와 같은 문제를 진단할 수 없습니다. OpenTelemetry는 이러한 작업을 중첩(nest)할 수 있는 기본 단위인 Span을 제공하며, 이를 통해 요청 라이프사이클의 계층적 뷰를 생성할 수 있습니다.
2. 아키텍처: AI 게이트웨이 관측 가능성 파이프라인
코드를 작성하기 전에, 우리는 아키텍처를 정의해야 합니다. AI 게이트웨이는 일반적으로 리버스 프록시(reverse proxy) 또는 SDK 래퍼(SDK wrapper) 역할을 수행합니다. 이 튜토리얼에서는 요청을 받아 LLM 제공업체로 전달하는 Python 기반의 게이트웨이 서비스를 가정할 것입니다.
데이터 흐름:
- Ingestion: 게이트웨이가 들어오는 HTTP 요청을 수신합니다.
- Root Span: 게이트웨이는 전체 요청을 나타내는 루트 스팬(root span)을 생성합니다.
- Child Spans: 게이트웨이가 요청을 처리하는 동안, 특정 작업을 위해 자식 스팬(child spans)을 생성합니다:
retrieval.search: 벡터 DB에서 컨텍스트를 가져오는 경우.llm.inference: 모델 제공업체에 대한 실제 호출을 위한 경우.llm.generate: 청크 스트리밍이 필요한 경우 (해당하는 경우).
- Enrichment: 각 스팬은 OpenTelemetry GenAI Semantic Conventions에서 정의한 속성(attribute)으로 풍부하게 만듭니다 (예:
gen_ai.model.name,gen_ai.usage.prompt_tokens). - Export: 생성된 스팬들은 배치 처리되어 OTLP를 통해 컬렉터로 내보내집니다.
- Storage & Visualization: 컬렉터는 데이터를 Jaeger/Tempo로 라우팅하여 확인합니다.
*참고: 근본적인 컬렉터 아키텍처에 대해 더 깊이 파고들려면, 고처리량 텔레메트리 파이프라인에 대한 Tamiz's Insights의 기술 분석을 참고할 수 있습니다.
3. Semantic Conventions: 무엇을 기록할 것인가
추적(traces)을 표준 도구에서 읽기 쉽게 만들려면, 새롭게 등장하는 OpenTelemetry GenAI Semantic Conventions를 준수해야 합니다. 현재는 아직 진화 중이지만, 지금 일관된 속성 세트를 정의하면 나중에 계측(instrumentation) 코드를 다시 작성할 필요가 없어집니다.
기록해야 할 주요 속성:
| Attribute Key | Description | Example Value |
|---|---|---|
gen_ai.system | 벤더 또는 시스템 이름 | openai, anthropic, local |
| ... | ||
| 중요: 엄격한 PII(개인 식별 정보) 제거 파이프라인이 없는 한, 스팬에 원시 프롬프트/완성 내용을 기록하지 마십시오. 스팬은 메트릭보다 낮은 비율로 샘플링되는 경우가 많지만, 여전히 로깅됩니다. 이에 대해서는 섹션 6에서 다루겠습니다. |
4. Implementation: Python으로 게이트웨이 계측하기
FastAPI와 httpx를 사용하여 최소한의 AI 게이트웨이를 구축할 것입니다. 우리는 opentelemetry-sdk와 opentelemetry-exporter-otlp를 사용하여 이를 계측할 것입니다.
Prerequisites
Python 3.10 이상이 설치되어 있는지 확인하세요. 가상 환경을 생성하고 종속성을 설치합니다:
pip install fastapi uvicorn httpx opentelemetry-sdk opentelemetry-exporter-otlp-proto-http opentelemetry-instrumentation-fastapi opentelemetry-instrumentation-httpx
4.1 Tracer Provider 설정하기
먼저 OpenTelemetry SDK를 초기화해야 합니다. 이렇게 하면 스팬(span)이 생성되고 내보내질 환경이 설정됩니다.
# app/telemetry.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
...
4.2 게이트웨이 로직 및 계측(Instrumentation)
여기가 AI 게이트웨이의 핵심입니다. 가상의 LLM 제공업체를 감싸는 서비스를 만들 것입니다. "검색(Retrieval)" 단계와 "생성(Generation)" 단계에 중첩된 스팬을 만드는 방식을 주목하세요.
# app/gateway.py
import asyncio
from fastapi import FastAPI, HTTPException
...
핵심 요약: tracer.start_as_current_span 사용에 주목하세요. 이 컨텍스트 매니저(context manager)는 부모-자식 관계를 자동으로 처리합니다. vector_db.search와 llm.generate 스팬은 gateway.handle_chat 내부에 자동으로 중첩됩니다.
5. 비동기 Python에서 컨텍스트 전파 처리하기
Python AI 게이트웨이에 계측을 적용할 때 흔히 발생하는 함정은 **비동기 코드에서의 컨텍스트 손실(context loss)**입니다. 새로운 asyncio.Task나 스레드를 생성하면, OpenTelemetry 컨텍스트(현재 트레이스 ID를 보유함)는 올바르게 처리되지 않는 한 자동으로 전파되지 않습니다.
위 예제에서는 모든 것을 동일한 태스크 내에서 await 했기 때문에 전파가 작동했습니다. 하지만 게이트웨이가 concurrent.futures나 별도의 워커 프로세스를 사용하는 경우, SpanContext를 명시적으로 캡처하여 자식 환경에 주입해야 합니다.
다음은 httpx 요청(많은 게이트웨이가 LLM 제공업체 호출을 위해 사용하는 방식)으로 컨텍스트를 전파하는 방법입니다. opentelemetry-instrumentation-httpx 패키지가 이 작업을 대부분 처리하지만, traceparent 헤더가 전송되도록 확실히 해야 합니다.
import httpx
from opentelemetry.trace import get_current_span, SpanKind
from opentelemetry.trace.context import Context
...
헤더를 주입함으로써, LLM 제공업체도 OpenTelemetry를 지원하거나 (또는 마이크로서비스 전반에 걸쳐 트레이싱하는 경우) 트레이스가 연속적으로 유지되도록 보장합니다. OTel을 지원하지 않는 로컬 LLM의 경우, 최소한 클라이언트 측 지속 시간과 상태가 기록됩니다.
6. PII 및 민감 데이터 마스킹(Redacting)
이것은 AI 트레이싱에서 가장 중요한 보안 측면입니다. LLM 프롬프트에는 종종 사용자의 PII (개인 식별 정보), API 키 또는 독점 데이터가 포함됩니다. 절대로 마스킹 계층 없이 원본 프롬프트/완성 내용을 스팬에 기록하지 마십시오.
우리는 속성을 첨부하기 전에 이를 정리(sanitize)하는 미들웨어 또는 유틸리티 함수를 추가할 수 있습니다.
import re
from opentelemetry.trace import Span
...
모범 사례: 전체 프롬프트/완성 내용은 접근 통제가 엄격한 별도의 보안 로그 저장소(CloudWatch 또는 Datadog Logs와 같은)에 보관하십시오. OpenTelemetry 스팬에는 해시 값 또는 참조 ID만 저장합니다. 이렇게 하면 개발자가 안전한 로그에서 ID를 조회하여 디버깅할 수 있으면서도, 트레이스 백엔드는 깨끗하고 규정을 준수하게 유지됩니다.
7. 데이터 시각화: Jaeger 및 Grafana
우리가 수행한 작업의 결과를 보려면 백엔드가 필요합니다. 로컬 개발을 위해 가장 쉬운 방법은 Jaeger를 All-In-One 모드로 실행하는 것입니다.
docker run -p 16686:16686 -p 4317:4317 jaegertracing/all-in-one
브라우저에서 http://localhost:16686에 접속하십시오. 서비스 이름으로 ai-gateway를 검색합니다. 다음과 같은 트레이스 폭포수(waterfall)가 보일 것입니다:
gateway.handle_chat (200ms)
├── vector_db.search (50ms)
└── llm.generate (150ms)
폭포수 해석하기
- 지연 시간 분석(Latency Breakdown): 만약
llm.generate가 전체 시간의 80%를 차지한다면, 문제는 검색(retrieval)이 아니라 추론(inference)에 있다는 것을 알 수 있습니다. 만약vector_db.search가 느리다면, 인덱싱을 최적화해야 합니다. - 오류 전파(Error Propagation): 만약
llm.generate가 실패한다면 (예: 429 Too Many Requests), 컨텍스트 전파(context propagation) 때문에 루트 스팬인gateway.handle_chat도 오류로 표시됩니다. 이는 Jaeger에서 '모든 오류 요청'을 필터링하기 쉽게 만듭니다. - 비용 상관관계(Cost Correlation):
gen_ai.usage.*속성을 내보냄으로써, Trace ID와 비용(Cost) 간의 상관관계를 Grafana 대시보드에서 구축할 수 있습니다. 예를 들어, 각 Trace ID당 평균 비용을 보여주는 패널을 만들어, 실패하거나 낮은 품질의 결과를 반환할 수 있는 고비용 쿼리를 식별하는 데 도움을 받을 수 있습니다.
8. 자주 묻는 질문(Frequently Asked Questions)
모든 LLM 호출을 추적해야 하나요, 아니면 샘플링된 것만 해야 하나요?
처리량이 높은 시스템에서는 모든 호출을 추적하는 것이 비용이 많이 들 수 있습니다. OpenTelemetry의 BatchSpanProcessor와 샘플링 전략(예: TraceIdRatioBased(0.1))은 트레이스의 10%만 샘플링할 수 있도록 합니다. 하지만 오류 스팬에 대해서는 더 높은 비율로 **헤드 기반 샘플링(Head-Based Sampling)**을 사용해야 합니다. 이렇게 하면 LLM 호출이 실패했을 때 (이는 더 흥미로운 경우입니다), 성공적인 호출이 드롭되더라도 항상 해당 트레이스를 캡처할 수 있습니다.
스트리밍 응답은 어떻게 처리하나요?
스트리밍 응답(SSE)은 completion_tokens가 스트림이 끝날 때까지 알려지지 않기 때문에 어려움을 야기합니다. 스트림이 완료된 후에 스팬 속성을 업데이트할 수 있습니다. async for 루프가 완료된 후 span.set_attribute를 사용하십시오. 전체 스트림 처리가 완료될 때까지 스팬을 최종 확정(end)하지 마십시오. 그렇지 않으면 불완전한 트레이스를 갖게 됩니다.
OpenTelemetry는 Llama 3와 같은 로컬 LLM과 작동하나요?
네, 하지만 클라이언트 라이브러리(예: transformers, ollama, vllm)를 계측해야 합니다. 로컬 LLM은 귀하의 인프라에서 실행되므로, '네트워크' 지연 시간은 CPU/GPU 컴퓨팅 지연 시간으로 대체됩니다. 트레이스 스팬과 함께 GPU 활용도 메트릭을 캡처하기 위해 opentelemetry-instrumentation-nvidia-smi(또는 유사한 도구)를 사용할 수 있습니다. 이를 통해 전체적인 시야를 확보할 수 있습니다: 트레이스는 무엇이 발생했는지 알려주고, 메트릭은 하드웨어가 어떻게 성능을 발휘했는지 알려줍니다.
이러한 관측 가능성(observability) 계층을 구현함으로써, AI 게이트웨이를 '블랙박스'에서 투명하고 디버깅 가능한 시스템으로 변환할 수 있습니다. 이를 통해 성능, 비용, 품질에 대한 복잡한 질문에 답할 수 있는 능력을 얻게 되며, 궁극적으로 프로덕션 환경에서 AI 애플리케이션을 더 안전하고 신뢰성 있게 배포할 수 있도록 합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기