@maru

Fastify v6와 @fastify/otel — GenAI 에이전트 관측 가능성 구축하기
고성능 Node.js 백엔드를 구축할 때 Fastify는 검증된 선택지이지만, 복잡한 생성형 AI 에이전트를 도입하는 순간 기존의 HTTP 요청 단위 모니터링은 한계에 부딪힙니다. 에이전트의 다단계 추론 과정과 도구 실행 흐름을 정확히 파악하려면, 애플리케이션 깊숙한 곳까지 추적할 수 있는 정밀한 관측 가능성이 필요합니다. Fastify v6의 격리된 스코프 아키텍처와 공식 오픈텔레메트리 플러그인을 통해 성능 오버헤드 없이 강력한 에이전트 추적 시스템을 구축하는 방법을 알아보겠습니다.
레거시의 종말: @opentelemetry/instrumentation-fastify에서 @fastify/otel로
기존 Node.js 환경에서 Fastify 모니터링을 담당하던 커뮤니티 패키지인 @opentelemetry/instrumentation-fastify가 공식적으로 지원을 중단했습니다. 대신 Fastify 생태계가 일급 시민으로 직접 유지 관리하는 공식 플러그인인 @fastify/otel로 완전히 전환해야 합니다. 외부 도구를 조립해 사용하던 임시방편적인 방식에서 벗어나, 프레임워크 코어와 유기적으로 결합된 관측 표준을 기반으로 아키텍처를 재편해야 할 때입니다.
@fastify/otel은 Fastify의 내부 생명주기 훅과 긴밀히 통합되어 동작하며 요청 단위의 컨텍스트를 안정적으로 유지합니다. 개발자가 직접 비동기 컨텍스트 전파 메커니즘을 복잡하게 조율할 필요 없이, 라우터 핸들러 내부에서 제공되는 request.openTelemetry() 메서드를 호출하는 것만으로 손쉽게 흐름을 제어할 수 있습니다.
이 메서드는 현재 요청에 맵핑된 루트 span, 해당 요청의 tracer, 그리고 활성화된 context를 담은 객체를 반환합니다. 덕분에 HTTP 요청이 들어오는 최초 진입점부터 하위 비동기 에이전트 레이어의 도구 호출 및 추론 연산 단계까지 하나의 분산 트레이스 흐름으로 끊김 없이 정교하게 연결할 수 있습니다.
Fastify v6의 스코프 플러그인과 비침습적 Tracer 전파
Fastify v6는 모노레포 환경에서 흔히 발생하던 전역 타입 오염 문제를 해결하기 위해 TypeScript 전역 모듈 보강 구조를 탈피했습니다. 기존에는 플러그인이 등록한 데코레이터가 전역 네임스페이스에 강제로 주입되어 예기치 않은 타입 충돌을 유발했습니다. v6부터는 데코레이터 타입을 플러그인이 등록된 스코프 내로 국한하는 등록 스코프 방식을 제안합니다.
이러한 고립된 타입 제어는 fastify-plugin 패키지의 createPlugin 헬퍼를 활용해 유연하게 구현할 수 있습니다. 플러그인 내부에서만 유효한 데코레이터와 타입을 정의함으로써, 루트에 등록된 OTel Tracer 인스턴스를 하위 LLM 에이전트 레이어까지 결합을 최소화한 상태로 안전하게 공유할 수 있게 됩니다.
최신 OpenTelemetry GenAI 시맨틱 컨벤션의 핵심 표준
오픈텔레메트리(OpenTelemetry)의 생성형 AI 관측 규격은 이제 독립된 저장소인 open-telemetry/semantic-conventions-genai로 완전히 분리되어 표준화 단계를 밟아가고 있습니다. 이번 개정의 핵심은 모호했던 레거시 명세들을 정교하게 다듬고, 에이전트의 작동 흐름 상에서 발생할 수 있는 민감한 개인정보(PII) 유출 방지 기준을 한층 강화한 점입니다.
가장 대표적인 변화는 기존 LLM 시스템의 구분을 위해 사용하던 gen_ai.system 속성이 공급자를 명확히 식별할 수 있는 gen_ai.provider.name으로 대체된 것입니다. 또한 원본 프롬프트와 완성형 텍스트가 가공 없이 저장되어 보안 사고를 유발하던 날 것 그대로의 문자열 속성들은 폐기되거나 기본 비활성화 처리되었습니다.
그 대신 대화 맥락을 구조화하여 안전하게 추적할 수 있도록 직렬화된 JSON 문자열 기반의 통합 속성이 새롭게 도입되었습니다. 시스템 프롬프트는 gen_ai.system_instructions에 지정하고, 사용자의 질문과 모델의 응답 메타데이터는 각각 gen_ai.input.messages와 gen_ai.output.messages에 JSON 구조로 직렬화하여 주입하는 방식을 권장합니다.
// OpenTelemetry GenAI 스팬 속성 할당
span.setAttributes({
'gen_ai.provider.name': 'openai',
'gen_ai.system_instructions': '안전한 보안 가이드를 준수합니다.',
'gen_ai.input.messages': JSON.stringify([{ role: 'user', content: 'Fastify v6' }]),
'gen_ai.output.messages': JSON.stringify([{ role: 'assistant', content: '안전하고 빠릅니다.' }])
});이러한 정형화된 표준 규격을 준수함으로써 멀티 에이전트 내부의 무분별한 텍스트 데이터 노출 문제를 미연에 방지하고, 시각화 도구와의 호환성을 극대화할 수 있습니다.
Fastify v6 환경에서의 실전 데코레이터 통합 구현
Fastify v6의 독립된 스코프 구조와 @fastify/otel 플러그인을 활용하면, HTTP 요청 진입부터 에이전트의 내부 작업 단계까지 매끄럽게 연결되는 추적 체인을 구현할 수 있습니다. 특히 @fastify/otel은 라우터 핸들러 전체를 활성 스팬 컨텍스트로 감싸주기 때문에, 개발자가 직접 상위 맥락을 전달해 주지 않아도 오픈텔레메트리 API가 자동으로 부모-자식 관계를 연결해 줍니다.
다음은 Fastify v6와 @opentelemetry/api를 결합하여 에이전트의 LLM 호출 단계를 추적하는 비침습적 구현 예시입니다.
import { type Span } from '@opentelemetry/api';
import type { FastifyPluginAsync } from 'fastify';
export const agentRoutes: FastifyPluginAsync = async (fastify) => {
fastify.post('/v1/chat', async (request, reply) => {
// 1. 요청 스코프의 OTel 컨텍스트 획득
const otel = request.openTelemetry();
if (!otel.enabled || !otel.instrumented) {
return { response: '텔레메트리 비활성 상태' };
}
const { tracer } = otel;
// 2. 상위 컨텍스트를 자동 상속받는 하위 활성 스팬 생성
return tracer.startActiveSpan('llm.generate', async (span: Span) => {
try {
const userPrompt = (request.body as { prompt: string }).prompt;
// 3. 최신 시맨틱 규격을 반영하여 제공자 정보와 메시지 설정 (JSON 직렬화)
span.setAttributes({
'gen_ai.provider.name': 'openai',
'gen_ai.request.model': 'gpt-4o',
'gen_ai.input.messages': JSON.stringify([
{ role: 'user', content: userPrompt }
])
});
// 실제 LLM 서비스 호출 과정 (예시)
const mockResponse = `답변 결과: ${userPrompt}`;
// 4. 출력 데이터 및 토큰 사용량 속성 기록
span.setAttributes({
'gen_ai.output.messages': JSON.stringify([
{ role: 'assistant', content: mockResponse }
]),
'gen_ai.usage.input_tokens': 15,
'gen_ai.usage.output_tokens': 30
});
return { response: mockResponse };
} catch (error) {
if (error instanceof Error) {
span.recordException(error);
}
reply.code(500);
return { error: '내부 에러 발생' };
} finally {
// 5. 작업 완료 후 반드시 스팬 명시적 종료
span.end();
}
});
});
};이 구현의 핵심은 Fastify v6의 장점인 스코프 격리를 그대로 유지한다는 점입니다. 전역 네임스페이스를 오염시키는 데코레이터 타입 병합 대신, 라우트 스코프 단위로 타입을 캡슐화하여 관리할 수 있습니다. 또한, 민감할 수 있는 프롬프트 입출력 데이터를 개별 이벤트가 아닌 직렬화된 JSON 문자열 속성으로 수집함으로써 개인정보의 외부 노출 가능성을 구조적으로 최소화합니다.
성능 저하 없는 안전한 관측 생태계 유지하기
Fastify v6는 기존의 복잡한 스키마 컴파일 방식 대신 V8 엔진의 네이티브 직렬화 최적화를 적극적으로 활용합니다. 이러한 초고속 프레임워크의 성능 이점을 온전히 누리려면, 실시간 트레이싱 가공 비용이 애플리케이션의 발목을 잡지 않도록 신경 써야 합니다. 프로덕션 환경에서는 트레이스 샘플링 비율을 합리적으로 제어하고, 무거운 생성형 AI 페이로드가 Node.js 이벤트 루프를 차단하지 않도록 비동기 배치 방식으로 백그라운드에서 안전하게 내보내는 아키텍처 설계가 필수적입니다.
참고 링크
- Fastify Project & Sentry Engineering — @fastify/otel Official Transition and instrumentHooks Optimization (v0.20.1)
- OpenTelemetry JS Release Group — Federation of GenAI Semantic Conventions in OpenTelemetry JS v1.42.0
- OpenTelemetry Semantic Conventions GenAI Working Group — Standardized Agent Spans: invoke_agent and execute_tool