콘텐츠로 이동

LLM 관측성 콜백: 설계 기록

이 문서는 요청별 사용량을 LLM 관측성 플랫폼의 네이티브 스키마로 변환하는 1차 콜백 연동의 설계를 기록합니다(이슈 #2376). Langfuse는 현재 제공되며, 이 페이지는 계획이 아니라 구현된 계약을 설명합니다.

현재

관측성 수단은 OTLP 트레이싱(tracing.otlp, --features otel), Prometheus 메트릭, 프로세스 로그, 사용량 내보내기(/admin/usage/export)입니다. observability.callbacks 섹션은 다섯 번째 수단을 더합니다. 요청마다 trace 하나를 플랫폼의 네이티브 객체 모델로 매핑하므로, 운영자는 OTLP나 사용량 내보내기 뒤에서 변환 구성 요소를 직접 운영하고 유지할 필요가 없습니다.

이 섹션은 tracing.enabled와 독립적입니다. 분산 트레이싱을 꺼도 콜백 내보내기가 조용히 함께 꺼지지 않고, 콜백을 켜도 OTLP가 켜지지 않습니다.

설정

observability:
  callbacks:
    enabled: true
    include_content: false
    max_queue: 1024
    max_batch_records: 32
    sinks:
      - provider: langfuse
        endpoint: https://cloud.langfuse.com
        public_key_env: LANGFUSE_PUBLIC_KEY
        secret_key_env: LANGFUSE_SECRET_KEY
        allow_private_endpoint: false
        timeout: 5s
        max_attempts: 3
        initial_delay: 500ms
        max_delay: 30s

섹션은 선택 사항이며 기본값은 꺼짐입니다. observability: 섹션이 없으면 아무것도 큐에 들어가거나 전송되거나 기록되지 않고, 요청 경로는 OnceLock 조회 한 번과 원자적 로드 한 번만 지불합니다.

설정 타입과 런타임은 모든 Cargo 기능 조합에서 컴파일되므로 설정 파일은 어디서나 동일하게 검증되고, 섹션을 조용히 버리는 빌드는 없습니다. enabled가 참이면 섹션은 최소 하나의 싱크를 요구합니다. max_queue는 시작 시 고정되어 바꾸려면 재시작이 필요하고, 나머지 필드는 다음에 게시되는 리로드에 적용됩니다.

프로바이더 집합과 자격 증명 변수는 적용 시점에 해석됩니다. 환경에 자격 증명이 없는 싱크는 변수 이름만 알리는 경고와 함께 비활성이 되고, 나머지 싱크는 계속 전달합니다.

이벤트 소스

요청 하나는 요청 단위 플래그 하나로 연결된 두 생산자에서 정확히 trace 하나를 만듭니다.

  • 본문 포함 가능. 프롬프트 로그 캡처가 마스킹 단계를 마친 뒤 조립된 레코드로 콜백 런타임을 호출합니다. 캡처는 이미 요청마다 레코드를 정확히 하나 쓰므로 /v1/chat/completions, /v1/responses, /anthropic/v1/messages를 스트리밍 여부와 관계없이 모두 포함합니다.
  • 메타데이터 전용. 완료 경로가 요청 원장(src/services/stats/, 이슈 #2053)의 요청 단위 정보, 즉 모델, 백엔드, 토큰 수, 지연 시간, 결과를 보탭니다. 나머지 모든 경로와, 위 세 경로 중 캡처가 요청을 받아들이지 않은 경우를 담당합니다. 공유 플래그가 본문 생산자가 이미 보냈다고 알리면 스스로를 건너뜁니다.

스트리밍 요청은 청크마다 이벤트를 만들지 않고 스트림이 확정될 때 출력을 모은 trace 하나를 만듭니다. 그래서 스트리밍 요청과 비스트리밍 요청의 형태가 같습니다.

본문 정책

프롬프트, 완성, 도구 호출 텍스트는 운영자가 include_content: true를 설정했고 프롬프트 로그 캡처가 키별 동의 게이트 아래에서 요청을 받아들였을 때만 프로세스를 떠나며, 그 시점에는 마스킹 단계가 이미 끝나 있습니다. include_content가 거짓이면 런타임이 큐에 넣기 전에 레코드에서 본문 필드를 지우고, 메타데이터 전용 생산자는 본문을 아예 조립하지 않습니다. 자격 증명은 시작 시 *_env 변수에서 읽어 HTTP Basic 인증에만 쓰며, 로그에 남기지 않고, 오류에 넣지 않고, 다른 어디로도 보내지 않습니다. 엔드포인트는 설정 출력과 diff에서 민감한 값으로 취급합니다.

전달

전달은 요청 경로 밖에서 실패를 허용하는 방식이며, 알림 런타임(src/services/alerting/)을 따릅니다. 생산자는 유한 큐 하나에 try_send만 합니다. 전용 워커가 HTTP를 수행하고, 같은 설정 세대에서 만들어진 trace를 max_batch_records개까지 묶으며, 응답을 막지 않습니다. 큐가 가득 차면 trace를 버리고 설정된 싱크마다 한 번씩 횟수를 셉니다.

싱크마다 max_attempts, initial_delay, max_delay로 상한을 둔 지수 백오프로 재시도합니다. 부분 성공 응답은 거부된 span을 실패로 세고, 수락된 span은 다시 보내지 않습니다. 408이나 429가 아닌 4xx는 영구 실패로 보고 재시도하지 않으므로, 잘못된 자격 증명이나 거부된 본문이 단일 워커를 전체 예산 동안 점유할 수 없습니다. 종료 시 워커는 큐를 비우라는 신호를 받고, 이미 진행 중인 전달은 자체 타임아웃 예산 안에서 먼저 끝난 뒤 큐에 남은 레코드가 5초 안에 배출됩니다.

엔드포인트는 공용 EndpointPolicy(src/services/export_webhook/policy.rs)를 거칩니다. 리다이렉트는 끄고, DNS를 고정하며, 자체 호스팅 수집기를 위해 allow_private_endpoint를 설정하지 않는 한 루프백과 사설 주소를 거부합니다.

스키마 매핑

Langfuse 어댑터는 x-langfuse-ingestion-version: 4 헤더와 Basic 인증으로 POST {endpoint}/api/public/otel/v1/traces에 보내고, 프롬프트 로그 내보내기가 이미 제공하는 OTLP/JSON 매퍼(src/services/prompt_log_export/langfuse.rs)를 재사용합니다. 요청 하나가 마스킹된 프롬프트와 완성, 사용량 수치, 결과를 담은 OTLP span 하나가 됩니다. 콜백 어댑터는 자체 스코프와 span 이름을 사용하므로 콜백 trace와 프롬프트 로그 내보내기 trace가 같은 스트림처럼 보이지 않습니다.

어댑터는 프로바이더 SDK를 링크하지 않고 기존 reqwest 클라이언트로 일반 HTTP를 사용하므로, embed 부정 의존성 계약에 의존성을 추가하지 않습니다.

메트릭

observability_callback_events_total{sink,result}가 싱크마다 delivered, failed, dropped trace를 셉니다. observability_callback_queue_depth가 현재 큐 점유를 보고합니다.

후속 프로바이더

Braintrust, Arize, Langsmith는 같은 어댑터 접점 뒤의 후속 작업입니다. 각각 고유한 네이티브 스키마 매핑과 인증이 필요하므로, 쓰이지 않는 일반 인터페이스 뒤에 묶지 않고 각자의 이슈로 제공합니다.