o11ylogsdb: in-memory engine design + 27 experiments validating 20× target#180
Conversation
|
Caution Review failedPull request was closed or merged during review WalkthroughThis pull request introduces 🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
| stats(): StoreStats { | ||
| let chunks = 0; | ||
| let totalLogs = 0; | ||
| let totalBytes = 0; | ||
| for (const id of this.streams.ids()) { | ||
| const chunkList = this.streams.chunksOf(id); | ||
| chunks += chunkList.length; | ||
| for (const c of chunkList) { | ||
| totalLogs += c.header.nLogs; | ||
| totalBytes += serializeChunk(c).length; | ||
| } | ||
| } | ||
| return { | ||
| streams: this.streams.size(), | ||
| chunks, |
There was a problem hiding this comment.
⚠️ Performance: stats() re-serializes every chunk on each call
The stats() method calls serializeChunk(c).length inside a loop over all chunks, performing a full wire-format serialization (header encoding + buffer allocation) just to measure byte count. Since ChunkHeader already carries payloadBytes, the total serialized size can be computed from header metadata without re-serializing. This is called from benchmarks (e.g., engine-roundtrip.bench.ts) and will become a bottleneck as chunk count grows.
Suggested fix:
Cache the serialized byte length when a chunk is frozen (e.g., store it alongside the chunk in the stream registry), or compute it from `chunk.header.payloadBytes` plus deterministic wire overhead, avoiding the re-serialization entirely:
// In stats():
for (const c of chunkList) {
totalLogs += c.header.nLogs;
totalBytes += c.header.payloadBytes + WIRE_HEADER_OVERHEAD;
}
Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion
Browser-native logs database for OpenTelemetry data. Targets 20× storage efficiency vs raw OTLP/JSON via a Drain + FSST + ALP + Roaring-lite + binary fuse codec stack with a streaming query executor. Adds: - Package metadata (package.json, tsconfig.json, .gitignore). - PLAN.md with priority stack, milestones (M0-M8), per-corpus storage targets, benchmark gates, and what we don't build. - README.md with milestone status table and a reading-order index. - dev-docs/: findings (validated and refuted assumptions with per-corpus B/log numbers and lossless ceilings), techniques (the shipped codec/index/query stack), drain-prototype (the M2 Rust port). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- types.ts: OTLP-aligned types (LogRecord, AnyValue, Resource, InstrumentationScope, KeyValue, BodyKind, StreamId). - chunk.ts: chunk format v1 (magic OLDB + JSON header + payload), ChunkPolicy plug-in surface with preEncode/postDecode and encodePayload/decodePayload hooks, severity zone-map computed at chunk-close, ChunkBuilder, serializeChunk/deserializeChunk/readRecords. - stream.ts: per-(resource, scope) stream registry with WeakMap reference-identity fast path and ordered chunk lists. - classify.ts: BodyClassifier and TemplateExtractor interfaces plus a default classifier. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- codec.ts: pluggable Codec / StringCodec / IntCodec interfaces and CodecRegistry. - codec-baseline.ts: rawCodec, GzipCodec, ZstdCodec, lengthPrefixStringCodec, rawInt64Codec, defaultRegistry. - codec-columnar.ts: ColumnarRawPolicy and ColumnarDrainPolicy — the M4 binary columnar payload (delta-zigzag-varint timestamps, u8 severity/body-kind runs, embedded template dict, length-prefixed binary body). Layout dominates compression; Drain refines. - codec-drain.ts: DrainChunkPolicy — NDJSON-form Drain (kept as a reference; structure primitive only — not a compression win on NDJSON, refuted as a hot-path codec). - codec-typed.ts: TypedColumnarDrainPolicy — M4 per-(template, slot) value-distribution-aware codec dispatch. Generic detectors (SIGNED_INT, UUID, UUID_NODASH, PREFIXED_INT64, PREFIXED_UUID, TIMESTAMP_DELTA) recognize byte shapes, never specific literals. - drain.ts: TS Drain port. Bit-identical to the Rust prototype and to the published Python reference (ARI = 1.0 on five public log corpora). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- engine.ts: LogStore composes the chunk format, codec registry, body classifier, stream registry, and chunk-policy plug-in surface. Optional per-stream policyFactory for stateful policies. - query.ts: streaming query executor (M7 first cut). Chunk-level pruning by zone map (time range, severity range) and stream pruning by resource attributes — only chunks that survive get decoded. QuerySpec covers time range, severity threshold, body substring, KVList leaf equality, resource attribute equality, and limit. Predicate set kept minimal but useful. - compact.ts: re-encodes a chunk's payload to a different codec — the hot-ingest @ z3 → background @ z19 promotion path. - index.ts: public API re-exports. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bench/ holds the reproducible bench harness (harness.ts, profile-harness.ts, run.mjs) and 26 modules that drive the merge gate: - bytes-per-log: top-line gate; per-corpus B/log under every baseline codec. - engine-roundtrip / engine-drain / engine-columnar / engine-typed: M3-M4 thesis tests at successive policy levels. - pino-roundtrip / pino-query: structured-JSON corpus paths. - per-stream-chunking / per-stream-drain / multi-stream: stream- registry behavior and chunk-list ordering. - hierarchical-drain / cross-chunk-dict / per-column-zstd / zstd-level-asymmetry: refutation/validation runs that informed the "what we ship" decisions in dev-docs/findings.md. - ngram-bloom / token-postings: chunk-scope index economics (refuted at chunk scope; revisit at stream scope). - query-latency / query-at-scale: M7 query-engine measurements. - sustained-ingest / append-latency / drain-churn: ingest-side GC pause and tree-growth profiles. - compaction / lossy-archive / byte-decomposition / typed-int-column: per-column codec-dispatch diagnostics. - profile-policies / cpuprofile-typed.mjs / cpuprofile-query.mjs: CPU profiles for hot-path analysis. Public Loghub-2k samples and a synthetic structured-JSON corpus checked in under bench/corpora/. Loghub-2k is the canonical academic sample with golden ground-truth labels for grouping-accuracy validation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
In-house Rust port of the published Drain log template extractor (He et al., ICWS 2017). Targets wasm32-unknown-unknown: - No std, no FFI dependencies. The available Rust crate of the algorithm links a C regex library that doesn't build for wasm32 without a sysroot; this 200-LoC port has zero deps. - Bump-leak allocator on wasm32 backed by a 16 MB static arena. Host re-creates the parser per chunk; arena resets by reloading the module. - Minimal extern "C" ABI — drain_create / ingest_line / template_count / get_template / destroy. The host owns line bytes and template-output buffers. - 6.7 KB gz, 0.9-3.3 M logs/s native, ARI = 1.0 vs the published Python reference on five public log corpora. Cross-validates bit-for-bit with the TS port at src/drain.ts. M2 graduation work — moving the crate into packages/o11y-codec-rt/drain/, adding a configurable masker, and adding persistable state — pending; see dev-docs/drain-prototype.md. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add packages/o11ylogsdb to root build:packages and typecheck:packages. - Ignore packages/*/dist-bench/ in gitignore (compiled bench output). - Biome: ignore **/dist-bench; disable noNonNullAssertion on the o11ylogsdb hot-path codec sources and bench harness (intentional array[i]! in tight decode loops). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
e18f2b0 to
7681304
Compare
- engine.ts stats(): use new chunkWireSize() helper instead of serializeChunk(c).length. Avoids materializing the full wire buffer just to count bytes; only the JSON header is encoded. Fixes a bot-flagged bottleneck that grew with chunk count. - chunk.ts: add chunkWireSize() — header-JSON length + payload length + 9-byte preamble. - package-lock.json: regenerate so `npm ci` picks up o11ylogsdb. The CI validate jobs were failing on missing lockfile entry. - Strip dangling "Experiment X" labels from src/, bench/, PLAN.md, and the corpus generator. The experiment write-ups they pointed to were removed when the research dump became dev-docs/. The surrounding text already explains the why. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| stats(): StoreStats { | ||
| let chunks = 0; | ||
| let totalLogs = 0; | ||
| let totalBytes = 0; | ||
| for (const id of this.streams.ids()) { | ||
| const chunkList = this.streams.chunksOf(id); | ||
| chunks += chunkList.length; | ||
| for (const c of chunkList) { | ||
| totalLogs += c.header.nLogs; | ||
| totalBytes += chunkWireSize(c); | ||
| } | ||
| } |
There was a problem hiding this comment.
⚠️ Performance: stats() calls JSON.stringify on every chunk header each invocation
The previous review flagged that stats() re-serializes every chunk. The commit message says "stats() perf" was fixed, but the current code still calls chunkWireSize(c) inside a nested loop, which per chunk.ts:211-214 runs JSON.stringify on each chunk's header on every call. For a store with thousands of chunks, this is O(n) stringification work on a read-only summary method.
Consider caching wireSize on the Chunk object at freeze time (it's immutable after that), or maintaining a running total in appendChunk()/flush() so stats() is O(1).
Suggested fix:
// In engine.ts, maintain running totals:
private _totalChunkBytes = 0;
private _totalLogs = 0;
private _chunkCount = 0;
// In appendChunk calls (flush + append):
this._totalChunkBytes += chunkWireSize(chunk);
this._totalLogs += chunk.header.nLogs;
this._chunkCount++;
// stats() becomes O(1):
stats(): StoreStats {
return { streams: this.streams.size(), chunks: this._chunkCount,
totalLogs: this._totalLogs, totalChunkBytes: this._totalChunkBytes,
bytesPerLog: this._totalLogs > 0 ? this._totalChunkBytes / this._totalLogs : 0 };
}
Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion
| *iterRecords(): Generator<{ streamId: StreamId; records: LogRecord[] }> { | ||
| for (const id of this.streams.ids()) { | ||
| const policy = this.policyFor(id); | ||
| for (const chunk of this.streams.chunksOf(id)) { | ||
| yield { streamId: id, records: readRecords(chunk, this.registry, policy) }; | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
💡 Edge Case: iterRecords() silently drops unflushed in-flight records
iterRecords() only yields records from frozen chunks in the stream registry. Any records appended but not yet flushed (i.e. the current in-flight ChunkBuilder) are silently omitted. The bench code in engine-columnar.bench.ts:117 correctly calls store.flush() before iterating, but the engine API doesn't document or enforce this — a caller using iterRecords() without flush() will get incomplete data with no warning.
Consider either auto-flushing at the start of iterRecords(), or adding a JSDoc note that flush() must be called first.
Suggested fix:
// Option A: auto-flush
*iterRecords(): Generator<{ streamId: StreamId; records: LogRecord[] }> {
this.flush();
for (const id of this.streams.ids()) {
// ...
}
}
Was this helpful? React with 👍 / 👎 | Reply gitar fix to apply this suggestion
|
Note Your trial team has used its Gitar budget, so automatic reviews are paused. Upgrade now to unlock full capacity. Comment "Gitar review" to trigger a review manually. Code Review
|
| Compact |
|
Important
Your trial ends in 6 days — upgrade now to keep code review, CI analysis, auto-apply, custom automations, and more.
Was this helpful? React with 👍 / 👎 | Gitar
Summary
Adds
packages/o11ylogsdb— a browser-native (also Node, Bun, Edge) logs database for OpenTelemetry, sister too11ytsdb. Goal: 20× storage efficiency vs raw OTLP/JSON.ChunkPolicy, per-(resource, scope) stream registry, baseline + columnar + Drain + typed-columnar policies, streaming query executor with header-only chunk pruning.packages/o11ylogsdb/rust-prototype/drain/. 6.7 KB gz, 0.9–3.3 M logs/s native, ARI = 1.0 vs the published Python reference on five public log corpora. TS port atsrc/drain.tsis bit-identical.ColumnarDrainPolicy+TypedColumnarDrainPolicywith generic byte-shape detectors (SIGNED_INT, UUID, UUID_NODASH, PREFIXED_INT64, PREFIXED_UUID, TIMESTAMP_DELTA). Engine doesn't assume the log format.bench/, public Loghub-2k corpora + a synthetic structured-JSON corpus checked in.dev-docs/findings.md,dev-docs/techniques.md,dev-docs/drain-prototype.mdcapture the validated/refuted assumptions and the codec/index/query stack we ship. No vendor coupling — algorithms are described by their published names; product names of comparator systems are not used.PR is no longer a draft. Milestones M0/M1/M5/M6/M7 are partially started or pending — see README for the status table. Ship is the design foundation: engine round-trips, query executor works against zone maps, M2 prototype is validation-complete.
Key files
packages/o11ylogsdb/PLAN.md— milestones, gates, architectural decisions.packages/o11ylogsdb/dev-docs/findings.md— what we measured.packages/o11ylogsdb/dev-docs/techniques.md— what we ship and why.packages/o11ylogsdb/dev-docs/drain-prototype.md— the M2 Rust port.packages/o11ylogsdb/bench/README.md— bench modules and corpora.Review-pass fixes
LogStore.stats()now uses a cheapchunkWireSize()helper instead of materializing each chunk's full wire buffer (bot-flagged perf bottleneck).package-lock.jsonregenerated sonpm cipicks up the new package.Test plan
npm run typecheck:packages— passesnpm run build:packages— passesnpx vitest run --coverage— 32 files, 479 tests passed (sibling packages; o11ylogsdb has no unit tests yet — design-validated via bench)npm run check:release— exit 0npx biome check packages/o11ylogsdb— 0 warnings🤖 Generated with Claude Code