fix(warehouse): route alerts to the minutely rollup and sweep the MV set - #559
Merged
Conversation
Two of these are bugs, not cleanup.
Alert evaluation could never reach the tier built for it.
`canUseAnnualServiceOverview` required `allMetrics === true`, which
`computeAlertBuckets` never sets, so every alert fell through to a flat
scan of the 325M-row per-span `service_overview_spans` while the 2.2M-row
`service_overview_minutely` sat unreachable. That flag is not a capability
limit: it only picks which aggregates the raw paths bother computing, and
the rollup tiers produce all five MetricNeeds unconditionally. Hour-multiple
buckets still yield to `traces_aggregates_hourly`, which stores
sample-WEIGHTED quantile states where these tiers store unweighted ones.
`span_metrics_calls_hourly` held 0 rows for its entire existence. The MV
filtered `MetricName IN ('span.metrics.calls', 'calls')`, but the collector
emits the counter namespaced by its pipeline as `traces.span.metrics.calls`
(~880k rows / 2 days, 30 services). Write filter and read fast-path missed
it identically, so they agreed with each other and disagreed with reality,
and every read fell back to the raw scan measured at ~7s p95.
Also in migration 0019 / local schema v9:
- Drop `error_spans` and its MV. No query ever called `from(ErrorSpans)`,
yet `warehouse-catalog.ts` still told LLM agents to prefer it over
`traces WHERE StatusCode = 'Error'`.
- Bound `attribute_values_hourly` cardinality. With `AttributeValue != ''`
as its only filter it had reached 1.59B rows / 12.3 GB to serve an
autocomplete dropdown read a couple hundred times a week. The rules come
from measuring the table, not from intuition: a length cap and an id-key
denylist would both have missed `idle_ns` and `busy_ns`, short numeric
measurements that were ~70% of the rows. Drops 93.6% of rows and zero
values for every canonical OTel key.
- Drop the three unread `Events*` columns from `trace_detail_spans`.
Alert windows now snap to the tick boundary, which excludes the
still-filling current minute and lets rules over one signal share a
`qe-evaluate` entry; its TTL was 30s, shorter than the 60s tick, so no
entry could outlive the tick that created it.
Adds docs/warehouse-rollups.md with the tiering contract and the four rules
whose absence produced each finding above.
… v9 edge The migration installed the narrowed v9 schema and assumed that removed the three `Events*` columns. It does not: the bundled DDL is `CREATE TABLE IF NOT EXISTS`, so on a store where `trace_detail_spans` already exists it is a no-op and the wide v8 table survives. `assertPhysicalSchema` then fails the edge with `unexpected column EventsTimestamp`. This is the same `IF NOT EXISTS` trap the edge already handles for the views — which is why they are dropped explicitly — applied to a table rather than a view. Issue `ALTER TABLE ... DROP COLUMN IF EXISTS` alongside them, mirroring what ClickHouse migration 0019 does and the shape v6 -> v7 uses for its ADDs. Also bumps the head identity pinned in the native probe to v9, which its own comment asks for in lockstep with LOCAL_SCHEMA_VERSION. Both native steps now pass locally: the migration reaches step 9/9 and reports 39 views, and the checkpoint smoke round-trips.
🍁 Maple PR previewNote Preview resources were removed when this pull request closed. Final commit |
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.
Started as "which of our 40 materialized views are actually useful?" Two of the findings turned out to be bugs rather than bloat, so this is mostly a fix.
Everything below is measured — production storage from
datasources_storage, and a week of read traffic from our ownquery.contextspans. Read time is ~97 CPU-hours/week, of which ~91 is background automation; interactive UI traffic is under 5%. So MV value is decided by what the alerting and error-tick loops do.Alert evaluation could never reach the tier built for it
canUseAnnualServiceOverviewrequiredallMetrics === true.computeAlertBucketsnever sets it, so all 372k alert evaluations/week fell through to a flat scan of the 325M-row per-spanservice_overview_spans— 165k scans per 3 days — while the 2.2M-rowservice_overview_minutely, built for exactly that shape, sat unreachable.allMetricsis not a capability limit. It only picks which aggregates the raw paths bother computing (metricSelectExprs); the rollup tiers read pre-aggregated columns and produce all fiveMetricNeeds unconditionally, so everyTracesMetricis serveable. The flag was encoding "this is the dashboard caller."Hour-multiple buckets are deliberately yielded back to
traces_aggregates_hourly: it stores sample-weighted quantile states where these tiers store unweighted ones, so claiming them would have silently downgraded accuracy under heterogeneous sampling. Only the sub-hour case is claimed, where the alternative is the per-span scan.span_metrics_calls_hourlyheld 0 rows for its entire existenceI was about to delete this as dead. A code comment citing a measured ~7s p95 didn't fit that story, so I checked: the metric is live as
traces.span.metrics.calls— ~880k rows / 2 days across 30 services, monotonic. The MV filteredMetricName IN ('span.metrics.calls', 'calls'), because spanmetricsconnector output is namespaced by its pipeline.The write filter and the read fast-path missed it identically, so they agreed with each other and disagreed with reality. Fixed on both sides rather than deleted.
Sweep (migration 0019 / local schema v9)
error_spans+ its MV. No query ever calledfrom(ErrorSpans), yetwarehouse-catalog.tsstill described it to LLM agents as "use this instead oftraces WHERE StatusCode = 'Error'" — sorun_sqlagents were actively steered onto a table no product code reads.attribute_values_hourlycardinality. WithAttributeValue != ''as its only filter it had reached 1.59B rows / 12.3 GB to serve an autocomplete dropdown read ~217 times a week. The rules come from measuring the table: a length cap and an id-key denylist — the two intuitive mechanisms — would both have missedidle_nsandbusy_ns, short numeric measurements that were ~70% of the rows. Verified against production: 93.6% of rows dropped, zero values lost forhttp.response.status_code,http.method,db.system,service.name,deployment.environment,http.route,server.addressand every other canonical OTel key.attribute_keys_hourlyis untouched, so all keys stay discoverable and filterable.Events*columns fromtrace_detail_spans(1.19 GB). Confirmed unused by all nine readers, including the two AI-session queries inquery-engine-integrations.metrics_exponential_histogramis empty but is written by ingest — it's an unused capability, not dead code. Recorded in the doc so it isn't re-flagged.Alert evaluation cache
buildEvaluateCacheKeysnapped to 15s whileendMs = nowand the tick runs every 60s, and the TTL was 30s — shorter than the tick that created the entry. Windows now snap to the tick boundary, which also excludes the still-filling current minute, and the TTL clears one tick.Reviewer note: this adds up to one tick of lag to alert evaluation, bounded by the tick period the scheduler already imposes. It's the one deliberate behaviour trade-off here. It does not collapse the per-service fan-out (each service compiles a distinct plan); it lets rules over one identical signal — e.g. warn-at-100 and page-at-500 — share an entry.
Verification
DROP VIEW→DROP TABLEordering (the inverse wedges ingest withCode: 60).countandspanCount, 1,686,684 spans matched exactly.One test worth flagging:
ch.test.tsassertedtoContain("FROM service_overview_spans")for this route. The tiered union reads that table too, as its raw edge — so the assertion passed on both routes and could never have caught this. It now asserts the tier that distinguishes them.Scope
alertRawQuery— 173,000 s/week, now the largest remaining consumer by far — is untouched. It's user-authored SQL against raw tables, so no MV can see it and no routing guard applies. It and the other open items (trace_detail_spansTTL, theservice_map_spansamplification) are recorded indocs/warehouse-rollups.md.That doc is the durable half of this: the tier ladder, the three shapes that justify an MV, and the four rules whose absence produced each finding above.
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.