Skip to content

feat(console): filter usage and logs by API key - #434

Merged
caigee-cmd merged 3 commits into
caigee-cmd:mainfrom
NaimioChan:usage-and-logs-by-key
Oct 29, 2026
Merged

caigee-cmd merged 3 commits into
caigee-cmd:mainfrom
NaimioChan:usage-and-logs-by-key

Conversation

@NaimioChan

Copy link
Copy Markdown
Contributor

Summary

Request logs now record which named console API key issued each request, and the four aggregates behind the Logs and Usage pages accept an api_key filter.

request
  auth.Authenticate            → Identity{Kind: key, KeyID: key_xxx}
  executor.Prepare
    Logs.Start(APIKeyID)       → request_logs.api_key_id
  gateway.finishRequestLog     → api_key_id = COALESCE(NULLIF(?, ''), api_key_id)

console read paths
  GET /api/logs/requests?api_key=   → buildRequestLogWhere   (row filter)
  GET /api/logs/stats?api_key=       → RequestStatsOptions.APIKeyID
  GET /api/logs/usage?api_key=       → SummarizeRequestUsage(apiKeyID)
  GET /api/usage/cost?api_key=       → SummarizeModelCosts(apiKeyID)

Because every aggregate funnels through buildRequestLogWhere, a key filter and an unfiltered window stay consistent with each other and with the logs drill-down.

 internal/
 ├── store/
 │   ├── migrations.go        # 025 adds api_key_id TEXT NOT NULL DEFAULT '' + index
 │   └── request_logs.go      # column in insert/update/select, filter clause, cost aggregate
 ├── executor/prepare.go      # IdentityAPIKeyID(identity) written at request start
 ├── logs/recorder.go         # APIKeyID threaded through Stats/Usage/CostBreakdown + cache keys
 └── console/
     ├── logs.go              # ?api_key on requests / stats / usage
     └── usage.go             # ?api_key on cost
 frontend/src/
 ├── hooks/useApiKeyOptions.ts        # key list → filter options (new)
 ├── pages/LogsPage.tsx               # key filter, 密钥 column, detail field
 └── pages/UsagePage.tsx              # one key picker above both tabs

Two decisions worth calling out:

  • Console-key traffic leaves api_key_id empty instead of inventing a sentinel. A filter is always a named key, so "all keys" is the only view that includes console traffic.
  • UPDATE request_logs uses api_key_id = COALESCE(NULLIF(?, ''), api_key_id) so a finish-write that omits the key cannot erase what the start-write recorded.

Evidence

Before: request rows carried no key attribution, and the only key-scoped view was the whole console.

After: end-to-end test in internal/api/logs_api_key_test.go creates a key, sends one keyed and one console-key request through the real handler, then asserts all five read paths:

$ go test ./internal/api -run TestAPIKeyAttributionAndFilters -v
=== RUN   TestAPIKeyAttributionAndFilters
--- PASS: TestAPIKeyAttributionAndFilters (0.45s)
PASS

Live check on a throwaway instance (2 keys, 4 seeded rows) — Logs page goes 1–4 / 4 → 1–1 / 1 when client-a is selected, URL carries api_key=key_0fad…; Usage 统计 goes 4 requests / 298k tokens → 1 / 62k, and the 额度 tab shows 0.40 credits for the same key.

Full suite (golang:1.25):

$ go test ./...   → all ok (api, store, runtime, gateway, logs, providers, ...)
$ go vet ./...    → clean
$ cd frontend && npm run lint && npx tsc -b   → no errors
$ npm run sync    → internal/webui/static rebuilt

On a Windows host a handful of internal/api and internal/store tests fail on TempDir cleanup (SQLite file still open) and on a 0600 file-mode assertion; both reproduce on unmodified main and pass under Linux.

Merge Danger

Door: two-way — reverting drops the column from console reads; the migration row stays applied but is inert.

Blast Radius: request-log schema

Migration 025 only adds a defaulted column plus an index, so existing databases migrate in place. Rows written before this PR report an empty api_key_id and are only visible in the unfiltered view — they are not backfillable, because the key was never recorded. Old Stats/Usage cache entries are unaffected since api_key_id is now part of the cache key.


本 PR 由 AI 助手(Hermes)代发,内容已由本人在本地跑通并 review。

request_logs now records which named console API key issued each request
(migration 025 adds api_key_id). Console-key traffic stays unattributed, so
the key filter still sees it as one bucket.

The four aggregates behind the two pages take an api_key filter:
GET /api/logs/requests, /api/logs/stats, /api/logs/usage and
/api/usage/cost. Stats and usage cache keys include it so a per-key window
never serves another key's numbers.

Logs gets a key filter, a 密钥 column and the key in the request detail;
Usage gets one key picker above both tabs, so 统计 and 额度 stay in sync.

Agent: hermes
Verify: go test ./... (golang:1.25 container) + go vet + npm run lint + tsc -b + browser check of /logs and /usage
internal/webui/static was built against npm-installed semver-maximum packages (vite 8.3.5 / rolldown 1.2.15 / @heroui 3.2.7), not the versions pinned in frontend/package-lock.json (8.2.2 / 1.2.5 / 3.2.4). Vite hashes asset filenames by content, so CI's npm-ci build produced different names and the static-is-up-to-date check failed.

Rebuilt in node:22 with npm ci + npm run sync so the committed assets match what CI regenerates.

Agent: hermes

Verify: npm ci + npm run sync in node:22 container
@caigee-cmd

Copy link
Copy Markdown
Owner

静态资源这轮对了,frontend 已经绿,谢谢。

还有两处筛选对不齐,合之前需要补上:

  1. 用量页从某一行点进日志时,logsHref 没有带上当前的 api_key。筛选状态下点进去会看到全部密钥的请求,和上面的数字对不上。
  2. 日志页的 api_key 只传给了请求列表,没传给 GET /api/logs/stats。选中一把密钥后,上面的汇总仍是全部流量。

另外两个小的,不挡合并:用量页的密钥选择没有进 URL,刷新会丢;GET /api/logs/requests 的 api_key 没有 TrimSpace,stats / usage 有。

Review follow-up.

Usage -> Logs drill-down dropped the active key filter, so a row that showed
one key's numbers opened a list of every key's requests. All three call sites
now go through openLogsFor, which merges api_key and row.filter.

The Usage key picker also lived only in component state, so a reload lost it.
It now mirrors ?api_key like the Logs page does.

GET /api/logs/requests trimmed its api_key while stats and usage did not;
aligned it with strings.TrimSpace.

Agent: hermes
Verify: go build + go test internal/console,internal/store + tsc -b + npm lint + browser check of drill-down and reload
@NaimioChan

Copy link
Copy Markdown
Contributor Author

@caigee-cmd 谢谢 review,前三条已修(d3e007d)。

1. 用量页 → 日志的 api_key 丢失 已修。三处跳转(移动端卡片点击、键盘 Enter/Space、桌面表格 onAction)现在都走同一个 openLogsFor(row),把 api_key 和 row.filter 合并后再拼 logsHref。实测:在用量页选 alpha 后点某一行,落地 URL 是

/logs?api_key=key_518e78f603cc&model=glm-5.2&from=…&to=…

日志页的密钥选择器回显 alpha,结果 1–1 / 1,与用量页数字一致。

2. 用量页密钥选择没进 URL 已修。只镜像 api_key(?api_key=…,replace: true),窗口和维度仍是页面状态。刷新后筛选保持。

3. /api/logs/requests 的 api_key 缺 TrimSpace 已修,与 stats / usage 对齐。

关于第 2 条(日志页汇总),我这边对不上,想跟你确认位置。

当前分支和 upstream/main 的 LogsPage.tsx 都只调用两个接口:fetchRequestLogs 和 fetchRequestLog,从没调用过 fetchRequestStats;页面上也没有独立汇总卡片,只有前端按分页 total 算出的 1–4 / 4 条目计数。我在浏览器里拦截了 window.fetch,切换密钥时只发出一次 GET /api/logs/requests?api_key=…,没有任何 stats 请求。

所以"选中一把密钥后上面的汇总仍是全部流量"这个现象我在当前版本复现不到。有两种可能:

  • 你说的是概览页或用量页的汇总(那两处 fetchRequestStats 已经带 api_key);
  • 或者你看到的是另一个分支/未合并的改动。

如果确实是日志页该有一块汇总而我漏了,麻烦指出是哪个元素或给我个截图,我补上。我不想凭猜去新加一块 UI。


本 PR 由 AI 助手(Hermes)代发,内容已由本人在本地跑通并 review。

@caigee-cmd

Copy link
Copy Markdown
Owner

对,第 2 条是我看错了页面,收回。

日志页和 main 一样,只拉 GET /api/logs/requests,没有单独的汇总卡片。页头的 1–n / total 来自这个列表接口的 total,而这个请求已经带了 api_key,所以选中一把密钥后这个数字会跟着变。fetchRequestStats 在概览和用量页,不在日志页。

另外三处我按 d3e007d 对过,都对上了:

  • 用量页三处跳转都走 openLogsFor,api_key 和行上的 filter 一起进 logsHref。
  • 用量页只把 api_key 写进 URL,刷新还在。
  • GET /api/logs/requests 的 api_key 现在有 TrimSpace。

还剩一个同类的小问题,不挡合并:GET /api/usage/cost 仍是 r.URL.Query().Get("api_key"),没有 TrimSpace。另外三个读接口都 trim 了。

CI 这轮还在跑,过了就可以合。

@caigee-cmd
caigee-cmd merged commit 2da8c45 into caigee-cmd:main Oct 29, 2026
6 checks passed
@NaimioChan
NaimioChan deleted the usage-and-logs-by-key branch October 29, 2026 07:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants