api-log uses CHANGELOG.md for shipped work. This
file is the forward view: what we plan to do, what we know we
are not going to do, and what we are watching for.
Most of v0 and v0.1 has landed (capture path + read API + viewer +
plugin system); see CHANGELOG. The open work for the next cycle
falls in two buckets: a small set of v0.2 candidates surfaced by
operator use, and the v0.1.0-review deferred-items list (post-tag
hygiene). Neither blocks the v0.1.0 tag.
Optional per-gateway adapters that join external data into api-log
traces by key_hash. The core proxy stays gateway-agnostic; adapters
live as separate repos with their own release cycles.
Use cases the operator has surfaced:
- CPA bridge — join
CPA's Redis usage queue (per-request upstream metering) onto api-log traces. Lets the JSONL line carry the upstream-reportedprompt_tokens/completion_tokensin addition to whatever the gateway forwarded to the client. - new-api bridge — same shape against
new-api's MySQL log table. - Generic bridge interface — any external store keyed by
key_hash+ts_rangeis a candidate; the adapter writes side-car JSONL or appends columns to the SQLite mirror via a documented extension point.
These are not in the core repo's scope. Tag them api-log-bridge-cpa,
etc.
- Default path-filter pattern set. Today operators install the
path-filterplugin and enumerate patterns themselves. A conservative default for the common LLM gateway shapes (admin polling, auth refresh, billing telemetry) would cut first-run noise. Needs operator-side validation before shipping defaults. - Per-
key_hashcapture budget. Not a gateway rate limit. A cap of the form "stop recording afterNMB / day for thiskey_hash" so a single noisy client cannot fill the disk. Off by default; opt-in via YAML. /healthzfield configurability. Some operators want the/healthzJSON to expose a subset of counters; others want everything. Ahealthz_expose: [counter_names...]knob keeps the default-all-fields behavior but lets adopters narrow it.
The largest gap surfaced by the v0.1.0 review. Captured in detail in the "v0.1.0 review — deferred items" section below; this is a pointer for navigation.
api-log rebuildsubcommand (rebuild SQLite from JSONL)api-log verifyintegrity check- Retention / pruning command
- Documented WAL checkpoint policy
- Backup procedure in ARCHITECTURE
api-log is a logging tool with no observability surface of its own.
/metricsPrometheus endpoint (the existinginternal/counters/package is a near-perfect fit; the work is wiring an encoder behind an unauth/metricsroute)- OTEL trace span per proxied request
- Plugin-chain latency histogram per
plugin.name
The "JSONL is truth, SQLite is rebuildable" invariant has no
implementation hook. v0.2 should add:
"v": 1field on every JSONL line.PRAGMA user_versionon SQLite + aschema_versiontable somigrate()does not detect "already applied" via error-string matching.JSONL_FORMAT_VERSIONconstant downstream tools can pin against.- Documented backward-compat policy: readers MUST tolerate absent fields; writers MUST NOT remove fields, only add.
Today's install path is Docker-only. The binary is pure Go and builds for darwin/arm64 + darwin/amd64 + windows/amd64 + linux/arm64 without changes. Shipping is the missing piece:
goreleaser-driven multi-arch binary releases- Homebrew tap, scoop manifest, Chocolatey
- README install paths beyond Docker
Output of a pre-release adversarial review (81 findings). The
Critical + Important items that blocked / belonged-in the v0.1.0
tag landed in commits b6a8cdf through the v0.1.0 prep window —
see CHANGELOG.md for the per-commit walk. What follows are items
the operator consciously deferred to post-v0.1.0 because they're
(a) larger than fits the tag window or (b) not
contract-breaking. None block the tag; each is a real follow-up.
- Multi-arch image. Current
docker/build-push-action@v6ships linux/amd64 only — Apple Silicon, RPi, Graviton getexec format error. Add aplatforms:key + buildx QEMU step, publish at leastlinux/amd64+linux/arm64. - Supply chain. No
provenance: true, no SBOM step (e.g.anchore/sbom-action), no cosign signature. Distroless base pinned by tag (gcr.io/distroless/static-debian12:nonroot) not by sha256 digest — pin by digest, refresh deliberately. - THIRD_PARTY_NOTICES.
modernc.org/sqlite(BSD-3) andgopkg.in/yaml.v3(Apache-2.0) impose notice-preservation. Add a NOTICE / THIRD_PARTY_NOTICES.md generated from go.sum + the npm tree on the viewer side. - Dependency hygiene. Neither repo ships
.github/dependabot.ymlor Renovate config. Add minimal dependabot forgomod+npm+github-actions, weekly cadence. - Reproducibility. Dockerfile has
-trimpath(good) but noSOURCE_DATE_EPOCH; image digest varies across rebuilds of the same git tag. govulncheck+pnpm auditCI steps. Both absent today; with the floating distroless tag this means silent base churn.RELEASING.md. README announces "v0.1.0 tag is being prepared" with no published checklist. ~10 lines describing: version bump surfaces (CHANGELOG, viewer README Status, dockerfile arg), tag conventions, ghcr publish path, smoke-test before announce.
The "JSONL is truth; SQLite rebuildable" invariant has no implementation today.
- No
"v": 1on the JSONL line. - No
PRAGMA user_versionorschema_versiontable on SQLite. migrate()detects "already applied" via string-matching error messages (sqlite.go:138, 156, 175, 192) — fragile across driver upgrades.- No
JSONL_FORMAT_VERSIONconstant downstream tools can pin against. - Downgrade is impossible because columns can't be dropped.
Plan a v0.2 schema-versioning Work Package: set PRAGMA user_version
on migrate(), stamp every JSONL line with "v": <int>, document a
backward-compat policy ("readers MUST handle absence of any field;
writers MUST NOT remove fields, only add").
api-log rebuildsubcommand: walk JSONL → drop + recreate index.sqlite → verify counts. The tooling for the "rebuildable from JSONL" promise does not exist today.api-log verify: integrity check (every SQLite row's jsonl_path- jsonl_offset resolves to a non-empty line; every JSONL line has at least one SQLite row OR is documented partial-line).
- Retention / pruning: no
apilog retain --before 30dor similar. A 2000-user-fleet adopter at month 6 has an unbounded data dir. - Disk-full circuit breaker beyond
DropJSONLFail: a back-pressure signal that switches capture to header-only or to a tmpfs spool. - WAL checkpoint policy: no manual
PRAGMA wal_checkpoint(TRUNCATE)anywhere; low-traffic instances may growindex.sqlite-walunboundedly. - Backup procedure documented in ARCHITECTURE / README:
sqlite3 .backup+ JSONL tar.
api-log is a logging-and-observability tool with no observability surface of its own.
/metricsPrometheus endpoint (the counters ininternal/counters/are already a near-perfect fit; add a Prometheus encoding behind a/metricsroute that's unauth-gated like/healthz).- OTEL exporter for proxy-side trace spans (one span per request with attrs: path, status, model, client_kind, duration, slow).
- Plugin-chain latency histogram per
plugin.name.
api-log validate <path>: parse + reject with line-pointing error message before the binary touches the network.api-log.example.yamlat repo root (currently buried in deploy/dev-stack/).- JSON Schema for editor autocomplete (generated from the
Configstruct's yaml tags). --config-helpflag listing every key + env override + default.- README "Configuration" section enumerating env vars + YAML keys (today the only authoritative reference is Go struct tags).
Bucket B added recoverMW + http.MaxBytesReader on PUT plugins.
The read API still ships with:
- No
MaxHeaderByteson the read API listener (apiSrv). - No
ReadTimeouton the read API. (The proxy'sWriteTimeout=0is correct for SSE; the read API doesn't need that exception.) - The proxy listener is intentionally permissive (operator-restricted at the network layer); the read API is admin-bearer-gated so its hardening profile should be tighter.
Single-commit follow-up: set apiSrv ReadTimeout=15s, WriteTimeout= 30s, MaxHeaderBytes=64KB; document the proxy carve-out.
- No Homebrew tap, no scoop manifest, no Chocolatey, no goreleaser multi-arch binary. The pure-Go binary builds for darwin/arm64, darwin/amd64, windows/amd64, linux/arm64 with no source changes; shipping is the missing piece.
- README install path is Docker-only despite the "one binary" pitch.
- No
instance_idmixed into the ULID generation. Two binaries pointed at the samedata/produce collision-prone IDs. - No advisory lock on
data/index.sqlite(modernc.org/sqlite shares the file mutex but two writers across processes is undefined). - README / ARCHITECTURE don't state the single-node constraint explicitly. Add to ARCHITECTURE Limitations section.
- 60-second
docker compose up; curl proxy; curl apistory. Todaydeploy/demo/requires cloning sub2api as a sibling.deploy/dev-stack/is hidden behind the "dev" label.deploy/README.md(added in Bucket C) helps; an asciicast or GIF would complete the story. - Glossary explaining trace / session / key_hash / prefix_canonical_hash / session_root_id.
- Annotated
api-log.example.yamlat the repo root.
- Semantic landmarks audit (header / main / nav).
- Focus trap on modals (PluginEditModal, CommandPalette, AuthModal).
- Documented Escape handler on CommandPalette.
- Skip-to-content link.
- axe-core CI step.
- Contrast-ratio statement for the muted-palette theme.
- ConversationTab session-view N sequential awaits → backend batch
endpoint (
?session_root_id=…&include=full) eliminates per-trace JSONL open overhead. The L9 perf finding that motivated Bucket B's SQLite conn pool fix; finishing the chain needs the batch endpoint. - ConversationTab
authFetchis imported directly instead of threaded via prop (breaks the DI pattern every other tab uses). - Trace ID interpolated unencoded into URLs in ConversationTab + OverviewTab (theoretical today because ULIDs are alphanumeric; inconsistent with PluginList which does encode).
- Export.svelte 8-input filter subscription via manual
void [f_status, f_path, …]— brittle on add; replace with$derived qsKey. - RawTab re-runs
jsonHLover the full body on every i18n label change; language toggle locks UI on large traces.
- Generated admin token printed to stdout today; survives in
journald + docker logs. Switch to stderr with a pointer at
data/admin_token; document journaling risk in SECURITY.md. - No CORS / standard security headers (X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer, Permissions-Policy: empty, explicit Access-Control-Allow-Origin). Single middleware.
Watchdog.Pulsecallstime.Timer.Reseton every byte chunk → runtime timer-lock contention on dense SSE. Coalesce: only Reset iflastPulse >= timeout/4ago.ListSessionscorrelated subqueries scan the table 3× per request. AddCREATE INDEX idx_session_ts ON traces(session_root_id, ts_start DESC), or refactor to a single self-join.- Filter columns (
path,model,client_kind,status,client_project) have no per-column index; SQLite scansidx_tsfor every filtered list. Addidx_path_tsand a partialidx_project_tsWHERE client_project IS NOT NULL. parser.finalizedoesio.ReadAllof the full body twice on the failure path — GC pressure under burst load.
CONTRIBUTING.mddescribing PR rules (test commands, restraint bar, code-style references).CODE_OF_CONDUCT.md(Contributor Covenant boilerplate).ISSUE_TEMPLATE/+ PR template.- Governance + maintainer note: "this is one contributor's project; sustainability is best-effort." Honesty over pretending-to-be-bigger.
These are restraints, not omissions. They keep the project small.
- No gateway features. No auth, no routing, no retries, no rate limiting, no caching, no request rewriting. Those belong in the upstream gateway.
- No SDK instrumentation. api-log captures HTTP traffic; it does not replace Langfuse / Phoenix / LangSmith for in-process tracing.
- No semantic interpretation of recorded content. Cost calculation, model-family classification, "what's a frequent skill", evaluation pipelines — all downstream of the captured JSONL.
- No automatic redaction in the capture path. The bytes the client sent are the bytes recorded. If you need redacted JSONL, run a sidecar that rewrites it. The operator-opt-in plugin surface can mutate response content, but the capture path itself is byte-faithful.
- No bundled "smart" middlebox behavior. No prompt collision detection, no fake-cache injection, no content classification on the wire.
- No matching on credential-carrying header VALUES, even via
off-by-default plugin or YAML "extra rules" hatch. The named
header taxonomy in
internal/parser/client.goreads headers designed to be public (User-Agent,x-stainless-*,Anthropic-Beta, etc); the project does not extend this surface toAuthorization/X-Api-Key/ equivalent VALUES, even when the operator's own infra has a known key-prefix convention. If client identity is unrecoverable from the named-header layer, the honest answer is the parser's existing zero-value (the trace stays tagged by whatever generic UA it carries —go-http-client,curl,browser). Per-deployment segmentation has a non-credential answer:key_hashis on every trace row and the operator's downstream tooling can maintain akey_hash → labelmap without ever touching a raw credential. Decided 2026-06-02 after an adversarial three-lens design pass (oss-fit / surface restraint / privacy) unanimously rejected the proposed slot. The slot's existence — off-by-default included — regresses the byte-faithful-but-doesn't-inspect-credentials posture that this README, SECURITY.md, and the parser package docstring all stand on.