iop/docs/openai-usage-grafana.md
toki 9b7b4e044f refactor: openai usage metrics stream relay with grafana doc update
- Move usage metrics aggregation to stream layer in edge openai handler
- Refactor stream.go to relay usage metrics from node responses
- Update grafana doc with new metric structure and dashboard notes
- Remove completed G05 plan/code-review artifacts
- Archive closed task artifacts
2026-07-14 18:57:43 +09:00

412 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# OpenAI-compatible Usage Grafana Query Guide
> **Purpose**: 이 문서는 운영자가 Prometheus metric을 Grafana 패널로 조회해 사용자별 OpenAI-compatible token 사용량, 사용 출처, cloud-equivalent cost, avoided-cost ROI 판단 기준을 볼 수 있도록 한다. Control Plane/Client dashboard, request-level ledger, live cloud pricing sync, billing/chargeback, 사용자별 제한 enforcement는 만들지 않는다.
---
## Metric 목록
### 1. `iop_openai_usage_tokens_total` (Counter)
Provider-reported OpenAI-compatible token usage by token type.
| Label | Value 예시 | 설명 |
|-------|-----------|------|
| `edge_id` | `edge-1` | Edge instance identity |
| `principal_ref` | `usr-abc123` | Principal foreign-key-like reference |
| `principal_alias` | `john@acme` | Human-readable alias |
| `token_ref` | `tok-xyz789` | Token identity (not a raw secret) |
| `model_group` | `gpt-4o` | Model group identifier |
| `endpoint` | `chat.completions`, `responses` | OpenAI-compatible route |
| `response_mode` | `passthrough`, `normalized` | Internal execution label; callers cannot set it via OpenAI metadata |
| `token_type` | `input`, `output`, `reasoning`, `cached_input` | Token type |
### 2. `iop_openai_requests_total` (Counter)
OpenAI-compatible requests processed by terminal status and usage source.
| Label | Value 예시 | 설명 |
|-------|-----------|------|
| `edge_id` | `edge-1` | Edge instance identity |
| `principal_ref` | `usr-abc123` | Principal reference |
| `principal_alias` | `john@acme` | Human-readable alias |
| `token_ref` | `tok-xyz789` | Token identity |
| `model_group` | `gpt-4o` | Model group identifier |
| `endpoint` | `chat.completions`, `responses` | OpenAI-compatible route |
| `response_mode` | `passthrough`, `normalized` | Internal execution label; callers cannot set it via OpenAI metadata |
| `status` | `success`, `error`, `cancel` | Terminal request status |
| `usage_source` | `provider_reported`, `unavailable` | Whether at least one token type was reported |
### 3. `iop_openai_reasoning_observed_total` (Counter)
Requests where reasoning text was observed but the provider did not report reasoning token usage.
Same labels as `iop_openai_requests_total` excluding `status` and `usage_source`: `edge_id`, `principal_ref`, `principal_alias`, `token_ref`, `model_group`, `endpoint`, `response_mode`.
### 4. `iop_openai_reasoning_chars_total` (Counter)
Observed reasoning text characters for requests without provider-reported reasoning tokens. The token estimate is emitted separately via `iop_openai_reasoning_estimated_tokens_total`; provider-reported `token_type="reasoning"` remains authoritative.
### 5. `iop_openai_reasoning_estimated_tokens_total` (Counter)
Estimated reasoning tokens for requests with observed reasoning text but without provider-reported reasoning token usage. Calculated as `ceil(chars / 4)` with `estimation_method="chars_div_4"`. This is an operator-grade observation estimate only, never billing-grade or chargeback-confirmed.
---
## Label Allowlist & Forbidden Labels
### 허용 label
```
edge_id, principal_ref, principal_alias, token_ref,
model_group, endpoint, response_mode, status,
token_type, usage_source, estimation_method
```
`estimation_method``iop_openai_reasoning_estimated_tokens_total` 전용 보조 라벨이며, 그 외 counter에는 사용되지 않습니다.
### 금지 label
- `request_id`, `session_id` — request-level 상세는 후속 ledger/Loki 축에서 다룬다.
- Bearer token, provider API key, raw payload, raw prompt/response text — secret 누출 방지 (SDD S08).
- 그 외 임의 label — cardinality 폭발을 막는다.
---
## Principal / Token Attribution 전제
- `principal_ref`는 사용자/테넌트/외부 운영 시스템의 안정 참조값이다.
- `principal_alias`는 Grafana legend와 table에 보여줄 낮은 cardinality 별칭이다.
- `token_ref`는 raw bearer token이 아니라 앱/통합/용도별 token 참조값이다.
- 같은 `principal_ref` 아래 여러 `token_ref`를 둘 수 있다. 이 경우 `principal_ref` 기준 query는 사용자 합산, `token_ref` 기준 query는 앱/통합별 breakdown으로 본다.
- 운영 token 발급은 raw token을 tracked 파일에 남기지 않고 hash/reference만 기록하는 절차를 따른다.
관련 기준:
- `agent-contract/outer/openai-compatible-api.md`: OpenAI-compatible principal token auth 계약
- `agent-contract/inner/edge-config-runtime-refresh.md`: `openai.principal_tokens[]` config 계약
- `agent-ops/skills/project/openai-usage-token-issue/SKILL.md`: 운영 token 발급 절차
- `apps/edge/internal/openai/identity_metering_test.go`: 같은 principal의 여러 token resolution 테스트
- `packages/go/config/config_test.go`: 같은 principal의 여러 `principal_tokens[]` config load 테스트
---
## PromQL Examples
### 기본: 사용자별 사용량 (principal_alias 기준)
```promql
# 입력 토큰 합계
sum by (principal_alias) (iop_openai_usage_tokens_total{token_type="input"})
# 출력 토큰 합계
sum by (principal_alias) (iop_openai_usage_tokens_total{token_type="output"})
```
### principal_ref별 usage breakdown
```promql
# principal_ref, token_type별 aggregation
sum by (principal_ref, token_type) (iop_openai_usage_tokens_total)
```
### token_ref별 사용량
```promql
# token_ref별 input/output 합계
sum by (token_ref, token_type) (iop_openai_usage_tokens_total)
```
### 사용자 합산 + 앱/통합별 breakdown
```promql
# 같은 principal_ref의 전체 사용량
sum by (principal_ref, principal_alias, token_type) (
iop_openai_usage_tokens_total
)
# 같은 principal_ref 안에서 token_ref별 사용량
sum by (principal_ref, principal_alias, token_ref, token_type) (
iop_openai_usage_tokens_total
)
```
### model_group별 usage
```promql
# model_group, token_type별 aggregation
sum by (model_group, token_type) (iop_openai_usage_tokens_total)
```
### endpoint별 요청 수
```promql
# endpoint, status별 요청 수
sum by (endpoint, status) (iop_openai_requests_total)
```
### response_mode별 분석
`response_mode`는 handler 실행 경로에서 파생되는 내부 라벨이다. provider 라우트의 raw tunnel 패스스루는 `passthrough`, 정규화된 RunEvent 경로는 `normalized`로 기록된다. caller가 OpenAI metadata로 설정할 수 있는 값이 아니며 API selector가 아니다.
```promql
# response_mode별 성공 요청 수
sum by (response_mode, status) (iop_openai_requests_total{status="success"})
```
### usage origin breakdown
```promql
# "어디서 얼만큼 사용했는지"를 보는 table용 breakdown
sum by (principal_alias, token_ref, model_group, endpoint, response_mode, token_type) (
iop_openai_usage_tokens_total
)
```
### token_type별 사용량 전체
```promql
# 모든 token_type별 합계 (time range 그래프용)
sum by (token_type) (irate(iop_openai_usage_tokens_total[5m]))
```
### usage_source별 요청 coverage
```promql
# provider_reported vs unavailable 비율
sum by (usage_source) (iop_openai_requests_total)
```
### reasoning 보조 metric
provider가 reasoning token을 보고하지 않는 경우:
```promql
# reasoning text가 관찰된 요청 수 (provider token 미보고)
sum by (principal_alias) (iop_openai_reasoning_observed_total)
# reasoning text character 합계
sum by (principal_alias) (iop_openai_reasoning_chars_total)
# estimated reasoning tokens (provider가 token 수를 보고하지 않는 경우)
sum by (principal_alias, estimation_method) (iop_openai_reasoning_estimated_tokens_total)
```
> **주의**: `iop_openai_reasoning_observed_total` / `iop_openai_reasoning_chars_total` / `iop_openai_reasoning_estimated_tokens_total`는 provider-reported reasoning token이 있을 경우 emit되지 않는다. 즉, 이 metric들은 "reasoning text는 있으나 token count 보고를 안 하는 provider"만 커버한다. `iop_openai_reasoning_estimated_tokens_total`은 `estimation_method="chars_div_4"`로 ceil(chars/4) 추정을 제공하지만 billing-grade 정산값이 아니다.
---
## Dashboard Panel 후보
| 패널 | query | visualization |
|------|-------|---------------|
| 사용자별 token 사용량 (line) | `sum by (principal_alias, token_type) (irate(iop_openai_usage_tokens_total[5m]))` | Time series, legend=`{{principal_alias}} {{token_type}}` |
| 일별 usage origin (table) | `sum by (principal_alias, token_ref, model_group, endpoint, response_mode, token_type) (increase(iop_openai_usage_tokens_total[1d]))` | Table |
| model_group별 token 비율 (pie) | `sum by (model_group, token_type) (iop_openai_usage_tokens_total)` | Stat or bar chart |
| 요청 상태 분포 (table) | `sum by (endpoint, status, usage_source) (iop_openai_requests_total)` | Table |
| cloud-equivalent cost (stat/table) | token rollup query에 price scalar 또는 Grafana calculated field 적용 | Stat or Table |
| avoided-cost ROI summary (stat) | cloud-equivalent cost 합계, 필요 시 별도 infra cost field 차감 | Stat |
| reasoning 보조 지표 (singlestat) | `sum(iop_openai_reasoning_observed_total)` | Singlestat |
| estimated reasoning tokens (bar) | `sum by (estimation_method) (iop_openai_reasoning_estimated_tokens_total)` | Bar chart |
| estimated reasoning vs provider-reported (line) | `sum by (estimation_method) (iop_openai_reasoning_estimated_tokens_total)` vs `sum by (principal_alias) (iop_openai_usage_tokens_total{token_type="reasoning"})` | Time series |
> Grafana JSON dashboard import file은 본 scope에 포함되지 않는다. 위 PromQL을 바탕으로 패널을 수동 구성한다.
---
## daily / monthly Rollup
### 일일 토큰 합계
```promql
# 일일 token rollup (principal/token/model/endpoint/response_mode/token_type별)
sum by (principal_ref, principal_alias, token_ref, model_group, endpoint, response_mode, token_type) (
increase(iop_openai_usage_tokens_total[1d])
)
```
> Grafana dashboard에서 `by time` aggregation으로 대체 가능:
> ```promql
> # Grafana 범용 aggregation (PromQL 함수 아님)
> # time range를 1일로 설정 후 sum by (principal_alias, token_type)
> iop_openai_usage_tokens_total
> ```
### 월간 토큰 합계
```promql
# 최근 30일 token rollup. 달력 월은 Grafana time range를 월 단위로 잡고 $__range를 사용한다.
sum by (principal_ref, principal_alias, token_ref, model_group, endpoint, response_mode, token_type) (
increase(iop_openai_usage_tokens_total[30d])
)
```
> Grafana dashboard에서 범용 aggregation으로 대체 가능:
> ```promql
> # Grafana 범용 aggregation (PromQL 함수 아님)
> # time range를 30일로 설정 후 sum by (principal_alias, token_type)
> iop_openai_usage_tokens_total
> ```
### 일일/월간 rollup 활용 시나리오
- `principal_alias` 또는 `principal_ref``by` clause에 추가해 사용자별 일일/월간 토큰 사용량을 산출한다.
- `token_ref`를 추가하면 같은 사용자 아래 앱/통합/용도별 사용량을 분해한다.
- `token_type`을 함께 group by하면 input/output/reasoning/cached_input을 구분한다.
- `model_group`, `endpoint`, `response_mode`를 추가하면 사용 위치와 응답 경로별 origin breakdown을 만든다.
---
## Cloud Price Baseline
가격은 런타임이 자동으로 가져오지 않는다. 운영자가 기준일과 기준 모델을 명시한 정적 baseline을 Grafana dashboard 변수, annotation, table panel, 또는 운영 문서에 둔다.
최소 필드:
| Field | 예시 | 설명 |
|-------|------|------|
| `cloud_provider` | `example-cloud` | 가격 기준 provider |
| `baseline_model` | `example-model` | 비교 기준 cloud model |
| `model_group` | `example-model` | IOP metric의 `model_group`과 매핑할 값 |
| `token_type` | `input`, `output`, `cached_input`, `reasoning` | 가격을 적용할 token type |
| `price_per_1m_tokens` | `1.00` | 1M tokens당 단가 |
| `currency` | `USD` | 통화 |
| `effective_date` | `2026-07-10` | 가격 기준일 |
| `source_note` | `operator baseline` | 운영자가 남기는 기준 설명 |
baseline 예시. 아래 숫자는 문서용 예시이며 최신 cloud 가격이 아니다.
| cloud_provider | baseline_model | model_group | token_type | price_per_1m_tokens | currency | effective_date |
|----------------|----------------|-------------|------------|----------------------|----------|----------------|
| example-cloud | example-model | example-model | input | 1.00 | USD | 2026-07-10 |
| example-cloud | example-model | example-model | output | 3.00 | USD | 2026-07-10 |
| example-cloud | example-model | example-model | cached_input | 0.50 | USD | 2026-07-10 |
`reasoning` token은 provider가 별도 단가를 제공하면 별도 baseline을 둔다. 별도 단가가 없으면 운영자가 `output`과 같은 단가로 볼지, report에서 `reasoning`을 제외할지 baseline에 명시한다.
> `iop_openai_reasoning_estimated_tokens_total`은 operator observation용 추정치이며, cloud-equivalent cost에 합산하거나 chargeback에 사용하지 않는다. billing-grade reasoning cost는 provider-reported `iop_openai_usage_tokens_total{token_type="reasoning"}`에 한한다.
---
## Cost Formula
기본 산식:
```text
cloud_equivalent_cost = (tokens / 1_000_000) * price_per_1m_tokens
daily_cloud_equivalent_cost = sum(cloud_equivalent_cost by day, principal/token/model/endpoint/token_type)
monthly_cloud_equivalent_cost = sum(cloud_equivalent_cost over selected month)
```
Prometheus metric 자체에는 price table이 없으므로 1차 표면에서는 다음 중 하나를 사용한다.
- Grafana table transformation으로 token rollup 결과에 static price column을 붙이고 calculated field를 만든다.
- model/token_type별 panel query에 운영자가 관리하는 scalar price를 곱한다.
- Prometheus recording rule을 운영자가 별도 관리한다. 이 repo는 recording rule 파일을 만들지 않는다.
PromQL scalar 예시:
```promql
# example-model input daily cloud-equivalent cost, USD
sum by (principal_alias, token_ref, model_group, endpoint) (
increase(iop_openai_usage_tokens_total{model_group="example-model", token_type="input"}[1d])
) / 1000000 * 1.00
# example-model output daily cloud-equivalent cost, USD
sum by (principal_alias, token_ref, model_group, endpoint) (
increase(iop_openai_usage_tokens_total{model_group="example-model", token_type="output"}[1d])
) / 1000000 * 3.00
# example-model cached input daily cloud-equivalent cost, USD
sum by (principal_alias, token_ref, model_group, endpoint) (
increase(iop_openai_usage_tokens_total{model_group="example-model", token_type="cached_input"}[1d])
) / 1000000 * 0.50
```
여러 token type을 합산할 때는 token type별 cost query를 Grafana transformation으로 더한다. 단일 query에서 서로 다른 가격을 자동 join하려면 별도 recording rule 또는 external price table이 필요하다.
---
## ROI / Avoided-Cost Summary
이 MVP의 ROI는 billing-grade 정산이 아니라 cloud-equivalent avoided cost다.
```text
cloud_equivalent_cost = cloud baseline으로 환산한 사용량 비용
avoided_cost = IOP/local/provider에서 처리한 사용량의 cloud_equivalent_cost
net_avoided_cost = cloud_equivalent_cost - separately_known_local_infra_cost
```
- local infra cost가 별도로 계상되지 않으면 `avoided_cost`만 표시한다.
- local infra cost를 운영자가 별도 산정해 Grafana에 넣을 수 있으면 `net_avoided_cost`를 보조 field로 둔다.
- 이 값은 실제 결제, chargeback, 조직별 비용 배부, full infra cost accounting 근거가 아니다.
- 최소 summary table column은 `date`, `principal_alias`, `token_ref`, `model_group`, `endpoint`, `tokens`, `currency`, `cloud_equivalent_cost`, `avoided_cost`다.
Grafana table 구성 예:
1. Query A: daily token rollup by `principal_alias`, `token_ref`, `model_group`, `endpoint`, `token_type`.
2. Transformation: baseline price table 또는 panel-level scalar를 token type별로 적용한다.
3. Calculated field: `tokens / 1000000 * price_per_1m_tokens`.
4. Group by: `date`, `principal_alias`, `token_ref`, `model_group`, `endpoint`.
5. Reduce: `cloud_equivalent_cost` 합계와 token 합계를 보여준다.
---
## Grafana Report 절차
1. Grafana time range를 `Today`, `Yesterday`, 최근 7일, 또는 이번 달로 설정한다.
2. `daily / monthly Rollup` query로 token usage table을 만든다.
3. `principal_ref`/`principal_alias` 기준 panel과 `token_ref` 기준 panel을 나란히 둔다.
4. `model_group`, `endpoint`, `response_mode`, `token_type` column을 유지해 usage origin을 확인한다.
5. 운영 price baseline의 `effective_date`, `baseline_model`, `currency`를 dashboard text 또는 table에 함께 표시한다.
6. token type별 cost formula를 적용해 cloud-equivalent cost를 계산한다.
7. daily/monthly `avoided_cost` summary stat을 추가한다.
8. `usage_source="unavailable"` 요청 비율과 reasoning 보조 metric을 함께 확인해 cost report coverage 위험을 판단한다.
---
## Limit Follow-up: 경계 명시
### 후속 Milestone으로 남기는 항목
- 사용자별 daily/monthly token **warn** 또는 **reject** enforcement는 이 Milestone의 범위가 아니며 후속 Milestone으로 Planned된다.
- 실제 결제, chargeback, 조직별 비용 배부, full infra cost accounting은 후속 범위다.
- live cloud pricing API 연동 또는 최신 가격 자동 동기화는 후속 범위다.
- request-level ledger / audit log는 Loki/queriable log축 후속 작업의 대상이다.
### 현재 문서가 제공하는 것
- `principal_ref`, `principal_alias`, `token_ref`, `model_group`, `endpoint`, `response_mode`, `token_type` 라벨 기반 daily/monthly rollup PromQL
- 같은 principal의 여러 token/app usage를 합산과 breakdown으로 함께 보는 기준
- 운영자가 관리하는 cloud price baseline 필드
- token type별 cloud-equivalent cost 산식과 query/report 예시
- avoided-cost 수준의 ROI summary 기준
- provider reasoning report 누락 시 `reasoning_observed_total` / `reasoning_chars_total`로 보조 확인, `reasoning_estimated_tokens_total`로 chars/4 token 추정치 제공 (billing-grade 아님)
이 rollup 기준은 후속 enforce Milestone에서 Prometheus query를 그대로 재사용할 수 있도록 설계되었다.
---
## Quick Reference: Label Values
| Label | 허용 값 |
|-------|---------|
| `endpoint` | `chat.completions`, `responses` |
| `status` | `success`, `error`, `cancel` |
| `usage_source` | `provider_reported`, `unavailable` |
| `token_type` | `input`, `output`, `reasoning`, `cached_input` |
| `response_mode` | `passthrough`, `normalized` |
| `estimation_method` | `chars_div_4` (특히 `iop_openai_reasoning_estimated_tokens_total` 전용) |
---
## Appendix: Grafana Scrape Target
Compose 관측 스택에서는 Prometheus가 내부 Docker DNS로 `control-plane:9093`을 scrape하고, Grafana datasource는 `http://prometheus:9090`을 사용한다. dev profile은 기존 native Edge endpoint를 바꾸지 않기 위해 `host.docker.internal:19101`도 scrape한다.
Host publish 기본 포트:
- local/test: Prometheus `19110`, Grafana `19120`, Control Plane metrics `19100`, Edge metrics `19092`
- dev: Prometheus `19111`, Grafana `19121`, Control Plane metrics `19103`, Edge metrics `19101`
- dev-corp compose: Prometheus `19112`, Grafana `19122`, Control Plane metrics `19104`, Edge metrics `19105`
이 관측 스택은 기존 테스트 사용자-facing endpoint를 변경하지 않는다. dev 기준 기존 UI/Control Plane/Edge OpenAI-compatible 접속점은 유지하며, Grafana/Prometheus/metrics 포트만 운영자용으로 추가한다. dev Grafana/Prometheus는 기본적으로 원격 runner 내부 또는 SSH 터널 접근 기준이다. dev-corp는 현재 접근 가능한 runner가 없을 때 문서 정의만 갱신하고 실제 compose 세팅은 별도 세션에서 수행한다.