이 글에서 사용하는 주요 용어 정의
| 용어 | 정의 |
|---|---|
| Trace | 하나의 요청에서 발생한 작업을 연결해 보는 실행 기록입니다. |
| Span | 시작·종료 시각과 속성을 가진 개별 작업 구간입니다. |
| Attribute | span에 붙이는 작업 이름, 결과 개수 등의 키·값 정보입니다. |
| Exporter | 수집한 기록을 콘솔이나 관측 시스템으로 내보내는 구성 요소입니다. |
| Semantic convention | 여러 도구가 같은 의미로 기록을 해석하도록 이름과 값의 용법을 정한 규약입니다. |
요청 전체 시간만으로 느린 원인을 알 수 있을까요?
OpenTelemetry로 LLM 지연을 분석하려면 요청 아래 검색·생성·도구 호출의 span을 나눠 기록해야 합니다. 전체 요청이 늦다는 사실만으로는 어느 단계가 원인인지 구별하기 어렵습니다. OpenTelemetry는 부모·자식 span을 연결해 한 요청의 실행 경로를 표현합니다. OpenTelemetry, Traces
예를 들어 문서를 검색하고 답변을 생성한 뒤 티켓을 조회하는 앱에서는 세 구간의 시간이 필요합니다. 검색이 길면 검색 경로를, 생성 구간이 길면 모델 호출 주변을 조사할 수 있습니다. 단, 한 span이 오래 걸린다는 관측만으로 그 안의 원인이 모두 밝혀지는 것은 아닙니다.
이 글은 Python으로 작은 trace를 직접 만드는 실습입니다. 기준일은 2026년 9월 8일이며, 실제 OpenTelemetry SDK를 실행하되 검색·LLM·티켓 API는 sleep()과 합성 예외로 대신합니다. 모델 속도와 비용을 측정한 벤치마크가 아닙니다.
계측할 구간을 먼저 정하기
첫 실습에서는 요청 하나를 부모 span으로 두고, 그 아래 순차 실행하는 작업 세 개를 배치합니다. 자동 계측 없이도 어느 코드 구간을 측정하는지 눈으로 확인할 수 있도록 구성했습니다.
request
├─ retrieve 문서 검색을 대신하는 대기
├─ generate 모델 호출을 대신하는 대기
└─ tool 티켓 조회를 대신하는 대기와 타임아웃
span 이름에는 작업 종류를 넣고 매번 달라지는 요청 본문을 넣지 않습니다. 이 예제는 구간 이름만으로 무엇을 재현했는지 알 수 있게 retrieve, generate, tool을 사용합니다. demo.* 속성은 합성 실습용으로 직접 정의한 이름입니다.
| 예제 구간 | 넣을 정보 | 읽으려는 내용 |
|---|---|---|
| request | 합성 실행 여부와 요청 실패 상태 | 전체 작업의 결과 |
| retrieve | 합성 문서 개수 | 검색 구간의 위치와 시간 |
| generate | 가짜 모델 이름 | 생성 구간의 위치와 시간 |
| tool | 가짜 도구 이름과 예외 | 도구 실패가 발생한 위치 |
이 구성을 실제 코드로 바꿀 때는 sleep() 자리에 검색 함수와 모델 호출을 넣으면 됩니다. 그때는 span의 경계가 어디인지 다시 확인해야 합니다. 예를 들어 생성 span이 HTTP 요청 전체를 감싼다면 전송·대기·응답 처리 시간이 함께 들어갑니다.
Python으로 부모·자식 span 만들기
Python 3.12.10 환경에서 opentelemetry-api와 opentelemetry-sdk 1.44.0을 설치해 다음 코드를 실행했습니다. 아래 설치 명령은 실습에 사용한 버전을 고정한 것이며, 최신 버전이라는 의미는 아닙니다. 새 작업 디렉터리에서 가상 환경을 만들고 설치하세요.
Windows PowerShell 예제입니다. 가상 환경을 활성화하는 대신 해당 환경의 Python 경로를 직접 사용합니다.
python -m venv .venv
& .venv/Scripts/python.exe -m pip install opentelemetry-api==1.44.0 opentelemetry-sdk==1.44.0
SDK 초기화, 콘솔 exporter 연결, 중첩 start_as_current_span()은 OpenTelemetry의 Python 수동 계측 문서에 기반합니다. 배치 처리한 span은 마지막의 provider.shutdown()에서 종료 처리를 합니다. Python Instrumentation, TracerProvider API
다음을 trace_demo.py로 저장하세요. 지연값 0.02·0.08·0.01초와 문서 개수 2는 직접 정한 실험 조건입니다. 실제 서버에서 얻은 측정값이나 API 응답이 아닙니다.
import time
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor, ConsoleSpanExporter
from opentelemetry.trace import Status, StatusCode
provider = TracerProvider()
provider.add_span_processor(BatchSpanProcessor(ConsoleSpanExporter()))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("blog.llm-demo")
try:
with tracer.start_as_current_span("request") as request:
request.set_attribute("demo.synthetic", True)
with tracer.start_as_current_span("retrieve") as span:
time.sleep(0.02)
span.set_attribute("demo.document_count", 2)
with tracer.start_as_current_span("generate") as span:
time.sleep(0.08)
span.set_attribute("demo.model", "mock-model")
with tracer.start_as_current_span("tool") as span:
span.set_attribute("demo.tool", "mock_ticket_lookup")
try:
time.sleep(0.01)
raise TimeoutError("synthetic timeout")
except TimeoutError as error:
span.record_exception(error)
span.set_status(Status(StatusCode.ERROR, "synthetic timeout"))
request.set_status(Status(StatusCode.ERROR, "tool failed"))
finally:
provider.shutdown()
같은 디렉터리에서 실행합니다. 출력은 콘솔 exporter가 만든 여러 JSON 객체이며, 한 개의 JSON 배열 형태는 아닙니다.
& .venv/Scripts/python.exe trace_demo.py
콘솔 출력에서 어떤 순서로 읽어야 할까요?
먼저 네 span의 trace_id가 같은지 확인하고, 자식 span의 parent_id가 request의 span_id와 같은지 봅니다. 출력 순서만으로 계층을 판단하지 마세요. 이번 실행에서는 자식이 먼저 끝나고 부모가 마지막에 끝나므로 부모 기록이 뒤에 나옵니다.
다음은 실제 실행 출력에서 확인한 구조를 정리한 표입니다. 표의 구간 이름과 상태는 원출력과 대조했으며, 식별자 값은 실행할 때마다 달라집니다.
| span 이름 | 부모 | status_code | 예외 이벤트 |
|---|---|---|---|
| retrieve | request | UNSET | 없음 |
| generate | request | UNSET | 없음 |
| tool | request | ERROR | exception 1개 |
| request | 없음 | ERROR | 없음 |
UNSET은 이 코드가 성공 상태를 명시적으로 설정하지 않았다는 뜻으로 읽어야 합니다. 실제 작업의 정확성을 인증한 결과는 아닙니다. 이번 검증에서는 출력에 span이 네 개 있는지, 연결 관계가 맞는지, 도구 예외가 기록됐는지를 별도 코드로 검사했습니다.
시작·종료 시각으로 구간 길이 읽기
이번 모의 실행의 UTC 시각 일부는 다음과 같습니다. 여기서 숫자는 SDK가 기록한 실제 실행 시각이며, 합성 대기와 코드 실행에 걸린 시간을 포함합니다.
retrieve 12:17:05.336562 → 12:17:05.357561
generate 12:17:05.357561 → 12:17:05.438753
tool 12:17:05.438753 → 12:17:05.450033
request 12:17:05.336562 → 12:17:05.450033
날짜는 2026-09-08이며, 약 81.2ms인 generate가 이 모의 실행에서 가장 긴 자식 구간입니다. 코드에서 해당 대기를 0.08초로 정했으므로 예상한 위치에 긴 구간이 나타났는지 확인한 것입니다. 특정 LLM이 이 속도로 응답한다는 뜻은 아닙니다.
이 예제는 자식 구간을 순서대로 실행합니다. 실제 앱에서 검색 두 개를 병렬로 실행하면 시간 구간이 겹칠 수 있으므로 자식 span의 길이를 단순히 더해 요청 전체 시간으로 사용하지 마세요. 시작·종료 위치와 겹침을 함께 읽는 편이 정확합니다.
잡아낸 예외도 오류로 기록하기
도구 함수 안에서 예외를 처리해도 실패 기록은 남겨야 합니다. 예제는 TimeoutError를 내부에서 잡은 뒤 record_exception()으로 이벤트를 추가하고, 도구 span의 상태를 ERROR로 설정합니다. 이 두 작업은 Python 공식 계측 안내에서도 함께 설명합니다. OpenTelemetry의 예외 기록 안내
이번 업무 규칙은 티켓 조회 실패를 요청 실패로 취급하므로 부모 request에도 명시적으로 ERROR를 설정했습니다. 자식 상태가 부모에게 자동 전파됐다는 뜻이 아닙니다. 선택적 도구의 실패를 허용하는 앱이라면 부모 상태를 어떻게 정할지는 그 앱의 성공 조건에 맞춰야 합니다.
원출력에서 도구 span에는 synthetic timeout 예외가 한 번 기록됐고 부모에는 별도 예외 이벤트가 없었습니다. 이처럼 어디서 실패했는지와 전체 요청을 실패로 볼지는 구분해서 남기면, 같은 장애를 여러 번 세는 해석을 피하기 쉽습니다.
프로그램의 종료 코드는 0이었습니다. 합성 예외를 코드에서 처리했기 때문입니다. 프로세스가 정상 종료됐다는 사실과 trace에 업무 실패가 기록됐다는 사실을 함께 읽어야 이 예제를 올바르게 이해할 수 있습니다.
GenAI 규약과 실습용 속성을 구분하기
이 코드의 demo.model은 GenAI 규약에 맞춘 모델 속성이 아닙니다. 수동 계측 원리를 설명하기 위한 로컬 이름입니다. API·SDK 패키지를 설치했다고 모델 토큰과 과금 정보가 자동으로 수집되는 예제도 아닙니다.
2026년 9월 8일 확인 기준으로 OpenTelemetry GenAI 문서는 별도 공식 저장소로 이전되어 있고, 그 README는 규약 상태를 Development로 표시합니다. 실제 GenAI 계측에 연결할 때는 사용하는 라이브러리의 속성 이름과 규약 버전을 확인해야 합니다. 이전 안내, GenAI semantic conventions
실무 적용에서는 우선 원문 없이 구간 이름·시간·오류 종류만 기록하는 범위를 정할 수 있습니다. 프롬프트나 도구 입력을 추가할 필요가 생기면 진단에 필요한 필드와 보존 대상을 따로 정하세요. 이번 실습은 합성 정보만 사용했으므로 실제 사용자 데이터의 수집·마스킹 동작은 검증하지 않았습니다.
또한 콘솔 출력 확인은 운영 수집 시스템의 검증과 다릅니다. 수집 서버로 전송하는 exporter, 샘플링 정책, 전송 실패 시 처리 방식은 이 실습에 포함하지 않았습니다. 이를 붙인 뒤에는 수집 대상과 누락 가능성을 별도로 확인해야 합니다.
처음 적용할 위치는 지금 가장 원인을 찾기 어려운 LLM 요청 하나면 충분합니다. 그 요청의 검색·생성·도구 호출 경계에 span을 넣고, 같은 trace 아래 연결되는지부터 확인하세요. 이후 실제로 길거나 실패한 구간 안에 필요한 계측을 추가하면 됩니다.
참고 자료
'AI' 카테고리의 다른 글
| Codex에서 GPT-6 아스트라를 써야 할 때: Sol·Terra와 작업별 선택 기준 (0) | 2026.09.16 |
|---|---|
| Codex Astra 비교 ㅡ GPT-6 Astra는 어떤 작업에 좋은가 (0) | 2026.09.15 |
| Codex Astra 비교 GPT-6 Astra는 어떤 작업에 좋은가 (0) | 2026.09.12 |
| AI 에이전트 평가 답변 도구 호출 최종 상태를 나눠 테스트하기 (1) | 2026.09.09 |
| AI 에이전트 컨텍스트 관리: 요약 메모 검색을 나누는 기준 (0) | 2026.09.08 |