Releases: 2nd1st/api-log
v0.1.3
v0.1.2
Capture-side path drop plugin + /api/export byte-cap + curl client kind.
Highlights:
- New
capture_filterplugin drops paths BEFORE writing (vs the existing observer-onlypath_filter). /api/exportbyte-cap (default 2 GiB,?bytes_all=1bypass; independent of the row cap).- Byte-cap wired to
APILOG_API_EXPORT_BYTE_HARDCAPenv /api.export_byte_hardcapYAML. curlUA classified as its own kind.
Added
/api/exportbyte-cap pre-flight (commitff0c42f):
complementary safety net to the v0.1.1 row cap. Default 2 GiB;
?bytes_all=1bypass is independent of?all=1(each cap names
what it lets through). Measured in source JSONL bytes (sum of
os.Statfile sizes for matched groups; one syscall per group, not
per row). Over-cap returns 413 withlimit:"bytes"discriminator
before any zip bytes hit the wire. Audit-driven: sub2api's 9 GB /
~6.9 GB+ zip didn't trip the 50000-row cap because media-heavy
rows are bytes-per-row dominant, not row-dominant.capture_filterplugin (commitd64863c): pre-write path
drop, sister to the existing observer-classpath_filter. Where
path_filterdrops at finalize (after bytes ran through the
capture sinks),capture_filteris checked in the proxy handler
BEFOREstartTrace— a matched path skips JSONL + SQLite + media- body tee entirely; the request still forwards upstream unchanged.
Audit-driven (v0.1.1 deploy workflow showed sub2api stores 86%
control-plane polling that's not LLM-observability material).
Opt-in viaplugins.capture_filter.patterns; existing
plugins.path_filterdeployments are not silently re-interpreted.
Pattern syntax mirrorspath_filter. Counter
traces_dropped_capture_filtersurfaces drop activity on
/healthz.counters. Live-verified on sub2gpt with
/api/v1/auth/*+/api/v1/subscriptions/*patterns.
- body tee entirely; the request still forwards upstream unchanged.
curlUA in client taxonomy (commitf60c66d): rule 11 in
parser/client.go, placed aftergo-http-client. Generalises
smoke-test + healthprobe traffic across every adopter. Justified
by a Workflow audit (we0wvgjhv) that sampled 80 of 197 null
client_kind rows on sub2gpt and found 7 cleancurl/8.7.1UAs.
Changed
pathfilter.Pattern+pathfilter.Compile+pathfilter.MatchAny
are now exported so sibling plugins (initiallycapturefilter)
reuse one validated parse + match implementation. Behavior of
pathfilter.Plugin.Initis unchanged.
Fixed
Removed
v0.1.1
Storage retention coordinator + streaming /api/export + Gitea-mirror-friendly viewer fetch.
Highlights:
- Storage coordinator with lease arbitration, retention engine,
/api/config/retentionGET/PUT. - Streaming
/api/export(50000-row pre-flight, Deflate-1, lease-protected groups). - Viewer pluggable releases endpoint (Gitea / Forgejo).
- Writer idle-close releases bucket leases for retention.
Added
- B1 / B2 — storage coordinator + lease arbitration (commit
ce31ac2): newinternal/storage/package owns file identity
(FileID), refcount leases, on-disk inventory, status, and
eviction. Writer acquires a per-(date, keyhash)bucket lease
BEFORE opening the JSONL — eviction'sdeleteIfIdlerefuses while
any lease is held. Foundation for runtime-toggleable retention
without TOCTOU. Adds idempotentidx_jsonl_pathindex; switches
SQLite DSN to embedjournal_mode=WAL+busy_timeout(5000)+
foreign_keys(on)so every conn the pool hands out has the right
pragmas (was only the first); bumpsMaxOpenConns8 → 10 to
absorb the storage monitor's reconcile sweep.media.Extract(t, bucket)now takes the writer-chosen bucket FileID so media
co-locates with its JSONL across UTC midnight rotation. - B3 — streaming
/api/exportwith 413 pre-flight (commit
e73f5c2): two-phase pipeline — Phase 1 borrows a single SQLite
conn via the newStreamMatchingcursor and walks rows building
per-file groups + slim media refs; Phase 2 builds the zip after
the cursor + conn release, withRegisterCompressorswapping
Deflate level 6 → level 1 (~3× wall-clock for ~5% size on 100k+
row exports). Pre-flightCountMatchinggates oversized exports
at a 50000-row default cap and returns 413 with a JSON pointer to
the bypass flag BEFORE any zip bytes hit the wire;?all=1
disables. Newproject=filter wired through;r.Context()
threads through the cursor + zip loop for clean mid-export cancel. - B4 —
/healthz.storage+/api/config/retentionGET+PUT
(commit8b1287c): operator-facing surface for the storage
coordinator. PUT validates against the same rulesstorage.New
enforces, applies in-memory FIRST viacoord.UpdateConfig, then
persists toruntime_overrides.jsonso the next process start
picks it up without env / yaml plumbing. Both knobs zero ==
disable retention without restart./healthzgains a top-level
storagekey carryingDataDirBytes / MaxBytes / MaxAgeDays / UsagePct / State / EngineRunning / EvictionCapHitplus
conditionalLastEvictionTs/LastEvictedBytes. - B5 — writer idle-close (commit
8a95408): after N min
(default 10m) without an append, the writer releases the bucket
lease and closes the OS handle so retention's byte-cap can touch
today's quiet buckets and long-tail keys don't pin a process-
lifetime fd each. Idle-close does NOT gzip — that's strictly a
date-cross event.SetIdleTimeoutis the public knob; tests
pass tiny values without exposing a constructor param.
Changed
exporter.WriteZipsignature:(ctx, w, store, dataDir, filters, coord). Removedlimit int— pre-flightCountMatchingis the
gate now; the cursor walks unbounded. Existing test callsites
passcontext.Background().writer.Newsignature gainscoord *storage.Coordinatoras the
8th argument beforeclock.nilkeeps the v0.1.0 behavior
(no lease arbitration); production callers wire a non-nil coord.media.Extract(t)→media.Extract(t, bucket storage.FileID).
Caller passes the writer-chosen bucket; fixes a UTC-midnight
co-location bug where a trace's TsStart could resolve a different
bucket than the writer just wrote the JSONL to.internal/api.DepsgainsStorageCoord *storage.Coordinator.
Nil-safe —/healthzomits the storage block,/api/config/ retentionreturns 503, andexporter.WriteZipfalls back to
lease-less reads.counters.CountersgainsAddEvictedTraces/AddEvictedBytes- matching
Snapshot.EvictedTraces/EvictedBytes.
- matching
UpdateConfigsynthesizes a baselineStatuswhen called before
the first monitor tick so PUT-then-GET sees the new thresholds
instead ofpending.EngineRunningstays false until the
monitor goroutine actually starts.
Added (late-cycle)
- Pluggable viewer releases endpoint (commit
20b2275): new
APILOG_VIEWER_RELEASES_API_BASE+APILOG_VIEWER_RELEASES_AUTH_TOKEN
config / env knobs route the dist.zip fetch at non-GitHub stores
(Gitea, Forgejo, GHE). Useful for staging viewer changes against an
internal artifact mirror before cutting a public release; tested live
on sub2gpt pulling fromgitea.homelab.lan(Gitea release shape is
GitHub-compatible). Default empty keeps v0.1.0 behavior.
Fixed
exporter.summarizeFilters(internal/exporter/exporter.go) now
emits a line for theProjectfilter in the in-zipREADME.md. The
filter was wired through SQLite correctly since v0.1.1's streaming
rewrite; only the human-readable summary line was missing. Caught by
the v0.1.1 pre-tag deploy audit (commitae5a97d).
v0.1.0
api-log v0.1.0 is the first public backend release: a transparent LLM gateway trace recorder for sub2api, CLIProxyAPI (CPA), new-api, and other OpenAI-compatible gateway stacks.
Quick start
docker pull ghcr.io/2nd1st/api-log:0.1.0Minimal Docker Compose shape:
services:
gateway:
# sub2api / CPA / new-api / your existing gateway
expose: ["7860"]
api-log:
image: ghcr.io/2nd1st/api-log:0.1.0
ports:
- "7861:7861"
- "7862:7862"
environment:
APILOG_PROXY_UPSTREAM: http://gateway:7860
volumes:
- ./api-log-data:/dataPoint clients at http://localhost:7861 instead of the gateway port. Read captured traces from the authenticated read API on http://localhost:7862.
Supported protocol surfaces
- OpenAI Chat Completions:
/v1/chat/completions - Anthropic Messages:
/v1/messages - OpenAI Responses:
/v1/responses - OpenAI Image Generations:
/v1/images/generations - Gemini generate/stream surfaces used by compatible gateways
Streaming responses are captured as SSE event arrays where the protocol exposes SSE.
Security notice
api-log records raw HTTP traffic. `Authorization` and `x-api-key` headers are written to JSONL exactly as clients sent them. Treat the `data/` directory like production API-key material: run api-log only on trusted networks, restrict filesystem access, and apply disk-encryption / backup policy accordingly.
The proxy listener does not authenticate clients. Network access control belongs at the operator layer.
Viewer
The companion UI is released separately:
https://github.com/2nd1st/api-log-viewer/releases/tag/v0.1.0
This backend can serve the pinned viewer bundle at `/viewer/` by default. The backend fetches the pinned dist.zip, verifies its SHA-256, extracts it into `data/viewer-cache/`, and serves it without embedding HTML into the binary.
Out of scope
Token accounting, redaction, billing, and eval pipelines are not built into api-log — they run as downstream JSONL processors. Replay returns recorded responses to read-API callers only; it never re-contacts the upstream LLM gateway. Hosted viewer updates are pinned by backend release; there is no auto-update path by design.
v0.1.x direction
Protocol edge cases, read-API filters, viewer pin updates, and adopter docs for sub2api / CPA / new-api deployments.