Skip to content

Additional metric alert clickhouse performance improvements#8201

Draft
jonathanawesome wants to merge 20 commits into
mainfrom
additional-metric-alert-clickhouse-performance-improvements
Draft

Additional metric alert clickhouse performance improvements#8201
jonathanawesome wants to merge 20 commits into
mainfrom
additional-metric-alert-clickhouse-performance-improvements

Conversation

@jonathanawesome

@jonathanawesome jonathanawesome commented Jul 9, 2026

Copy link
Copy Markdown
Member

This PR flattens the climbing ClickHouse query duration and total run time of the evaluateMetricAlertRules cron by cutting how often long-window rules run, how many buckets each query scans, and how much work each query does. It also adds Grafana dash panels to keep watch on the number of rules with filters attached (expensive).

  • Cadence gating: long-window rules evaluate less often (30-day rules ~every 30 min instead of every 60s) via last_evaluated_at, keeping full 1-min resolution during PENDING/RECOVERING so confirmation dwell is unaffected.
  • Daily rollup + routing: new operations_by_target_daily ClickHouse rollup; windows ≥7 days read a daily table (~60 buckets for a 30-day query instead of ~1,440), fixing both per-query cost and the 30-day hourly-retention cliff for long windows.
  • Traffic accuracy: TRAFFIC-count rules stay on the hourly table, because a rolling window's edges don't fall on day boundaries, so a daily aggregate rounds them to whole days and adds or drops up to a full edge day of traffic (a real error for an absolute count threshold, though it barely moves a latency percentile); groups split by rollup tier so a co-located latency rule still gets the daily win.
  • Skip the previous window for absolute-only groups (1× scan instead of 2×); FIXED_VALUE rules persist a null previous value consistently regardless of group-mates so the UI responds accordingly.
  • Skip unused duration columns for error/traffic groups, so ClickHouse never reads the heavy percentile blob when no latency rule needs it.
  • Concurrency pool replaces the batch barrier so one slow group can't stall cheaper ones (including across orgs).
  • Whole-day validation: windows ≥7 days must be a whole number of days, guarding direct API callers so daily buckets never silently round a window.
  • Fail loud if a latency rule ever reads a duration column its query didn't select, instead of silently reading 0 (which renders as 0ms and never fires). Not reachable today, but the column-skipping optimization above is what makes a select/read mismatch possible, so this guards against a future desync.
  • Observability: added data to the evaluate-group spans (metric/threshold types, whether a filter is applied) so slow traces are self-describing in TraceQL, plus filtered-vs-unfiltered rule and group Prometheus gauges with matching Metric-Alerts dashboard panels.
  • Deploy ordering: the workflows service now deploys after DB migrations so the daily table exists before the new route goes live.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request optimizes metric alert evaluations for long-window rules (>= 7 days) by introducing a daily ClickHouse rollup table (operations_by_target_daily) and materialized view. It updates the workflows evaluator to query this rollup, skip previous window scans for absolute-only rules, avoid reading unused duration columns, and run evaluations on a tiered interval. It also adds Prometheus metrics, Grafana dashboards, and API validation for these windows. Feedback is provided regarding a ClickHouse query issue where grouping by the alias timestamp instead of toStartOfDay(timestamp) will cause ClickHouse to resolve to the high-resolution source column, preventing proper daily aggregation.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🚀 Snapshot Release (alpha)

The latest changes of this PR are available as alpha on npm (based on the declared changesets):

Package Version Info
hive 11.5.0-alpha-20260709221036-1ba000f75e5b3808955d19d41f7aaefbc89ed131 npm ↗︎ unpkg ↗︎

@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

🐋 This PR was built and pushed to the following Docker images:

Targets: build

Platforms: linux/amd64

Image Tags: 1ba000f75e5b3808955d19d41f7aaefbc89ed131, 1ba000f

@jonathanawesome jonathanawesome marked this pull request as ready for review July 9, 2026 22:37
? requireColumn(row.average, 'duration_avg', rule)
: requireColumn(row.percentiles, 'duration_quantiles', rule)[
percentileIndex(rule.metric)
];

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: I think what this is doing is clear and no need for the long comments.

For requireColumn -- is this if for example, there's no data for this window? Or is it for if there's an issue in the code and this column wasnt fetched for some reason?
Could we get away with a null check and return null if it's missing?

evaluationTime: Date,
): boolean {
if (rule.state === 'PENDING' || rule.state === 'RECOVERING') return true;
if (!rule.lastEvaluatedAt) return true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can combine this condition with the others to return true.
If you want them separate for clarity on what they are checking, then you could assign a new variable, i.e. const isInPriorityState = rule.state === 'PENDING' || rule.state == 'RECOVERING';

// All rules in a group share a saved filter (it's part of the group key), so the
// filter is built once from the representative. A malformed filter yields no
// conditions (evaluates unfiltered) and is logged, isolating the failure here.
export function deriveGroupNeeds(groupRules: MetricAlertRuleRow[], logger: Logger): GroupNeeds {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function was unexpected. I anticipated more concrete query building depending on the rule.

E.g. buildLatencyAlertQuery(rule: MetricAlertRuleRow)

I think generic can be good, but as we add more rules i worry this will become spaghetti.

filteredRules += group.length;
}
}
metricAlertRuleGroups.set({ filtered: 'true' }, filteredGroups);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not following this, but it's late. I'll try to take another look tomorrow.

@kamilkisiela

Copy link
Copy Markdown
Contributor

This PR combines quite a few pretty much independent changes. Could we split work like this into smaller, focused PRs? Reviewing all of these changes together makes it difficult to understand each decision and verify performance.
Smaller PRs would be much easier to review, test, discuss, and roll back independently.

@kamilkisiela

Copy link
Copy Markdown
Contributor

There are known things that can improve performance, that we can verify in production quickly:

  • Queue instead of Promise.all(5 items)
  • query only what's needed (error rate metrics does not need duration quantiles etc)

@jonathanawesome jonathanawesome marked this pull request as draft July 13, 2026 14:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants