비용 분석 & 관측성

토큰 지출 추적, 이중 분석 파이프라인, PII 제어, GrowthBook 게이팅.

01

개요

Claude Code의 비용 분석 시스템은 모든 토큰 사용을 마이크로달러 단위로 추적하고, 이중 분석 파이프라인(1P OTel + Datadog)을 통해 관측성을 제공합니다. 동시에 PII(개인 식별 정보) 보호를 위한 엄격한 분리 정책이 적용됩니다.

다루는 소스 파일: analytics/cost-tracker.tsanalytics/sinks.tsanalytics/datadog.tsanalytics/otel-pipeline.tsanalytics/pii.ts

핵심 구성 요소:

02

2파이프라인 아키텍처

Claude Code는 분석 데이터를 두 개의 독립적인 파이프라인을 통해 전송합니다. 이 이중 구조는 각 파이프라인의 장점을 활용하면서도 단일 장애점을 방지합니다.

// 2파이프라인 아키텍처 개요
//
//  이벤트 발생
//      │
//      ├──→ 파이프라인 1: 1P OTel (OpenTelemetry)
//      │     └─ OTel Logs SDK → OTLP 내보내기 → 디스크 JSONL 복원
//      │
//      └──→ 파이프라인 2: Datadog
//            └─ 40 이벤트 허용목록 → SHA-256 해싱 → RUM
03

cost-tracker.ts

비용 추적기는 모든 API 호출의 토큰 사용량을 마이크로달러 단위로 변환하여 3개 목적지로 전송합니다.

// analytics/cost-tracker.ts — 마이크로달러 단위 비용 추적
interface CostEvent {
  model: string
  inputTokens: number
  outputTokens: number
  cacheReadTokens: number
  cacheWriteTokens: number
  costMicroUSD: number      // 1 마이크로달러 = $0.000001
  timestamp: number
}

function trackCost(event: CostEvent): void {
  // 목적지 1: 로컬 세션 상태 (UI 표시용)
  state.totalCostUSD += event.costMicroUSD / 1_000_000

  // 목적지 2: 1P OTel 파이프라인
  emitOtelCostLog(event)

  // 목적지 3: Datadog RUM
  emitDatadogCostEvent(event)
}

마이크로달러 단위를 사용하는 이유

API 호출 한 건의 비용은 매우 작을 수 있습니다(예: 100 토큰 입력 = $0.00015). 부동소수점 연산의 정밀도 문제를 피하기 위해 정수 기반 마이크로달러($1 = 1,000,000 마이크로달러)를 사용합니다. 최종 표시 시에만 달러로 변환합니다.

주의사항

캐시 읽기 토큰(cacheReadTokens)과 캐시 쓰기 토큰(cacheWriteTokens)은 서로 다른 요금이 적용됩니다. 캐시 읽기는 일반 입력 토큰의 10% 비용이지만, 캐시 쓰기는 125% 비용이 부과됩니다.

04

분석 싱크 & 큐

분석 싱크는 제로 의존성으로 구현되며, 큐-드레인 패턴을 사용하여 싱크가 초기화되기 전에 발생한 이벤트도 유실 없이 처리합니다.

// analytics/sinks.ts — 큐-드레인 패턴
let eventQueue: AnalyticsEvent[] = []
let sinksInitialized = false

function logEvent(name: string, data: Record<string, unknown>): void {
  const event = { name, data, timestamp: Date.now() }

  if (!sinksInitialized) {
    // 싱크 초기화 전: 큐에 저장
    eventQueue.push(event)
    return
  }

  // 싱크 초기화 후: 직접 전송
  dispatchToSinks(event)
}

function initSinks(): void {
  // 싱크 연결
  connectOtelSink()
  connectDatadogSink()
  sinksInitialized = true

  // 대기열의 모든 이벤트 드레인
  for (const event of eventQueue) {
    dispatchToSinks(event)
  }
  eventQueue = []
}
딥 다이브 — 제로 의존성 설계

분석 모듈은 외부 라이브러리에 의존하지 않습니다(OTel SDK 제외). HTTP 전송, 큐 관리, 재시도 로직이 모두 자체 구현되어 있습니다. 이는 분석 라이브러리의 버그나 호환성 문제가 Claude Code의 핵심 기능에 영향을 주는 것을 방지하기 위함입니다.

05

Datadog 파이프라인

Datadog 파이프라인은 40개의 허용목록(allowlist) 이벤트만 전송하며, 모든 식별 가능 정보는 SHA-256 해싱 후 30개 버킷으로 매핑됩니다.

// analytics/datadog.ts — 이벤트 허용목록 & SHA-256 해싱
const ALLOWED_EVENTS = new Set([
  'tengu_started',
  'session_end',
  'tool_use',
  'api_call',
  // ... 총 40개 이벤트
])

function hashToBucket(value: string): number {
  const hash = createHash('sha256').update(value).digest()
  // 첫 4바이트를 uint32로 읽고 30으로 모듈러 연산
  return hash.readUInt32BE(0) % 30
}

function sanitizeForDatadog(event: AnalyticsEvent): DatadogEvent {
  return {
    name: event.name,
    // 사용자 ID → 30개 버킷 중 하나로 해싱
    userBucket: hashToBucket(event.data.userId),
    // 원본 사용자 ID는 제거
    timestamp: event.timestamp,
  }
}
딥 다이브 — 30버킷 SHA-256 해싱

사용자 식별 정보를 30개 버킷으로 해싱하는 이유는 k-익명성을 보장하기 위함입니다. 각 버킷에는 충분히 많은 사용자가 포함되므로 개별 사용자를 식별할 수 없습니다. SHA-256은 역산이 불가능하므로 버킷 번호에서 원본 ID를 복원할 수 없습니다. 30이라는 버킷 수는 분석에 유용한 세분화를 제공하면서도 프라이버시를 보호하는 균형점입니다.

06

1P OTel 파이프라인

1P(First-Party) 파이프라인은 OpenTelemetry Logs SDK를 기반으로 풍부한 원격 측정 데이터를 수집합니다. 네트워크 장애 시 디스크 JSONL 파일로 백업하고 이차 백오프로 재전송합니다.

// analytics/otel-pipeline.ts — 디스크 JSONL 복원 & 이차 백오프
async function exportWithResilience(logs: OtelLog[]): Promise<void> {
  try {
    await otlpExport(logs)
  } catch (err) {
    // 네트워크 장애: 디스크에 JSONL로 저장
    await appendToJSONL(RECOVERY_PATH, logs)

    // 이차 백오프로 재시도 스케줄링
    scheduleRetry({
      backoff: 'quadratic',  // 1s, 4s, 9s, 16s, 25s...
      maxRetries: 10,
      onRetry: () => drainJSONL(RECOVERY_PATH),
    })
  }
}

// 이차 백오프: delay = attempt^2 * 1000ms
function quadraticBackoff(attempt: number): number {
  return attempt * attempt * 1000
}
주의사항

이차 백오프(quadratic backoff)는 지수 백오프보다 초기 재시도 간격이 짧으면서도 급격히 증가합니다. 10번째 재시도는 100초(약 1.7분) 후에 발생합니다. 이는 짧은 네트워크 중단에는 빠르게 복구하면서도 장기 장애 시 과도한 재시도를 방지합니다.

07

PII 분리

PII(개인 식별 정보) 분리는 _PROTO_ 접두사 패턴과 TypeScript의 never 타입 마커를 결합하여 컴파일 타임에 PII 유출을 방지합니다.

// analytics/pii.ts — _PROTO_ 패턴 & never 타입 마커

// PII 데이터는 _PROTO_ 접두사가 붙은 필드에만 저장
interface UserEvent {
  _PROTO_userId: string     // PII: 1P 파이프라인에만 전송 가능
  _PROTO_email: string      // PII: 1P 파이프라인에만 전송 가능
  eventName: string         // 비PII: 모든 파이프라인에 전송 가능
  timestamp: number         // 비PII: 모든 파이프라인에 전송 가능
}

// Datadog 전송 타입: _PROTO_ 필드는 never로 마킹
type DatadogSafe<T> = {
  [K in keyof T]: K extends `_PROTO_${string}` ? never : T[K]
}

// 컴파일 에러: _PROTO_ 필드를 Datadog에 전송하려 하면 타입 에러
function sendToDatadog(event: DatadogSafe<UserEvent>): void {
  // event._PROTO_userId는 never 타입 → 접근 불가
}
딥 다이브 — 왜 런타임 검증이 아닌 타입 시스템인가?

런타임 검증(예: if (key.startsWith('_PROTO_')) throw)은 테스트에서 누락될 수 있습니다. TypeScript의 never 타입 마커를 사용하면 PII 필드에 접근하는 코드 자체가 컴파일되지 않으므로, 리뷰어의 주의력에 의존하지 않는 구조적 안전장치가 됩니다.

08

GrowthBook 게이팅

GrowthBook은 Claude Code에서 4가지 용도로 사용됩니다:

  1. 기능 플래그 (Feature Flags): 새 기능의 점진적 롤아웃 제어
  2. A/B 테스트: UI 변형, 프롬프트 변형 등의 실험
  3. 점진적 롤아웃: 사용자 비율 기반 기능 활성화 (1% → 10% → 100%)
  4. 원격 구성: 재배포 없이 임계값, 문자열, 숫자 등을 변경
// GrowthBook 4가지 용도 예시

// 1. 기능 플래그
if (isFeatureEnabled('ultraplan_v2')) {
  launchUltraplanV2(prompt)
}

// 2. A/B 테스트
const variant = getExperimentVariant('prompt_style')

// 3. 점진적 롤아웃
const rolloutPct = getFeatureValue('new_model_rollout', 0)

// 4. 원격 구성
const maxTokens = getFeatureValue('max_output_tokens', 16384)
09

고객용 OTel (옵트인)

엔터프라이즈 고객은 자체 OTel 수집기를 구성하여 Claude Code의 원격 측정 데이터를 자사 관측성 스택으로 전송할 수 있습니다. 이 기능은 완전한 옵트인 방식이며, 별도의 내보내기 경로를 사용합니다.

// 고객용 OTel 옵트인 구성
// settings.json에 엔드포인트 지정
{
  "otel": {
    "endpoint": "https://otel-collector.company.com:4318",
    "headers": {
      "Authorization": "Bearer ${OTEL_TOKEN}"
    }
  }
}
10

핵심 요약

핵심 포인트

  • 2파이프라인 아키텍처: 1P OTel(풍부한 원격 측정)과 Datadog(실시간 대시보드)이 독립적으로 운영되어 단일 장애점을 방지합니다
  • cost-tracker.ts는 마이크로달러 단위를 사용하여 부동소수점 정밀도 문제를 회피하며, 3개 목적지(로컬 상태, OTel, Datadog)로 전송합니다
  • 큐-드레인 패턴으로 싱크 초기화 전에 발생한 이벤트도 유실 없이 처리합니다
  • Datadog은 40개 이벤트 허용목록30버킷 SHA-256 해싱으로 프라이버시를 보호합니다
  • 1P OTel은 네트워크 장애 시 디스크 JSONL 복원이차 백오프로 데이터 유실을 방지합니다
  • PII 분리는 _PROTO_ 접두사와 TypeScript never 타입으로 컴파일 타임에 유출을 차단합니다
  • GrowthBook은 기능 플래그, A/B 테스트, 점진적 롤아웃, 원격 구성의 4가지 용도로 활용됩니다
11

지식 확인

퀴즈 — 5문제

Q1. cost-tracker.ts가 달러 대신 마이크로달러 단위를 사용하는 이유는?

  • A) 국제화(i18n)를 위해 통화 단위를 추상화하기 위해
  • B) API 호출당 비용이 매우 작아 부동소수점 정밀도 문제를 피하기 위해 정수 기반 계산을 사용
  • C) 서버 API가 마이크로달러 단위로 응답하기 때문에
  • D) UI에서 큰 숫자를 표시하면 사용자에게 더 인상적이기 때문에
100 토큰 입력의 비용은 $0.00015 수준입니다. 이런 작은 값의 부동소수점 연산은 정밀도 오류를 유발할 수 있으므로, 정수 기반 마이크로달러($1 = 1,000,000)를 사용하고 최종 표시 시에만 변환합니다.

Q2. 큐-드레인 패턴의 목적은?

  • A) 이벤트를 배치로 묶어 네트워크 효율을 높이기 위해
  • B) 이벤트 순서를 역순으로 처리하기 위해
  • C) 메모리 사용량을 제한하기 위해
  • D) 분석 싱크가 초기화되기 전에 발생한 이벤트를 큐에 저장했다가, 초기화 후에 드레인하여 유실을 방지
부트 시퀀스 초기에 발생하는 이벤트(예: tengu_started)는 분석 싱크가 아직 초기화되지 않은 시점에 발생합니다. 큐-드레인 패턴은 이 이벤트들을 임시 큐에 저장했다가 initSinks() 후에 일괄 전송합니다.

Q3. Datadog 파이프라인에서 사용자 ID를 30개 버킷으로 SHA-256 해싱하는 이유는?

  • A) k-익명성을 보장하여 개별 사용자를 식별할 수 없게 하면서도 유용한 세분화를 제공하기 위해
  • B) Datadog API의 필드 길이 제한을 충족하기 위해
  • C) 해시 충돌을 최대화하여 데이터를 압축하기 위해
  • D) 정렬 성능을 향상시키기 위해
30개 버킷 해싱은 각 버킷에 충분히 많은 사용자가 포함되도록 하여 k-익명성을 보장합니다. SHA-256은 역산이 불가능하므로 버킷 번호에서 원본 ID를 복원할 수 없으며, 30이라는 숫자는 분석 유용성과 프라이버시의 균형점입니다.

Q4. _PROTO_ 접두사와 TypeScript never 타입의 조합이 런타임 검증보다 우수한 이유는?

  • A) 런타임 성능이 더 좋기 때문에
  • B) 런타임 검증보다 구현이 간단하기 때문에
  • C) PII 필드 접근 자체가 컴파일되지 않으므로 테스트 누락이나 리뷰어 실수에 의존하지 않는 구조적 안전장치
  • D) JavaScript 런타임이 never 타입을 강제하기 때문에
런타임 검증은 테스트에서 누락될 수 있고 코드 리뷰에서 놓칠 수 있습니다. TypeScript의 never 타입은 해당 필드에 접근하는 코드 자체를 컴파일 에러로 만들므로, 사람의 주의력에 의존하지 않는 강력한 안전장치입니다.

Q5. 1P OTel 파이프라인이 이차 백오프(quadratic backoff)를 사용하는 이유는?

  • A) 지수 백오프보다 구현이 간단하기 때문에
  • B) 짧은 네트워크 중단에는 빠르게 재시도하면서도, 장기 장애 시에는 간격이 급격히 증가하여 과도한 재시도를 방지
  • C) 서버가 이차 함수 형태의 Rate Limit을 적용하기 때문에
  • D) 랜덤 지터가 필요 없어 예측 가능한 재시도가 가능하기 때문에
이차 백오프(1s, 4s, 9s, 16s...)는 초기 재시도 간격이 짧아 일시적 네트워크 문제에 빠르게 대응하면서도, 시도 횟수의 제곱에 비례하여 증가하므로 장기 장애 시 서버에 과부하를 주지 않습니다.
0 / 5