챗봇을 배포하고 나서 제일 답답했던 건, 뭔가 느리거나 이상할 때 안이 안 보인다는 거였다. 응답이 5초 걸렸는데 그게 모델이 느린 건지, 툴 호출이 여러 번 돈 건지, 프롬프트가 비정상적으로 길어진 건지 로그만으로는 알 수가 없었다. console.log(usage)를 여기저기 박는 것도 한두 번이지, 스텝이 여러 개인 에이전트에서는 금방 지저분해진다.
Vercel AI SDK에는 experimental_telemetry라는 옵션이 있다. 플래그 하나 켜면 generateText, streamText 호출을 OpenTelemetry 스팬으로 자동 기록해준다. 토큰 수, 지연시간, 모델 이름, 각 스텝의 프롬프트까지 다 스팬 속성으로 붙는다.
켜는 법
호출할 때 옵션만 넘기면 된다.
import { generateText } from 'ai';
import { anthropic } from '@ai-sdk/anthropic';
const result = await generateText({
model: anthropic('claude-sonnet-4-5'),
prompt: '이 코드 리뷰해줘...',
experimental_telemetry: {
isEnabled: true,
functionId: 'code-review',
metadata: { userId: 'user_123', feature: 'review' },
},
});
functionId는 이 호출이 앱 어디서 났는지 구분하는 이름이고, metadata에 넣은 값은 그대로 스팬 속성으로 붙는다. 나중에 "이 유저의 호출만 보기" 같은 필터링을 할 때 이게 있어야 편하다.
트레이스를 어디론가 보내기
플래그만 켠다고 데이터가 보이진 않는다. OpenTelemetry가 스팬을 수집할 곳이 있어야 한다. Next.js에서는 @vercel/otel이 제일 간단하다.
npm install @vercel/otel
instrumentation.ts를 프로젝트 루트에 만들어준다.
// instrumentation.ts
import { registerOTel } from '@vercel/otel';
export function register() {
registerOTel({ serviceName: 'my-chatbot' });
}
이렇게만 해두면 Vercel에 배포했을 때 대시보드 Observability 탭에서 트레이스가 잡힌다. 로컬이나 별도 백엔드로 보고 싶으면 Langfuse, Braintrust 같은 도구가 OTLP 엔드포인트를 제공하니 OTEL_EXPORTER_OTLP_ENDPOINT 환경변수만 맞춰주면 된다.
스팬에 뭐가 담기나
호출 하나가 트레이스로 열리고, 그 안에 스텝마다 자식 스팬이 생긴다. 실제로 유용했던 속성들:
ai.usage.promptTokens/ai.usage.completionTokens— 스텝별 토큰ai.response.msToFirstChunk— 스트리밍 첫 토큰까지 걸린 시간gen_ai.request.model— 어떤 모델이 불렸는지- 툴 호출은
ai.toolCall스팬으로 따로 뜬다
민감한 프롬프트가 트레이스에 통째로 남는 게 부담이면 recordInputs: false, recordOutputs: false로 본문만 뺄 수 있다.
남는 것
관측을 붙이고 나서 제일 크게 달라진 건, 느린 응답의 원인을 추측하지 않게 됐다는 점이다. 에이전트가 툴을 세 번 돌아서 느렸던 건지, 프롬프트에 딸려온 컨텍스트가 8천 토큰이었던 건지 트레이스 한 번 열면 바로 보인다. 프로덕션에 올리기 전에 isEnabled: true 한 줄, 이건 진작 켤 걸 그랬다 싶었다.