Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7a3e3b1
Gate metric-alert evaluation cadence by rule window
jonathanawesome Jul 9, 2026
381df29
Add operations_by_target_daily ClickHouse rollup migration
jonathanawesome Jul 9, 2026
62f2b95
Route metric-alert windows >= 7 days to daily rollups
jonathanawesome Jul 9, 2026
a9760ee
Reject metric-alert windows >= 7 days that are not whole days
jonathanawesome Jul 9, 2026
2d0a0c1
Make workflows service deploy after db migrations
jonathanawesome Jul 9, 2026
815eb93
Skip the previous window for absolute-only alert groups
jonathanawesome Jul 9, 2026
31fd97f
Handle null previous value in alert notifications
jonathanawesome Jul 9, 2026
6a44e11
Skip unused duration columns for error/traffic alert groups
jonathanawesome Jul 9, 2026
fdb9022
Evaluate metric-alert groups with a concurrency pool
jonathanawesome Jul 9, 2026
d2fcb93
Add rule intent to metric-alert evaluate-group spans
jonathanawesome Jul 9, 2026
bb5fc08
Add filtered-vs-unfiltered rule gauges and dashboard panels
jonathanawesome Jul 9, 2026
a8c2e6c
prettier
jonathanawesome Jul 9, 2026
20e753f
Count actually-filtering rules in the metric-alert filter gauges
jonathanawesome Jul 9, 2026
fbef784
Derive metric-alert group query needs in one helper
jonathanawesome Jul 9, 2026
bd19727
const MINUTES_PER_DAY and pin the daily-rollup threshold
jonathanawesome Jul 9, 2026
816fdbc
Fail loud when a latency alert reads an unselected column
jonathanawesome Jul 9, 2026
868cbf6
Keep traffic on hourly and split alert groups by rollup tier
jonathanawesome Jul 9, 2026
176003f
Persist null previous value for fixed-threshold alerts
jonathanawesome Jul 9, 2026
822b2f7
Merge branch 'main' into additional-metric-alert-clickhouse-performan…
jonathanawesome Jul 9, 2026
1ba000f
Merge branch 'main' into additional-metric-alert-clickhouse-performan…
jonathanawesome Jul 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
140 changes: 133 additions & 7 deletions deployment/grafana-dashboards/Metric-Alerts.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,9 +272,135 @@
"title": "evaluateMetricAlertRules task duration (p99)",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "PROM_DATASOURCE_UID" },
"description": "Enabled query groups (one ClickHouse query each) split by whether a saved filter is attached. Filtered groups read the heavier legacy tables (cost scales with the target's traffic cardinality), so watch this for growth in the more expensive with-filter population.",
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false,
"axisPlacement": "auto",
"axisSoftMin": 0,
"drawStyle": "line",
"fillOpacity": 10,
"lineInterpolation": "linear",
"lineWidth": 1,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"min": 0,
"noValue": "0",
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "short"
},
"overrides": [
{
"matcher": { "id": "byName", "options": "with filter" },
"properties": [{ "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } }]
}
]
},
"gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 },
"id": 5,
"options": {
"legend": {
"calcs": ["last", "max"],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": { "mode": "multi", "sort": "desc" }
},
"pluginVersion": "9.3.6",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "PROM_DATASOURCE_UID" },
"editorMode": "code",
"expr": "hive_metric_alert_rule_groups{filtered=\"true\"}",
"legendFormat": "with filter",
"range": true,
"refId": "A"
},
{
"datasource": { "type": "prometheus", "uid": "PROM_DATASOURCE_UID" },
"editorMode": "code",
"expr": "hive_metric_alert_rule_groups{filtered=\"false\"}",
"legendFormat": "no filter",
"range": true,
"refId": "B"
}
],
"title": "Alert query groups: filtered vs unfiltered",
"type": "timeseries"
},
{
"datasource": { "type": "prometheus", "uid": "PROM_DATASOURCE_UID" },
"description": "Enabled alert rules split by whether a saved filter is attached. Companion to the query-groups panel (a group can hold several rules that share one query).",
"fieldConfig": {
"defaults": {
"color": { "mode": "palette-classic" },
"custom": {
"axisCenteredZero": false,
"axisPlacement": "auto",
"axisSoftMin": 0,
"drawStyle": "line",
"fillOpacity": 10,
"lineInterpolation": "linear",
"lineWidth": 1,
"stacking": { "group": "A", "mode": "none" },
"thresholdsStyle": { "mode": "off" }
},
"mappings": [],
"min": 0,
"noValue": "0",
"thresholds": { "mode": "absolute", "steps": [{ "color": "green", "value": null }] },
"unit": "short"
},
"overrides": [
{
"matcher": { "id": "byName", "options": "with filter" },
"properties": [{ "id": "color", "value": { "fixedColor": "orange", "mode": "fixed" } }]
}
]
},
"gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 },
"id": 6,
"options": {
"legend": {
"calcs": ["last", "max"],
"displayMode": "table",
"placement": "bottom",
"showLegend": true
},
"tooltip": { "mode": "multi", "sort": "desc" }
},
"pluginVersion": "9.3.6",
"targets": [
{
"datasource": { "type": "prometheus", "uid": "PROM_DATASOURCE_UID" },
"editorMode": "code",
"expr": "hive_metric_alert_enabled_rules{filtered=\"true\"}",
"legendFormat": "with filter",
"range": true,
"refId": "A"
},
{
"datasource": { "type": "prometheus", "uid": "PROM_DATASOURCE_UID" },
"editorMode": "code",
"expr": "hive_metric_alert_enabled_rules{filtered=\"false\"}",
"legendFormat": "no filter",
"range": true,
"refId": "B"
}
],
"title": "Enabled rules: filtered vs unfiltered",
"type": "timeseries"
},
{
"collapsed": false,
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 16 },
"gridPos": { "h": 1, "w": 24, "x": 0, "y": 24 },
"id": 100,
"panels": [],
"title": "Traces",
Expand All @@ -296,7 +422,7 @@
},
"overrides": []
},
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 17 },
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 25 },
"id": 101,
"options": {
"cellHeight": "sm",
Expand Down Expand Up @@ -334,7 +460,7 @@
},
"overrides": []
},
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 17 },
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 25 },
"id": 102,
"options": {
"cellHeight": "sm",
Expand Down Expand Up @@ -389,7 +515,7 @@
}
]
},
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 26 },
"gridPos": { "h": 9, "w": 12, "x": 0, "y": 34 },
"id": 104,
"options": {
"legend": {
Expand Down Expand Up @@ -443,7 +569,7 @@
},
"overrides": []
},
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 26 },
"gridPos": { "h": 9, "w": 12, "x": 12, "y": 34 },
"id": 105,
"options": {
"legend": {
Expand Down Expand Up @@ -497,7 +623,7 @@
},
"overrides": []
},
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 35 },
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 43 },
"id": 106,
"options": {
"legend": {
Expand Down Expand Up @@ -541,7 +667,7 @@
},
"overrides": []
},
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 44 },
"gridPos": { "h": 9, "w": 24, "x": 0, "y": 52 },
"id": 103,
"options": {
"cellHeight": "sm",
Expand Down
1 change: 1 addition & 0 deletions deployment/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ deployWorkflows({
schema,
redis,
clickhouse,
dbMigrations,
});

const zendesk = configureZendesk({ environment });
Expand Down
8 changes: 7 additions & 1 deletion deployment/services/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { serviceLocalEndpoint } from '../utils/local-endpoint';
import { ServiceSecret } from '../utils/secrets';
import { ServiceDeployment } from '../utils/service-deployment';
import { Clickhouse } from './clickhouse';
import { DbMigrations } from './db-migrations';
import { Docker } from './docker';
import { Environment } from './environment';
import { Observability } from './observability';
Expand All @@ -29,6 +30,7 @@ export function deployWorkflows({
schema,
redis,
clickhouse,
dbMigrations,
}: {
postgres: Postgres;
observability: Observability;
Expand All @@ -41,6 +43,7 @@ export function deployWorkflows({
schema: Schema;
redis: Redis;
clickhouse: Clickhouse;
dbMigrations: DbMigrations;
}) {
const featureFlagsConfig = new pulumi.Config('featureFlags');
return (
Expand Down Expand Up @@ -75,7 +78,10 @@ export function deployWorkflows({
image,
replicas: environment.podsConfig.general.replicas,
},
[redis.deployment, redis.service],
// Depend on dbMigrations so the ClickHouse migration (which creates
// operations_by_target_daily) runs before this service starts routing
// long-window queries to it. Mirrors deployGraphQL.
[dbMigrations, redis.deployment, redis.service],
)
// PG
.withSecret('POSTGRES_HOST', postgres.pgBouncerSecret, 'host')
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { Action } from '../clickhouse';

// Daily by-target rollup for the metric-alert evaluator. Mirrors the
// minutely/hourly rollups in 018 but bucketed per day, so long-window rules
// (>= 7 days) read ~60 daily buckets for a 30-day query instead of ~1,440
// hourly ones. The 90-day TTL covers the 60-day (2x window) scan of a 30-day
// rule.
const tableColumns = `
target LowCardinality(String) CODEC(ZSTD(1)),
timestamp DateTime('UTC') CODEC(DoubleDelta, LZ4),
total UInt32 CODEC(T64, ZSTD(1)),
total_ok UInt32 CODEC(T64, ZSTD(1)),
duration_avg AggregateFunction(avg, UInt64) CODEC(ZSTD(1)),
duration_quantiles AggregateFunction(quantilesTDigest(0.75, 0.9, 0.95, 0.99), UInt64) CODEC(ZSTD(1))
`;

export const action: Action = async exec => {
await exec(`
CREATE TABLE IF NOT EXISTS default.operations_by_target_daily
(
${tableColumns}
)
ENGINE = SummingMergeTree
-- Monthly, not toYYYYMMDD like the hourly rollup: daily buckets would make
-- one partition per day. Keep monthly so a 90-day TTL has few parts.
PARTITION BY toYYYYMM(timestamp)
PRIMARY KEY (target, timestamp)
ORDER BY (target, timestamp)
TTL timestamp + INTERVAL 90 DAY
SETTINGS index_granularity = 8192, ttl_only_drop_parts = 1
`);

await exec(`
CREATE MATERIALIZED VIEW IF NOT EXISTS default.operations_by_target_daily_mv TO default.operations_by_target_daily
AS (
SELECT
target,
toStartOfDay(timestamp) AS timestamp,
CAST(count() AS UInt32) AS total,
CAST(sum(ok) AS UInt32) AS total_ok,
avgState(duration) AS duration_avg,
quantilesTDigestState(0.75, 0.9, 0.95, 0.99)(duration) AS duration_quantiles
FROM default.operations
GROUP BY
target,
timestamp
Comment thread
jdolle marked this conversation as resolved.
)
`);
};
1 change: 1 addition & 0 deletions packages/migrations/src/clickhouse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ export async function migrateClickHouse(
import('./clickhouse-actions/016-subgraph-otel-traces-cleanup'),
import('./clickhouse-actions/017-affected-app-deployments-performance'),
import('./clickhouse-actions/018-metric-alert-target-rollups'),
import('./clickhouse-actions/019-metric-alert-target-daily-rollup'),
]);

async function actionRunner(action: Action, index: number) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import type { MetricAlertRule } from '../../../shared/entities';
import { AccessError } from '../../../shared/errors';
import { Session } from '../../auth/lib/authz';
import {
METRIC_ALERT_RULE_DAILY_ROLLUP_THRESHOLD_MINUTES,
METRIC_ALERT_RULE_TIME_WINDOW_MAX_MINUTES,
METRIC_ALERT_RULE_TIME_WINDOW_MIN_MINUTES,
METRIC_ALERT_RULES_PER_TARGET_LIMIT,
MINUTES_PER_DAY,
} from '../../commerce/constants';
import { OrganizationManager } from '../../organization/providers/organization-manager';
import { Logger } from '../../shared/providers/logger';
Expand Down Expand Up @@ -338,6 +340,17 @@ export class MetricAlertRulesManager {
`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).`,
);
}
// Windows at/above the daily-rollup threshold read whole-day ClickHouse
// buckets, so they must be a whole number of days or the window is silently
// rounded. The UI presets already satisfy this; this guards direct API callers.
if (
timeWindowMinutes >= METRIC_ALERT_RULE_DAILY_ROLLUP_THRESHOLD_MINUTES &&
timeWindowMinutes % MINUTES_PER_DAY !== 0
) {
throw new MetricAlertRuleValidationError(
'Time windows of 7 days or more must be a whole number of days.',
);
}
}

private async assertChannelsBelongToProject(
Expand Down
9 changes: 8 additions & 1 deletion packages/services/api/src/modules/commerce/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,12 @@ export const METRIC_ALERT_RULES_PER_TARGET_LIMIT = 10;
* reasons; these constants are the absolute bounds the API enforces against
* any caller (form, seed scripts, customer integrations).
*/
export const MINUTES_PER_DAY = 24 * 60; // 1440
export const METRIC_ALERT_RULE_TIME_WINDOW_MIN_MINUTES = 1;
export const METRIC_ALERT_RULE_TIME_WINDOW_MAX_MINUTES = 30 * 24 * 60; // 43200
export const METRIC_ALERT_RULE_TIME_WINDOW_MAX_MINUTES = 30 * MINUTES_PER_DAY; // 43200

// Windows at or above this read the daily ClickHouse rollup, whose buckets are
// whole days. A window this size must therefore be a whole number of days
// (a multiple of MINUTES_PER_DAY) or the daily aggregate would silently round it.
// Mirrors DAILY_THRESHOLD_MINUTES in the workflows evaluator; keep the two in sync.
export const METRIC_ALERT_RULE_DAILY_ROLLUP_THRESHOLD_MINUTES = 7 * MINUTES_PER_DAY; // 10080
1 change: 1 addition & 0 deletions packages/services/workflows/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
"graphql-yoga": "5.13.3",
"mjml": "4.14.0",
"nodemailer": "9.0.1",
"p-limit": "6.2.0",
"sendmail": "1.6.1",
"zod": "3.25.76"
}
Expand Down
Loading
Loading