플랫폼 지표 계약
지표 이름과 라벨과 집계 기준을 확인한다
계약
설치 전체의 Prometheus 지표 이름·타입·라벨과 집계 기준이다. 수집기 연결은 지표 수집을 따른다.
값을 만드는 방식
- scrape할 때 DB에서 계산한다.
metrics역할(bun run apps/control-host/src/main.ts metrics)은 상태를 갖지 않는다. 요청마다 DB를 읽어 지표로 바꾼다. 그래서 Pod가 재시작해도 counter가 0으로 돌아가지 않는다. - target은 하나다. 값은 설치 전체의 합이라 replica가 둘이면 같은 값을 두 번 낸다. chart가 replica를 1로 고정하고
Recreate로 바꾼다. - 읽기 전용 연결이다. DB 연결은
default_transaction_read_only=on으로 열려 있어 실수로 보낸 쓰기는 DB가 거절한다. 세션 설정일 뿐 DB 권한은 아니다.DATABASE_URL의 계정은 다른 역할과 같아 쓰기 권한이 있고, 연결 안에서 설정을 되돌리면 쓸 수 있다. 이 설정은 연결 시작 파라미터options로 보내므로,options를 거절하는 연결 풀러(ignore_startup_parameters에options가 없는 PgBouncer 등)를 거치면 metrics 역할만 연결하지 못한다. - 동시 scrape는 한 번 읽는다. 읽는 동안 들어온 요청은 같은 결과를 받는다. 읽기가 실패하면 503을 돌려주고 이전 값을 내지 않는다(수집기에서는 scrape 실패, Prometheus의
up= 0). /healthz는 프로세스가 응답하는지만 본다. probe는 DB를 읽지 않는다.
scrape마다 DB를 읽어 값을 집계한다. scrape 간격을 짧게 잡을수록 DB 부하가 는다. 수집 시간이 scrape 간격의 절반을 넘으면 scrape 간격을 늘리는 쪽을 검토한다.
지표
접두어는 werft_이다. 코드에서는 한 곳에서 정한다.
| 이름 | 타입 | 라벨 | 뜻 |
|---|---|---|---|
werft_sessions | gauge | state, status, admission | 세션 수. state는 아래 상태표로 정한다. status는 API가 보여 주는 상태(needs_input 포함), admission은 sessions.admission_state |
werft_turns_total | counter | status | completed·failed·interrupted·cancelled로 끝난 turn 수 |
werft_turn_duration_seconds | histogram | status, le | 위 turn 중 시작한 것의 시작부터 끝까지 걸린 시간. bucket 1, 5, 15, 30, 60, 120, 300, 600, 1800, 3600초 |
werft_turns_outcome_unknown | gauge | 없음 | 실행이 사라져 결과를 모르는 turn 수. 운영자의 복구 결정을 기다린다 |
werft_tool_calls_total | counter | tool | 엔진이 부른 도구 호출 수 |
werft_tool_calls | gauge | tool, outcome | 도구 호출 수를 지금의 결과별로. ok, failed, denied, unknown(결과가 아직 없음) |
werft_provider_tokens_total | counter | model, priced_by, kind | provider 호출에서 잰 토큰 수. kind는 input·output·cache_read·cache_creation |
werft_provider_cost_usd_total | counter | model, priced_by | provider 추정 비용(USD). Kubernetes 인프라 비용이 아니다 |
werft_launch_queue_sessions | gauge | 없음 | worker launch를 기다리는 세션 수 |
werft_launch_queue_oldest_wait_seconds | gauge | 없음 | launch 대기열에서 가장 오래 기다린 세션의 대기 시간. 비면 0 |
werft_input_queue_oldest_age_seconds | gauge | 없음 | worker가 아직 가져가지 않은 입력 중 가장 오래된 것의 나이. 비면 0 |
tool_calls가 counter가 아니라 gauge인 이유: 도구 결과는 나중에 바뀔 수 있다. 결과 이벤트가 늦게 도착하면 unknown이 ok나 failed가 되고, 더 이른 결과가 뒤에 도착하면 기존 결과를 바꾼다. counter 값이 줄면 수집기가 reset으로 읽어 증가량이 튄다. 호출 수의 증가량은 tool_calls_total로 보고, 결과별 비율은 tool_calls로 본다.
outcome_unknown turn을 turns_total에 넣지 않는 이유도 같다. 운영자가 복구하면 completed나 cancelled로 바뀌고 끝난 시각도 그때로 바뀐다. 바뀐 turn은 그때 turns_total과 histogram에 들어간다.
세션 상태표
위에서부터 처음 맞는 줄이 state다. 모든 세션이 정확히 한 줄에 들어가므로 state별 합은 전체 세션 수와 같다.
state | 조건 | 사용자에게 보이는 상태 |
|---|---|---|
ended | admission이 stopping·stopped·closing·closed, 또는 저장된 status가 stopped | 종료 |
paused | admission이 pausing·paused | 일시정지 |
recovery_required | admission이 recovery_required | — |
awaiting_approval | API status가 needs_input이고 사람이 답할 수 있는 permission 요청이 있다 | 승인 대기 |
awaiting_answer | API status가 needs_input이고 사람이 답할 수 있는 요청이 question뿐이다 | — |
running·queued·idle·failed | API status 그대로 | 실행 중은 running |
needs_input은 DB에 저장되지 않는다. 실행 중인 세션이나 turn을 멈춰 둔(parked) 세션에 사람이 답할 수 있는 요청이 있으면 읽을 때 needs_input이 된다. 지표도 같은 규칙을 쓴다. ended는 닫힌 세션이 지워지지 않으므로 계속 커진다. 지금의 부하는 ended를 뺀 합으로 본다.
도구 결과와 거절
ok·failed·unknown은tool_calls.outcome(도구 결과 블록의is_error)이다.denied는 사람이 permission 요청에deny로 답한 호출이다.pending_requests(kind = 'permission',answer.decision = 'deny')와tool_calls를(session_id, tool_use_id)로 잇는다. tool_use id는 provider가 주는 고유값이라, 멈춰 둔 요청에 다음 attempt가 답한 경우도 같은 호출로 잇는다. 같은 tool_use에deny답이 하나라도 있으면 그 뒤에 다시 허용되어 실행됐더라도denied로 센다.- 에이전트 도구 정책의
deny, 만료되거나 취소된 요청,tool_use_id를 보내지 않는 옛 worker의 거절은denied가 아니라 결과 블록대로(failed등) 센다.
비용과 토큰
- 비용은
provider_usage.cost_usd의 합이다. 이 값은 호출을 기록할 때 플랫폼 가격표가 계산했다. 지표는 단가를 다시 계산하지 않고, 단가 설정도 따로 두지 않는다. priced_by는 그 호출의 가격 근거다.table(가격표에 있는 모델),fallback(가격표에 없는 모델이라 가장 비싼 단가로 계산),unmetered(profile이billing: none이라 비용 0). 자체 호스팅 모델처럼 비용이 0인 호출은priced_by="unmetered"의 토큰 수로 사용량을 본다.- 기간 기준은 호출을 기록한 시각(
provider_usage.recorded_at)이다. 기간 비용은increase(werft_provider_cost_usd_total[1h])처럼 counter 증가량으로 본다. GET /v1/usage/summary와 다른 점이 셋 있다. 기간 안에 만들고 기간 안에 과금한 세션만 있으면 둘은 같다.- summary는 기간 안에 만든 세션(
sessions.created_at)을 골라 그 세션의 비용 전체를 더한다. 기간 전에 만든 세션이 기간 안에 쓴 비용은 지표에만 있다. - summary는
sessions.cost_usd를 더한다. 이 값은 세션 비용 상한 칸(99,999,999.999999 USD)에서 멈추지만 호출별 비용은 멈추지 않는다. - summary에는 호출 단위 기록이 생기기 전(마이그레이션 0169 이전)의 세션 비용(
session_pre_metering_cost)이 들어 있고 지표에는 없다.
- summary는 기간 안에 만든 세션(
- 토큰은
provider_usage의 호출별 값을 더한다.cache_creation은 1시간 cache 쓰기를 포함한 전체 cache 쓰기다.
대기열
- launch 대기열: scheduler가 다음 pass에 worker를 띄울 수 있는 세션이다. scheduler와 같은 SQL 조건으로 센다. launch 가능한 admission, 실행 회수·takeover 없음, 비용·토큰 상한 안, restore 재시도 시각 지남, 슬롯을 잡은 launch 없음. 상한은 scheduler와 같은
config.limits에서 읽는다. 대기 시간은unassigned_sessions.signaled_at부터 잰다. - 입력 대기열:
queue_messages중 아무 worker도 가져가지 않았고 보일 시각이 지난 입력이다. compose의werft.queue.oldest_age_seconds와 뜻이 같다.
라벨 규칙
- 세션 id, turn id, 사용자 id, API key id, 에이전트 id, attempt id는 라벨로 쓰지 않는다. 에이전트별 비용은
GET /v1/usage/*로 본다. state·status·admission·outcome·priced_by·kind·le는 정해진 값만 나온다.tool과model은 데이터에서 온다. MCP 도구mcp__<server>__<tool>은mcp__<server>로 접는다. 그다음 처음 나타난 순서로tool은 100개,model은 50개까지만 이름을 두고 나머지는other로 합친다. 행을 지우지 않으므로 이름을 얻은 값은 보통 그대로 남는다. 다만 상한이 찬 뒤 projection이 늦게 넣은 행(tool)이나 늦게 커밋된 행(model,recorded_at은 트랜잭션 시작 시각)이 더 이른 시각을 가지면 이름 하나가other와 자리를 바꿀 수 있다. 그때 밀려난 이름의 series는 끊기고, 들어온 이름은 새 series로 시작하며,other는 한 번 튀거나 reset으로 읽힌다.- 테스트: cardinality 시험이 라벨 이름 목록, id 모양 값이 없음, 상한을 확인한다.
compose 지표와의 대응
compose의 Datadog overlay는 PostgreSQL custom query로 비슷한 값을 낸다(Datadog 감시 기준, infra/datadog/conf.d/postgres.d/conf.yaml). Kubernetes 설치에서는 이 지표를 읽으면 된다.
| compose(DB query) | 이 지표 |
|---|---|
werft.queue.oldest_age_seconds | werft_input_queue_oldest_age_seconds(같은 뜻) |
werft.provider.cost_usd_1h | increase(werft_provider_cost_usd_total[1h]) |
werft.provider.unmetered_tokens_1h | increase(werft_provider_tokens_total{priced_by="unmetered"}[1h])의 합 |
werft.provider.fallback_pricing_count_1h | 대응 없음(호출 수 대신 priced_by="fallback"의 비용·토큰을 본다) |
werft.turn.outcome_unknown_count | 대응 없음. werft_turns_outcome_unknown은 최근 5분에 생긴 수가 아니라 지금 남은 수다 |
werft.checkpoint.pending_age_seconds, werft.lease.stale_count | 대응 없음 |