LAM-2215: fold trace aggregates in ClickHouse materialized views - #2284
Draft
laminar-coding-agent[bot] wants to merge 2 commits into
Draft
LAM-2215: fold trace aggregates in ClickHouse materialized views#2284laminar-coding-agent[bot] wants to merge 2 commits into
laminar-coding-agent[bot] wants to merge 2 commits into
Conversation
Move the span-derived half of the trace fold out of app-server and into two materialized views on `spans`. `traces_agg` / `traces_static` / `traces_v0` are unchanged - an MV fires per INSERT block and sees only that block, so its GROUP BY output IS the per-batch delta app-server used to write. app-server keeps writing the residue the views structurally cannot see: metadata patches, extracted agent io, and the `should_record_to_clickhouse` signal spans (`cdp_use.session`, the sole carrier of `has_browser_session`, plus skipped Claude Code spans whose tokens still count). The FULL aggregation still drives realtime SSE, debugger blocks and signals. Requires a two-release rollout: the app-server change must be live before the migration runs, or the views double-count every `sum` column. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…actly The views diverged from `TraceAggregation::from_span_pairs` in two places: - `trace_types` emitted a per-span set that only recognised SpanType 5, so a trace typed PLAYGROUND by attribute AND holding an evaluation span produced `['PLAYGROUND','EVALUATION']`. `traces_v0` ranks PLAYGROUND above EVALUATION, so that reads back as PLAYGROUND where app-server's unconditional override yields EVALUATION. Now an override, keyed on SpanType 3/4/5 (EXECUTOR/EVALUATOR/EVALUATION), with DEFAULT dropped to the empty identity. - `traces_agg_mv` stripped `lmnr_trace_output` from `metadata`, which `CHTraceAgg::encode_metadata` deliberately keeps (only `traces_static` strips it). The asymmetry is app-server's; copy it rather than harmonise it. Also records the blocking dependency on `spans.cache_read_input_tokens` / `cache_creation_input_tokens` / `reasoning_tokens`, which `traces_agg` has but no migration adds to `spans`. Verified on a scratch ClickHouse database: PLAYGROUND-only -> ['PLAYGROUND']; PLAYGROUND + Evaluator span -> ['EVALUATION'] alone; Executor-only -> ['EVALUATION']; untyped -> []; out-of-range trace_type 7 / span_type 99 -> [] and NULL root_span_type with no read poisoning; reserved-key asymmetry and out-of-order root coalescing both unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Draft — validity check for LAM-2215. Moves the span-derived half of the trace fold out of app-server and into two materialized views on
spans.Do not merge without the two-release rollout below.
What changes
traces_agg,traces_staticandtraces_v0are untouched. An MV fires per INSERT block and can only see that block, so itsGROUP BY project_id, trace_idoutput is the per-batch delta app-server used to write — the same fold-from-partials contract, the same columns, a different producer.frontend/lib/clickhouse/migrations/61_traces_mv.sql—traces_agg_mv TO traces_aggandtraces_static_mv TO traces_static.app-server/src/traces/processor.rs— the ClickHouse write path now aggregates only the spans that never reach thespanstable. The full aggregation still drives realtime SSE, debugger blocks and signals.app-server/src/ch/traces.rs—from_spansdelegates to a newfrom_span_pairs, so the same fold can run over a filtered subset without cloning spans.docs/internal/clickhouse-traces.md— new section on MV semantics and the rollout constraint.What app-server still writes, and why it must
The views see
spans. Ingestion drops three classes of span before that table, and each keeps its existing writer:POST /v1/traces/metadata)CHTraceAgg::from_metadata_patch,CHTraceStatic::from_metadata_patchinput/output_hashes)CHTraceStatic::from_agent_ioshould_record_to_clickhousesignal spansTraceAggregation→from_aggregationThat third row matters more than it looks:
cdp_use.sessionis the only carrier ofhas_browser_session, and the skipped Claude Codeanthropic.messagesspans carry tokens and costs that still count toward the trace. Both writers are deltas into the same folding tables, so the union is exact — pinned bych::traces::tests::recordable_and_residual_halves_sum_to_the_full_aggregation.Rollout — load-bearing
While an app-server that still writes full span-derived partials is running, the views double-count every
sumcolumn. ClickHouse migrations auto-run on frontend boot and app-server rolls independently, so:61run.Self-hosters get both in one image, so the window is a single boot — but it is a real window and the migration header says so.
Draft-status risks
spansrow still lands. Client sees a 5xx after a partial write;spansis a plain MergeTree with no insert dedup, so the retry duplicates every span in the batch and double-counts its tokens. The view SELECT is total today (hence thetrace_type <= 3androot_type > 8clamps), but this is a new failure mode on the hot path and is the main reason this is a draft.traces_agg/traces_staticrows are unaffected; the views only cover inserts after they exist. A cutover plan for self-hosted is sketched in the issue thread, not in this PR.traces_static.metadatanow has two writers per batch (view + residual). Both derive it from the same trace-level metadata so they agree in practice, but this widens the existing "setting metadata twice is undefined" caveat from per-trace to per-batch.root_span_name_from_pathusessplitByChar('.', path)[1], which is approximate when a span name itself contains a dot. Only affects the preview column that readerscoalescebehind the real root name.Verification
Validated on staging ClickHouse against a scratch database, using the exact file text put through
clickhouse-migrations' ownsql_queriesparser (2 clean statements):gen_ai.usage.*on non-LLM spans:span_usage_vecisSpanUsage::default()for every non-LLM span, so summing the physicalspanscolumn is the LLM gate, whereas the draft'ssimpleJSONExtractInt(attributes, …)bypassed it.trace_typesreproducesTraceAggregation's override exactly: PLAYGROUND-only →['PLAYGROUND']; PLAYGROUND attribute plus an Evaluator span →['EVALUATION']alone (a union would read back as PLAYGROUND, sincetraces_v0ranks it higher); Executor-only →['EVALUATION']; untyped →[]→DEFAULT.traces_aggkeepslmnr_trace_output,traces_staticstrips it; both striplmnr_user_task.ai.telemetry.metadata.*) and tags are preserved.root_span_name_from_pathand NULL root columns; the late root block coalesces inroot_span_id/root_span_name/root_span_typewithout clobbering anything.span_type/trace_typeare clamped, so reads don't hitUNKNOWN_ELEMENT_OF_ENUM.wait_for_async_insert=1INSERT acks — no added delay for the signal triggers that readtraces_aggright after ingest.cargo checkclean,cargo test --bin app-server ch::45 passed.🤖 Generated with Claude Code
Blocking dependency
traces_agghascache_read_input_tokens/cache_creation_input_tokens/reasoning_tokens(migration 49), but no migration in this repo adds them tospansandCHSpanhas no writer for them — they exist on staging and are all-zero. The views must fold them, so this migration cannot be applied to a fresh database until the migration adding those columns tospanslands ahead of it.