Trace IDs Are Opaque. Storage Still Needs a Time Signal.
The first useful clue in our ClickHouse schema was not a query. It was this line:
Real production work debriefed after the fact — what broke, what it cost, and the lessons carried forward.
8 published entries
These series give this subject an intentional reading order.
A reading order through the rebuilt tracing pipeline: Kafka delivery, Go processing, Redis coordination, ClickHouse analytics, and Quickwit search.
See the reading order →The first useful clue in our ClickHouse schema was not a query. It was this line:
A child span can reach the aggregate processor before its root. If the root’s timestamp falls in the previous month, publishing both rows under their own timestamps splits the trace across monthly partitions.
I had moved trace ingestion away from PostgreSQL, split large payloads before Kafka rejected them, and separated text search from analytical reads. ClickHouse accepted the rows without drama. Then I tried to show one trace, list a feature’s recent runs, and chart latency by day. Each read wanted the data arranged differently.
I needed three ingestion services, but I did not need three Go binaries.
The collector had CPU headroom and responsive Kafka producers, yet one OTLP batch made the hot path queue. The parser retained the whole payload while four downstream writers finished.
A record-size test exposed an accounting error: an 870 KiB text field fit, while an 880 KiB field did not.
The logs table stored trace IDs, users, token counts, latency, prompt input, model output, metadata, and variables. We used the same table to show one trace, graph cost by day, filter a feature’s recent spans, and find text a user remembered typing three weeks ago.
At 6 p.m. on December 19, 2025, the product was down again.