English | 中文
api-log 是一个面向 LLM 网关可观测性的透明 HTTP 录制 proxy。它部署在 sub2api、CLIProxyAPI(CPA)、new-api 等 OpenAI 兼容网关前面,原样转发流量,捕获 OpenAI Chat、Anthropic Messages、Responses、Gemini 等协议。每条完成的请求/响应 trace 以 append-only JSONL 落盘,并构建 SQLite 索引,用于本地检索、重放和按 key 排查问题。
api-log is a transparent HTTP recording proxy for LLM gateway observability. It sits in front of sub2api, CLIProxyAPI (CPA), new-api, or any OpenAI-compatible gateway and captures OpenAI Chat, Anthropic Messages, Responses, and Gemini traffic. Each completed request/response is written as append-only JSONL with a SQLite index for local search, replay, and per-key analysis.
转发 goroutine 不解析任何 body。JSON 反序列化、SSE 事件切分、session 推断都在响应回写给客户端之后的 finalize 阶段进行。Token 计费、评测流水线、以及任何语义层面的解释都在本项目范围之外。
配套前端: api-log-viewer —— api-log trace 的 Svelte 5 SPA 前端;可由后端 /viewer/ 托管,也可放在 Caddy/nginx 后与 read API 同源部署。
正在准备 v0.1.0 tag。capture 路径、read API、viewer(独立仓库)、以及 plugin 系统(Phase A observer + Phase B/C mutator)均已落地并跑在真实流量上。HTTP read API 契约已稳定;tag 之前的 commit 仍可能 rebase。
磁盘格式与 read API 契约见 ARCHITECTURE.md,排期见 ROADMAP.md。
| 工具 | 是什么 | 为什么 api-log 不一样 |
|---|---|---|
| Helicone | 网关 / 可观测性整套方案 | api-log 只做透明录制;不路由、不计费、不鉴权,也不是托管服务。 |
| Langfuse | 应用层 LLM tracing 平台 | api-log 在网关边界抓 HTTP 流量,无需 SDK 埋点。 |
| Phoenix | 评测 / tracing / 可观测性工具集 | api-log 先把原始网关 trace 录下来;评测流水线是下游环节。 |
| LangSmith | LangChain 的 tracing / 评测平台 | api-log 与框架无关,本地落 JSONL + SQLite。 |
| mitmproxy | 通用交互式 proxy | api-log 理解 LLM 的 JSON/SSE envelope,写结构化 trace。 |
api-log 不是网关。它不鉴权、不路由、不重试、不限流、不缓存、不改写——这些都留给上游网关。
# docker-compose.yml — added alongside your existing gateway
services:
gateway: # CPA / sub2api / new-api / your stack
# ... existing config ...
expose: ["7860"] # move 7860 from "ports" to "expose"
api-log:
image: ghcr.io/2nd1st/api-log:latest
ports:
- "7861:7861" # proxy listener (clients connect here)
- "7862:7862" # read API
environment:
APILOG_PROXY_UPSTREAM: http://gateway:7860
volumes:
- ./api-log-data:/datadocker compose up -ddeploy/ 下有三套参考栈——dev-stack/(api-log + mock LLM 网关,无需真实上游)、demo/(api-log 挡在 sub2api 前面)、bench/(api-log 单跑,上游 URL 通过 env 传入)。想 5 分钟跑通就用 deploy/dev-stack/;tests/integration/run.sh 也是跑这套。
跑 sub2api / CLIProxyAPI / new-api 的运维在 homelab 机器或小 VPS 上,可以直接装 binary。
下载 release binary(不需要 Go 工具链)—— 从 最新 release 挑对应 OS + arch 的 archive。Linux / darwin / windows × amd64 / arm64 都已预编译(windows/arm64 后续补)。Linux/amd64 示例:
VERSION=v0.1.1
curl -L https://github.com/2nd1st/api-log/releases/download/${VERSION}/api-log_${VERSION}_linux_amd64.tar.gz | tar xz
./api-log -version或者从源码 build(需要 Go 1.22+):
go install github.com/2nd1st/api-log/cmd/api-log@latest
api-log -version完整流程——service 用户、env 文件、data 目录、带 hardening 默认值的 systemd unit——见 deploy/systemd/。Caddy + nginx 把 TLS 套在 read API + viewer SPA 同一个域名上的示例见 deploy/reverse-proxy/。
把客户端的 base_url 从网关端口(:7860)改到 api-log 的 proxy listener(:7861)。客户端其他配置都不动。
# 1. Liveness — unauthenticated, k8s probe compatible
curl -s http://localhost:7862/healthz | jq .
# 2. Send one request through the proxy (replace with a real client call)
curl -s http://localhost:7861/v1/messages \
-H "x-api-key: $UPSTREAM_KEY" \
-H "anthropic-version: 2023-06-01" \
-d '{"model":"claude-sonnet-4-6","max_tokens":32,"messages":[{"role":"user","content":"hi"}]}'
# 3. Read the auto-generated admin bearer
TOKEN=$(cat ./api-log-data/admin_token)
# 4. List recent traces from the read API
curl -s -H "Authorization: Bearer $TOKEN" \
'http://localhost:7862/api/traces?limit=5' | jq '.traces[] | {id, path, status, model}'admin bearer 在首次启动时生成,写入 data/admin_token。删掉该文件并重启即可轮换。
api-log 的配置按三层解析,优先级从低到高:内建默认值、-config 传入的 YAML 文件、APILOG_* 环境变量。环境变量总是最终胜出。
完整的参考——每个 YAML key、默认值、对应的 env 覆盖——见仓库根目录的 api-log.example.yaml。把这个文件复制一份,只反注释你要改的 key,然后用 api-log -config /etc/api-log/api-log.yaml 启动。canonical 结构在 internal/config/config.go。
运维最常覆盖的几个 env:
| Env var | 含义 |
|---|---|
APILOG_PROXY_LISTEN |
客户端连接的 proxy listener 的 地址:端口。默认 :7861。 |
APILOG_PROXY_UPSTREAM |
api-log 转发的上游网关 URL。默认 http://localhost:7860。 |
APILOG_STORAGE_DATA_DIR |
JSONL 文件、SQLite 索引、admin_token、runtime_overrides.json 的根目录。默认 ./data。 |
APILOG_LOGGING_LEVEL |
slog 级别(debug | info | warn | error)。默认 info。 |
APILOG_API_EXPORT_BYTE_HARDCAP |
/api/export 的字节上限,超过则需要 ?bytes_all=1。0(或未设置)使用内建默认 2 GiB;正值覆盖;负值禁用该上限。 |
插件的 pattern 列表(plugins.path_filter.patterns、plugins.capture_filter.patterns)和 mutator 插件实例(text-replace、text-append)没有 env 覆盖——前两者只能在 YAML 里配,mutator 走 runtime_overrides.json,通过 PUT /api/config/plugins 管理。
client(s) → api-log → CPA / sub2api / new-api / any OpenAI-compatible gateway → upstream
↓
data/<date>/<key_hash>.jsonl (append-only, source of truth)
data/index.sqlite (derived index, rebuildable)
proxy listener 接受 plain HTTP。转发用 httputil.ReverseProxy 加自定义 Transport,把请求与响应 body 同时 tee 到 per-trace 临时文件。响应结束(成功、客户端断连、或上游报错)后,finalize 阶段把 body 解析成 JSONL 行,写入当天的 JSONL 文件,并把索引列插入 SQLite。完整写路径见 ARCHITECTURE § 7。
两层,权威次序严格:
data/<date>/<key_hash>.jsonl—— 每条完成 trace 一行。按客户端 key 分文件、按日分目录,append-only。每行携带完整 HTTP 事务(请求 header + body,响应 header + body 或流式的events[],时间戳、size、截断标志)。data/index.sqlite—— read API 需要的派生列(status、model、token 数、session 关联、jsonl_path+jsonl_offset)。可删;秒级从第一层重建。WAL 模式,连接池 10(v0.1.1)。
当 JSONL 与 SQLite 不一致时,JSONL 为准。
默认关闭。通过 PUT /api/config/retention 开启(写入 runtime_overrides.json,重启保留):
curl -X PUT -H "Authorization: Bearer $ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"max_bytes":10000000000,"max_age_days":30}' \
http://localhost:7862/api/config/retention两个阈值都为 0:引擎仍运行(维护 inventory + /healthz.storage),但永不删除。详见 docs/retention.md:阈值、淘汰顺序、以及防止 retention 与 writer 竞争的 lease 语义。
{
"id": "01HX7K8MS...",
"ts_start": "2026-05-27T10:23:45.123Z",
"ts_end": "2026-05-27T10:23:46.357Z",
"client": "172.17.0.5:54321",
"method": "POST",
"path": "/v1/messages",
"upstream": "http://gateway:7860",
"status": 200,
"req": {
"headers": {"x-api-key": "sk-***", "anthropic-version": "2023-06-01"},
"body": {"model": "claude-sonnet-4-6", "messages": [...], "stream": true}
},
"resp": {
"headers": {"content-type": "text/event-stream", "x-request-id": "..."},
"events": [
{"event": "message_start", "data": {"message": {"id": "msg_...", ...}}, "t_delta_ms": 12},
{"event": "content_block_delta", "data": {"delta": {"text": "Hello"}, ...}, "t_delta_ms": 234},
{"event": "message_delta", "data": {"usage": {"output_tokens": 8}}, "t_delta_ms": 511},
{"event": "message_stop", "data": {}, "t_delta_ms": 514}
],
"stream_done": true
},
"disconnected": false,
"truncated_req": false,
"truncated_resp": false
}上面示例的 header 值是为文档脱敏过的;磁盘上的 JSONL 保留客户端发来的原始字节。见下面 Security。
完整字段参考见 ARCHITECTURE § 3。
api-log 字节级原样转发每个 HTTP 请求。finalize 阶段对已知的 LLM API shape 做解析;不认识的 path 仍会落 header + 原始 body,只是跳过结构化字段。
- OpenAI Chat Completions(
/v1/chat/completions)—— 请求的messages[]、响应的choices[],流式(data: {...}\n\n分块)和非流式都覆盖。 - OpenAI Responses(
/v1/responses)—— 请求的input[]、响应的output[],包括response.output_item.added这类 SSE 事件,用于 tool-call 抽取。 - Anthropic Messages(
/v1/messages)—— 请求的messages[]+system、响应的content[],SSE 事件message_start/content_block_delta/message_delta/message_stop。 - SSE streams —— 每个
event:/data:对切成resp.events[]里一项,t_delta_ms相对响应起点记录。非 SSE 响应作为解析后的 JSON 落在resp.body。 - After-mutation capture for streams —— 当插件改写流式响应(Phase B/C),api-log 记录客户端实际收到的字节,而非上游未改写前的流。
tool call、reasoning block、以及这些协议承载的其他内容原样位于 req.body 和 resp.body / resp.events[] 中。不会被提到顶层字段,api-log 也不做解释。
下面这些是参考片段,不是 benchmark 工作负载。请按自己的数据跑。
跨已录的 Responses-API trace 统计 tool-call 频率:
zcat data/2026-*/*.jsonl{,.gz} 2>/dev/null \
| jq -r '.resp.events[]? | select(.event=="response.output_item.added")
| .data.item | select(.type=="function_call") | .name' \
| sort | uniq -c | sort -rn被中断的流:
jq 'select(.disconnected==true or .resp.stream_done==false)
| {id, path, status, ts_start}' \
data/2026-05-27/*.jsonl按 model 和 path 看最近的失败 trace:
sqlite3 data/index.sqlite \
"SELECT ts_start, model, path, status FROM traces
WHERE status >= 400
ORDER BY ts_start DESC LIMIT 20"按时间列出某个 session 的所有 trace:
sqlite3 data/index.sqlite \
"SELECT id, jsonl_path, jsonl_offset FROM traces
WHERE session_root_id='01HX7K...' ORDER BY ts_start"jsonl_path + jsonl_offset 这对值让消费方直接 seek 到 JSONL 文件中的对应行。做批量分析的 AI agent 可以绕过 read API,直接从磁盘读 JSONL。
curl -N -H "Authorization: Bearer $TOKEN" \
"http://localhost:7862/api/traces/01HX7K8MS.../replay?speed=2"/api/traces/:id/replay 按录制时的 per-chunk 间隔重新吐出 SSE 帧(或以 speed 倍率加速)。speed=2 把间隔减半;nodelay=1 把所有事件背靠背一次性吐完。重放只对 API 调用方进行,不会回联上游 LLM。完整语义(包括重解析后缺 t_delta_ms 的 trace 如何处理)见 ARCHITECTURE § 6.4。
read API 监听独立端口(默认 :7862),和 proxy 分开。一共 13 条路由;完整表面见 ARCHITECTURE § 6。要点:
GET /healthz—— 不鉴权;暴露内存里的 drop / overflow 计数器,让运维不用 grep 日志就能发现 capture 退化。GET /api/traces—— 列表,SQLite 后端,支持since/until/status(精确值或2xx/4xx/5xx桶)/model/path(结尾*表示前缀)/key_hash/session_root_id/project/limit/ 游标分页。GET /api/traces/:id—— 详情;返回{row, trace},调用方一次往返同时拿到 SQLite 行和解析后的 JSONL 行。GET /api/traces/:id/replay—— 保留 pacing 的 SSE 重放。GET /api/sessions—— 按session_root_id聚合的 session 摘要。只做行级聚合,不做树遍历。GET /api/export—— 流式吐出匹配 JSONL 行的 zip + 打包的agent/CLAUDE.md,供离线 / AI 辅助分析。GET/PUT /api/config/plugins(+PUT /api/config/plugins/:id、DELETE /api/config/plugins、GET /api/plugins/types)—— 热更text-replace/text-append/path-filter插件。YAML 仍是声明式的真相;运行时覆盖持久化到data/runtime_overrides.json。默认关闭。
除 /healthz 外,所有 read 端点都要求 Authorization: Bearer <data/admin_token>,并以 Cache-Control: no-store 响应。
api-log 不内嵌 HTML viewer。GET / 返回一个 JSON 指针,指向独立的 api-log-viewer 项目;二进制里没有任何 HTML。
api-log 默认在 /viewer/ 托管 viewer。首次请求时,后端从 api-log-viewer 的某个固定 release 拉取 dist.zip,对照烧进 backend binary 的 SHA-256 常量校验该 asset,然后把 bundle 解压到 data/viewer-cache/ 下的缓存目录。后续请求直接从缓存读。
SHA-256 不匹配对该路由是致命的——/viewer/ 返回 503,binary 把不匹配写进日志。后端从不提供未校验的 asset。
覆盖开关(env 或 YAML):
| Knob | 默认 | 效果 |
|---|---|---|
APILOG_VIEWER_ENABLED |
true | 设为 false 则不托管;/viewer/ 返回 503 disabled。 |
APILOG_VIEWER_REPO |
2nd1st/api-log-viewer |
可指向任何 GitHub 仓库。 |
APILOG_VIEWER_VERSION |
与 backend release 绑定 | 要拉取的 tag。 |
APILOG_VIEWER_SHA256 |
与 backend release 绑定 | 覆盖 REPO 或 VERSION 时必须同时提供;不匹配返回 503。 |
APILOG_VIEWER_LOCAL_PATH |
(未设) | 指向某个 dist/ 目录的绝对路径。完全跳过 fetch + 校验;适用于离线 / 隔离网络部署,以及本地 viewer 开发。 |
APILOG_VIEWER_CACHE_DIR |
<data_dir>/viewer-cache |
缓存根目录。 |
APILOG_VIEWER_PUBLIC_PATH |
/viewer |
URL 前缀。 |
后端从不自动更新已缓存的 viewer——fetch 在每个 (repo, version, sha) 三元组下只发生一次。要前滚版本,要么发新的 backend release(同时 bump 这两个常量),要么显式覆盖 APILOG_VIEWER_VERSION + APILOG_VIEWER_SHA256。
Bearer token 在磁盘上不做脱敏。 JSONL 文件里的 Authorization / x-api-key header 就是客户端原样发来的字节。api-log 在 capture 路径上不做任何脱敏。把 data/ 目录当成 ~/.ssh/ 或装生产 API key 的文件来对待:
- 把
data/挂到一个有严格文件系统权限的路径(chmod 700或更严)。 - 在没有反向代理做 transport security 与访问控制的前提下,不要把 proxy listener(
:7861)或 read API listener(:7862)暴露到不可信网络。 data/admin_token处自动生成的 admin bearer 是 read API 唯一的凭据。删文件重启即轮换。- 同主机容器之间的 plain HTTP 是主要支持的拓扑。如果需要 TLS 或跨主机路由,用你已有的反向代理终结;api-log 自身只听 HTTP。
脱敏是 capture 路径上有意为之的非目标。需要脱敏 trace 的话,针对 JSONL 文件跑一个 sidecar;磁盘格式已文档化且稳定。
威胁模型与漏洞披露流程见 SECURITY.md。
git clone https://github.com/2nd1st/api-log.git
cd api-log
# unit + integration tests (race detector on)
go test ./...
# lint (golangci-lint v2)
golangci-lint run
# build
go build -o bin/api-log ./cmd/api-log
# the dev-stack integration harness
./tests/integration/run.sh项目包含 23 个 Go 包,race 检测下测试全过,CI 用 golangci-lint v2 跑 lint。每个 push 与 PR 都触发 CI;见 .github/workflows/ci.yml。
- v0 —— capture 路径(解析 + JSONL 落盘 + SQLite 镜像)、session 推断、最小 read API。
- v0.1 viewer —— api-log-viewer —— 多实例聚合、session 树可视化、tool-call 渲染、SSE 重放。
- v0.1 plugins ——
text-replace/text-append/path-filter,通过PUT /api/config/plugins热更。默认关闭。 - v0.2 —— 可选的每网关 bridge adapter(独立项目)—— 按
key_hash把外部数据(CPA 的 Redis usage queue、new-api 的 MySQL 日志表)join 进 api-log trace。核心 proxy 仍保持网关无关。
完整列表(含 v0.1.0-deferred 区段)见 ROADMAP.md。
- tcpdump / pcap —— append-only 抓取,解释延后
- CLIProxyAPI (CPA) —— 单二进制、单配置的审美
- Claude Code / Codex CLI —— 把本地 JSONL session 文件当作可用格式
- Langfuse —— LLM 可观测性这一面,本项目在 capture-vs-instrument 这一轴上有意作出不同选择
- sub2api —— 用来在真实流量下验证 capture 路径的主要上游网关;path-filter 模式集合与 client-identification 分类法都是对着它的真实流量样本调出来的。
本项目代码与文档由 Claude Opus 4.8(Anthropic)作为主要 pair-programmer 完成,并由 GPT-5.5(通过 Codex CLI)作为 research + review 助手——发布前 adversarial review、对照 OSS 参考项目做 README 结构分析、事实交叉验证。保留 / 删除 / 修订的判断由人类作者负责;这里列出 AI 协助是为了透明,不是 authorship。
MIT —— © 2026 Leo Yun.
