Telemetry Pipeline

Alpha Preview"I'm Logging it."

Strike48's telemetry pipeline (McLogs) is a single high-performance Rust service that ingests logs, metrics, and traces, normalizes and routes them in-flight, and delivers them durably to a streaming layer (for real-time consumers and replay) and a lakehouse (DuckLake, for analytics).

The three signals have different dominant query shapes, so each is headed for a purpose-built storage path. Today, all three land in DuckLake — Parquet on object storage, registered in a Postgres catalog — delivered at-least-once, with a streaming copy for real-time consumers.

Roadmap

The signal-specific storage designs below — using the catalog as a hot tier, secondary indexes, metric rollups, and a trace index — are the design direction. Several are proposed and not yet shipped; each section notes what is planned. Today the catalog holds file-level metadata; it is not yet a hot tier and there are no secondary indexes.

## Pipeline overview
McLogs ingestor (single Rust binary) OTLP gRPC / HTTP HTTP POST (JSON) syslog TCP - TLS/mTLS Streaming layerreal-time - replay DuckLakeParquet + Postgres catalog Sources VRL Remapnormalize VRL Routefan-out WAL(per sink) Streaming sink WAL(per sink) DuckLake sink
McLogs ingestor (single Rust binary) OTLP gRPC / HTTP HTTP POST (JSON) syslog TCP - TLS/mTLS Streaming layerreal-time - replay DuckLakeParquet + Postgres catalog Sources VRL Remapnormalize VRL Routefan-out WAL(per sink) Streaming sink WAL(per sink) DuckLake sink

Each sink has its own write-ahead log — the append happens as events cross from the parser to that sink, so the two destinations buffer independently. The WAL holds events until the sink acknowledges and replays unacked events on crash, giving at-least-once delivery to both.

Logs — the firehose path

Roadmap

The firehose design below is planned. Today, logs are batched and written as Parquet files registered in the DuckLake catalog. The catalog-as-hot-tier and secondary indexes are not implemented yet.

Logs are an **append-only firehose**. The planned design skips the traditional "ingest → buffer → compact" loop: small batches land **inline in the Postgres catalog** and only flush to Parquet once a batch reaches the target Parquet size (`target_file_size`, ~128 MB). The catalog becomes the **hot tier**, object storage the **cold tier**, queryable as one table.
inline · planned reaches target_file_size (~128 MB) Log events Batch Postgres catalog — hot tier · plannedsub-second visibilitysecondary indexes · planned Parquet on object storecold tier — fewer, larger files One logs tableUNION of inline + Parquet SQL
inline · planned reaches target_file_size (~128 MB) Log events Batch Postgres catalog — hot tier · plannedsub-second visibilitysecondary indexes · planned Parquet on object storecold tier — fewer, larger files One logs tableUNION of inline + Parquet SQL
  • Fewer, larger Parquet files — flushing at target_file_size (~128 MB) rather than on every small batch means better query planning than thousands of tiny files.
  • Planned — sub-second visibility — recent logs would live inline in the catalog DB, queryable immediately (no flush lag).
  • Planned — secondary indexes on trace_id, request_id, user_id, maintained in Postgres while rows are still inline. Not implemented today.

Metrics — the tiered-rollup path

Roadmap

Proposed design. Today, metrics are stored through the logs schema (values in JSON); the typed-column and tiered-rollup design below is planned.

McLogs doesn't try to replace Prometheus for *live* metrics — it's the **long-term tier**. The novel part: **downsampling is part of the write path**, not a separate job. One atomic multi-table commit maintains raw + rollups together, so there's no separate compaction job with its own failure modes.
Metric eventstyped: gauge / counter / histogram Atomic multi-table commit raw(ages out) 1m 5m 1h 1d (kept forever) Queryauto-selects resolution
Metric eventstyped: gauge / counter / histogram Atomic multi-table commit raw(ages out) 1m 5m 1h 1d (kept forever) Queryauto-selects resolution
  • Typed columns, not JSON — values are stored as native typed metrics (gauge/counter/histogram), avoiding the per-row json_extract cost that dominates analytical queries.
  • Transactional rollups — raw, 1m, 5m, 1h, and 1d resolutions are written in a single transaction; a query automatically reads the coarsest resolution that answers it.
  • Retention by tier — raw data ages out; rollups are kept indefinitely.

Traces — catalog as index

Roadmap

Proposed design — there is no traces storage path today. The trace_id index and hot-trace tier below are planned.

Traces are treated as a **key-value lookup**, not a search problem: the dominant query is "give me trace `abc123`." A real **Postgres index on `trace_id`** points at exact Parquet row groups, so a lookup is an index seek plus one object-store read.
row-group coordinates Get trace abc123 Postgres trace_indextrace_id -> (parquet_file, row_group) Parquet on object storespan-per-row, sorted by (service, trace_id) Last 1h inline in catalog(hot traces) Spans Analyticslatency p99, service maps
row-group coordinates Get trace abc123 Postgres trace_indextrace_id -> (parquet_file, row_group) Parquet on object storespan-per-row, sorted by (service, trace_id) Last 1h inline in catalog(hot traces) Spans Analyticslatency p99, service maps
  • Point lookup by trace_id — Postgres index seek → row-group coordinates → a single object-store GET.
  • Analytics stay columnar — percentiles, service maps, and error rates run over the span table.
  • Hot traces (last hour) live inline in the catalog for sub-second visibility; sampling stays at the collector, not the storage layer.

Distributed DuckLake: one catalog, two sides

DuckLake is normally single-process DuckDB. McLogs makes it distributed by putting the catalog in Postgres and the data in an object store — so many writers and readers share one lake and coordinate only through catalog transactions. Crucially, ingest and query are separate parts: the write path and read path never talk to each other; they meet only at the shared catalog and storage, and scale independently.

INGEST - write path SHARED LAKE QUERY - read path Ingestor A Ingestor B Postgres catalogmetadata + file index Object storeParquet files - cold tier mclogs-queryArrow Flight + HTTP DuckDB / DataFusion
INGEST - write path SHARED LAKE QUERY - read path Ingestor A Ingestor B Postgres catalogmetadata + file index Object storeParquet files - cold tier mclogs-queryArrow Flight + HTTP DuckDB / DataFusion
  • Ingest (write path) — buffers events → Arrow → Parquet (atomic write-then-rename) → registers files in the catalog → acks. Multiple ingestors write concurrently.
  • Query (read path) — a separate service (mclogs-query, Arrow Flight + HTTP) resolves file/row-group locations from the catalog, then reads only the needed Parquet row groups. Adding query capacity never touches ingest.
  • The catalog is the contract — a Postgres-native DuckLake catalog (no separate catalog service) is the single coordination point and file index for the lake. (The per-signal hot-tier and secondary-index designs above build on this catalog but are not yet shipped.)
Destination Guarantee Mechanism
Streaming layer At-least-once WAL → producer ack → finalizer
DuckLake At-least-once WAL → Parquet write → catalog commit → finalizer