Skip to content

Commit 6f23583

Browse files
Remove most long window metric alert rule options (#8241)
1 parent 68361c9 commit 6f23583

7 files changed

Lines changed: 28 additions & 29 deletions

File tree

packages/migrations/src/clickhouse-actions/019-metric-alert-target-daily-rollup.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import type { Action } from '../clickhouse';
22

33
// Daily by-target rollup for the metric-alert evaluator. Mirrors the
44
// minutely/hourly rollups in 018 but bucketed per day, so long-window rules
5-
// (>= 7 days) read ~60 daily buckets for a 30-day query instead of ~1,440
6-
// hourly ones. The 90-day TTL covers the 60-day (2x window) scan of a 30-day
7-
// rule.
5+
// (the 7-day max) read ~14 daily buckets (current + previous window) instead of
6+
// ~336 hourly ones. The 90-day TTL comfortably covers the 14-day (2x window)
7+
// scan of a 7-day rule.
88
const tableColumns = `
99
target LowCardinality(String) CODEC(ZSTD(1)),
1010
timestamp DateTime('UTC') CODEC(DoubleDelta, LZ4),

packages/services/api/src/modules/alerts/providers/metric-alert-rules-manager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,7 @@ export class MetricAlertRulesManager {
337337
timeWindowMinutes > METRIC_ALERT_RULE_TIME_WINDOW_MAX_MINUTES
338338
) {
339339
throw new MetricAlertRuleValidationError(
340-
`Time window must be a whole number of minutes between ${METRIC_ALERT_RULE_TIME_WINDOW_MIN_MINUTES} and ${METRIC_ALERT_RULE_TIME_WINDOW_MAX_MINUTES} (30 days).`,
340+
`Time window must be a whole number of minutes between ${METRIC_ALERT_RULE_TIME_WINDOW_MIN_MINUTES} and ${METRIC_ALERT_RULE_TIME_WINDOW_MAX_MINUTES} (7 days).`,
341341
);
342342
}
343343
// Windows at/above the daily-rollup threshold read whole-day buckets, so they

packages/services/api/src/modules/commerce/constants.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,19 @@ export const METRIC_ALERT_RULES_PER_TARGET_LIMIT = 10;
4242
* Min = 1: the evaluator cron runs every minute, so a sub-1-minute window
4343
* never gets a fresh evaluation and the rule's behaviour is undefined.
4444
*
45-
* Max = 30 days: rules with longer windows query ClickHouse's
46-
* `operations_hourly` rollup, which is bounded by the retention policy. 30
47-
* days is the practical ceiling beyond which queries start scanning data
48-
* that may already be aged out.
45+
* Max = 7 days: the product ceiling for alert windows. At the max the window
46+
* equals METRIC_ALERT_RULE_DAILY_ROLLUP_THRESHOLD_MINUTES, so it reads
47+
* ClickHouse's whole-day rollup and must be a whole number of days (7d
48+
* qualifies). Longer windows added little alerting value and pushed queries
49+
* toward the edge of the retention policy.
4950
*
5051
* The UI form's `Select` exposes a smaller subset of this range for UX
5152
* reasons; these constants are the absolute bounds the API enforces against
5253
* any caller (form, seed scripts, customer integrations).
5354
*/
5455
export const MINUTES_PER_DAY = 24 * 60; // 1440
5556
export const METRIC_ALERT_RULE_TIME_WINDOW_MIN_MINUTES = 1;
56-
export const METRIC_ALERT_RULE_TIME_WINDOW_MAX_MINUTES = 30 * MINUTES_PER_DAY; // 43200
57+
export const METRIC_ALERT_RULE_TIME_WINDOW_MAX_MINUTES = 7 * MINUTES_PER_DAY; // 10080
5758

5859
// Windows >= this read the daily ClickHouse rollup (whole-day buckets), so they
5960
// must be a whole number of days. Mirrors DAILY_THRESHOLD_MINUTES in the workflows

packages/services/workflows/src/lib/metric-alert-evaluator.spec.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -76,8 +76,8 @@ describe('groupRulesByQuery', () => {
7676

7777
test('at >= 7d a TRAFFIC rule and a latency rule split into hourly + daily groups', () => {
7878
const groups = groupRulesByQuery([
79-
makeRule({ id: 'a', type: 'TRAFFIC', timeWindowMinutes: 43200 }),
80-
makeRule({ id: 'b', type: 'LATENCY', metric: 'P95', timeWindowMinutes: 43200 }),
79+
makeRule({ id: 'a', type: 'TRAFFIC', timeWindowMinutes: 10080 }),
80+
makeRule({ id: 'b', type: 'LATENCY', metric: 'P95', timeWindowMinutes: 10080 }),
8181
]);
8282
expect(groups.size).toBe(2);
8383
const tiers = [...groups.values()]
@@ -104,7 +104,6 @@ describe('evaluationIntervalMinutes', () => {
104104
expect(evaluationIntervalMinutes(1440)).toBe(15);
105105
expect(evaluationIntervalMinutes(1441)).toBe(30);
106106
expect(evaluationIntervalMinutes(10080)).toBe(30);
107-
expect(evaluationIntervalMinutes(43200)).toBe(30);
108107
});
109108
});
110109

@@ -125,7 +124,6 @@ describe('resolutionFor', () => {
125124
expect(resolutionFor(DAILY_THRESHOLD_MINUTES, true)).toBe('daily');
126125
// allowDailyRollup=false (a TRAFFIC group) never reaches daily.
127126
expect(resolutionFor(DAILY_THRESHOLD_MINUTES, false)).toBe('hourly');
128-
expect(resolutionFor(43200, false)).toBe('hourly');
129127
});
130128
});
131129

@@ -136,13 +134,13 @@ describe('isRuleDue', () => {
136134
const ago = (minutes: number) => new Date(evalTime.getTime() - minutes * 60_000).toISOString();
137135

138136
test('a never-evaluated rule is always due', () => {
139-
expect(isRuleDue(makeRule({ lastEvaluatedAt: null, timeWindowMinutes: 43200 }), evalTime)).toBe(
137+
expect(isRuleDue(makeRule({ lastEvaluatedAt: null, timeWindowMinutes: 10080 }), evalTime)).toBe(
140138
true,
141139
);
142140
});
143141

144-
test('30-day rule: due once its 30-min interval has elapsed', () => {
145-
const base = { timeWindowMinutes: 43200, state: 'NORMAL' as const };
142+
test('7-day rule: due once its 30-min interval has elapsed', () => {
143+
const base = { timeWindowMinutes: 10080, state: 'NORMAL' as const };
146144
expect(isRuleDue(makeRule({ ...base, lastEvaluatedAt: ago(0) }), evalTime)).toBe(false);
147145
expect(isRuleDue(makeRule({ ...base, lastEvaluatedAt: ago(29) }), evalTime)).toBe(false);
148146
expect(isRuleDue(makeRule({ ...base, lastEvaluatedAt: ago(30) }), evalTime)).toBe(true);
@@ -153,7 +151,7 @@ describe('isRuleDue', () => {
153151
const almost = new Date(evalTime.getTime() - (30 * 60_000 - 5_000)).toISOString();
154152
expect(
155153
isRuleDue(
156-
makeRule({ timeWindowMinutes: 43200, state: 'NORMAL', lastEvaluatedAt: almost }),
154+
makeRule({ timeWindowMinutes: 10080, state: 'NORMAL', lastEvaluatedAt: almost }),
157155
evalTime,
158156
),
159157
).toBe(false);
@@ -168,13 +166,13 @@ describe('isRuleDue', () => {
168166
test('PENDING/RECOVERING keep full 1-min resolution regardless of window', () => {
169167
for (const state of ['PENDING', 'RECOVERING'] as const) {
170168
expect(
171-
isRuleDue(makeRule({ timeWindowMinutes: 43200, state, lastEvaluatedAt: ago(1) }), evalTime),
169+
isRuleDue(makeRule({ timeWindowMinutes: 10080, state, lastEvaluatedAt: ago(1) }), evalTime),
172170
).toBe(true);
173171
}
174-
// The same 30-day rule in a steady state, 1 min after eval, is NOT due.
172+
// The same 7-day rule in a steady state, 1 min after eval, is NOT due.
175173
for (const state of ['NORMAL', 'FIRING'] as const) {
176174
expect(
177-
isRuleDue(makeRule({ timeWindowMinutes: 43200, state, lastEvaluatedAt: ago(1) }), evalTime),
175+
isRuleDue(makeRule({ timeWindowMinutes: 10080, state, lastEvaluatedAt: ago(1) }), evalTime),
178176
).toBe(false);
179177
}
180178
});
@@ -337,7 +335,7 @@ describe('queryClickHouseWindows', () => {
337335
{ clientFilters: [{ name: 'web', versions: null }] },
338336
makeLogger().logger,
339337
);
340-
await queryClickHouseWindows(clickhouse, target, 43200, conds, evalTime);
338+
await queryClickHouseWindows(clickhouse, target, DAILY_THRESHOLD_MINUTES, conds, evalTime);
341339
const { sql } = calls[0];
342340
expect(sql).toContain('FROM operations_daily');
343341
expect(sql).not.toContain('_by_target');

packages/services/workflows/src/lib/metric-alert-evaluator.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ export function evaluationIntervalMinutes(timeWindowMinutes: number): number {
252252
if (timeWindowMinutes <= 60) return 1; // ≤ 1h window: every tick (unchanged)
253253
if (timeWindowMinutes <= 360) return 5; // ≤ 6h window: every 5 min
254254
if (timeWindowMinutes <= 1440) return 15; // ≤ 24h window: every 15 min
255-
return 30; // > 24h (7d, 30d): every 30 min
255+
return 30; // > 24h (up to the 7d max): every 30 min
256256
}
257257

258258
// PENDING/RECOVERING rules stay at 1-min resolution so the confirmationMinutes

packages/web/app/src/components/target/alerts/alert-form.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -224,8 +224,6 @@ const RANGE_OPTIONS = [
224224
{ value: '360', label: '6h' },
225225
{ value: '1440', label: '1d' },
226226
{ value: '10080', label: '7d' },
227-
{ value: '20160', label: '14d' },
228-
{ value: '43200', label: '30d' },
229227
] as const;
230228

231229
// Condition labels depend on threshold type. For a fixed value the metric is
@@ -583,7 +581,7 @@ export function AlertForm(props: AlertFormProps) {
583581

584582
const previewWindowMinutes = Math.min(
585583
(parseInt(watchedValues.timeWindowMinutes, 10) || 10_080) * 2,
586-
43_200,
584+
20_160,
587585
);
588586
const { period, resolution } = useMemo(() => {
589587
const now = new Date();

packages/web/app/src/components/target/alerts/alert-metric-chart.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,12 @@ const SEVERITY_COLOR_KEY: Record<string, 'critical' | 'warning' | 'info'> = {
8181
INFO: 'info',
8282
};
8383

84-
// The preview fetch span is capped at 30 days (see `previewWindowMinutes` in
85-
// alert-form.tsx), so for windows longer than 15 days the "previous" window
86-
// falls outside the fetched data and can't be drawn or compared.
87-
const PREVIEW_SPAN_CAP_MINUTES = 43_200;
84+
// The preview fetch span is capped at 14 days (see `previewWindowMinutes` in
85+
// alert-form.tsx), so for windows longer than 7 days the "previous" window
86+
// falls outside the fetched data and can't be drawn or compared. With the rule
87+
// window itself capped at 7d, this always holds, but the guard stays as a
88+
// backstop against a larger `timeWindowMinutes` reaching the chart.
89+
const PREVIEW_SPAN_CAP_MINUTES = 20_160;
8890

8991
const MS_PER_MINUTE = 60_000;
9092

0 commit comments

Comments
 (0)