CC Usage Dashboard

Architecture

파이프라인

클라이언트에 커스텀 계측이 없습니다 — Claude Code의 native OTel export를 그대로 받습니다.

Claude Code 참가자 CLI가 OTLP로 메트릭·로그를 내보냄
OTel Collector collector-config.yaml · clickhouse exporter
ClickHouse ReplicatedMergeTree · hot/cold(S3) · 시간별 rollup — 이 스택의 심장
Node.js API + React SPA 컨테이너 1개(포트 8080) — EKS · ECS · 로컬 동일

저장 계층

누적 카운터 함정

otel_metrics_sum의 값은 델타가 아니라 세션 단위 누적입니다. sum(Value)를 그대로 쓰면 100배 이상 과대집계됩니다 — 과거에 실제로 당한 문제이고, 이 저장소에서 가장 지키는 규칙입니다.

기간 [시작, 끝)의 실제 증가량은 (시리즈 × 세션) 단위로 "끝 직전 누적값 − 시작 직전 누적값"을 diff해서 구합니다. Prometheus의 increase()와 같은 모양이고, 서버에서는 incFlat(스냅샷) / incBucketed(시계열) 헬퍼가 담당합니다.

SELECT sum(inc) FROM (
  SELECT greatest(maxIf(Value, TimeUnix < {끝}) - maxIf(Value, TimeUnix < {시작}), 0) AS inc
  FROM otel_metrics_sum
  WHERE MetricName = 'claude_code.cost.usage' AND TimeUnix < {끝}
  GROUP BY cityHash64(toString(Attributes)), SessionId
)

같은 이유로 "수집이 끊겼는지"는 값의 변화가 아니라 행 도착 여부로 봐야 합니다. 세션이 살아있으면 같은 누적값이 계속 재보고되므로, 시간당 행 수가 고정된 구간은 정상(신규 활동만 없음)이고 끊김은 버킷에 행이 0인 구간입니다.

그룹 판별 (bedrock / enterprise)

Workshop Studio 시나리오에서는 같은 이미지를 쓰고 참가자가 로그인 방식을 고르기 때문에, EXPERIMENT_GROUP 같은 정적 플래그를 심을 수 없습니다. 그래서 텔레메트리로 사후 판별합니다.

우선순위 규칙 결과
1 세션이 호출한 모델 이름에 anthropic. 또는 : 포함 bedrock
2 Bedrock 신호가 없고 datapoint Attributesorganization.id 존재 enterprise
3 둘 다 아님 unknown (약 11%)

그레인은 SessionId입니다. 리전 프리픽스는 us.일 수도 global.일 수도 있어 프리픽스가 아니라 anthropic. 포함 여부로 판별합니다. organization.id는 ResourceAttributes가 아니라 datapoint별 Attributes에 실립니다 — 처음 가정이 틀렸던 지점입니다.

모델 이름 정규화

Bedrock의 리전·날짜·버전 접미사를 떼어 같은 모델이 비용/사용량 breakdown에서 한 줄로 모이게 합니다. 서버에는 같은 5단계 정규식이 두 곳(normModel() in queries.js, normalizeModelId() in pricing.js)에 있고 서로 동기 상태를 유지해야 합니다.

global.anthropic.claude-sonnet-5   →  claude-sonnet-5
us.anthropic.claude-opus-4-8-v1:0  →  claude-opus-4-8
claude-haiku-4-5-20251001          →  claude-haiku-4-5

배포 대상 — 특정 오케스트레이터에 종속되지 않는다

애플리케이션은 컨테이너 하나입니다. 멀티스테이지 빌드가 React SPA를 dist/로 만들고, 같은 이미지의 Node 서버가 API와 그 정적 파일을 함께 서빙합니다. 쿠버네티스 API를 호출하는 코드도, 사이드카 전제도 없습니다 — 상태는 전부 ClickHouse에 있고 프로세스는 무상태입니다.

대상필요한 것비고
EKS (현재 운영) Deployment 2 replica, Graviton 노드풀, ClickHouse Operator + Keeper Terraform 전체가 infra/에 있음
ECS / Fargate linux/arm64 태스크(포트 8080) + ALB, 태스크 롤에 Bedrock 권한(챗을 쓸 때만) 이미지·환경변수는 동일. 태스크/서비스/ALB IaC는 새로 작성해야 함
로컬 / 온프렘 docker compose -f dashboard/docker-compose.yml up ClickHouse까지 함께 뜬다

공통 전제는 하나뿐입니다 — HTTP로 닿는 ClickHouse. 자체 호스팅이든 매니지드든 앱은 구분하지 않습니다. 헬스체크는 /healthz(Basic Auth 면제, ClickHouse ping 포함)를 쓰면 됩니다.

AI 분석 경로

POST /api/chat은 Bedrock ConverseStream에 run_sql 툴 하나를 붙인 루프입니다(최대 4 hop). 모델이 SQL을 만들고, 서버가 sanitizeSql()로 검사한 뒤 읽기 전용 계정으로 ClickHouse에 실행하고, 결과 행(최대 200)을 모델에게 돌려주고, 모델이 한국어 요약과 표를 스트리밍합니다.

인프라

비용 절감 메모: 대시보드 쿼리를 원본에서 rollup으로 옮긴 것이 가장 큰 효과였습니다 (읽는 행 수 ~86배 감소). PII 보존은 원본과 rollup 모두 180일 TTL로 맞춰, 원본이 지워진 뒤 rollup에 이메일이 남는 우회 경로를 막았습니다.