@intentius/chant-lexicon-prometheus 0.106.4 → 0.108.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -1
- package/dist/api/alertmanager.d.ts +56 -0
- package/dist/api/alertmanager.d.ts.map +1 -0
- package/dist/api/apply.d.ts +126 -0
- package/dist/api/apply.d.ts.map +1 -0
- package/dist/api/bind.d.ts +49 -0
- package/dist/api/bind.d.ts.map +1 -0
- package/dist/api/client.d.ts +76 -0
- package/dist/api/client.d.ts.map +1 -0
- package/dist/api/evaluated.d.ts +36 -0
- package/dist/api/evaluated.d.ts.map +1 -0
- package/dist/api/fake-servers.d.ts +74 -0
- package/dist/api/fake-servers.d.ts.map +1 -0
- package/dist/api/index.d.ts +18 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/ruler.d.ts +87 -0
- package/dist/api/ruler.d.ts.map +1 -0
- package/dist/composites/alert-routing.d.ts +104 -0
- package/dist/composites/alert-routing.d.ts.map +1 -0
- package/dist/composites/catalog.d.ts.map +1 -1
- package/dist/composites/index.d.ts +6 -0
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/red-alerts.d.ts +96 -0
- package/dist/composites/red-alerts.d.ts.map +1 -0
- package/dist/composites/watchdog.d.ts +69 -0
- package/dist/composites/watchdog.d.ts.map +1 -0
- package/dist/config.d.ts +273 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +48 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/export-resources.d.ts +41 -0
- package/dist/export-resources.d.ts.map +1 -0
- package/dist/import/alertmanager-live.d.ts +35 -0
- package/dist/import/alertmanager-live.d.ts.map +1 -0
- package/dist/import/parser.d.ts +12 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +6 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +3 -3
- package/dist/lint/post-synth/prom-helpers.d.ts +11 -3
- package/dist/lint/post-synth/prom-helpers.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.d.ts +25 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/op/activities/promtool.d.ts +78 -0
- package/dist/op/activities/promtool.d.ts.map +1 -0
- package/dist/op/activities/rule-audit.d.ts +65 -0
- package/dist/op/activities/rule-audit.d.ts.map +1 -0
- package/dist/op/activities/rules-loaded.d.ts +60 -0
- package/dist/op/activities/rules-loaded.d.ts.map +1 -0
- package/dist/op/activities/silence.d.ts +80 -0
- package/dist/op/activities/silence.d.ts.map +1 -0
- package/dist/op/activity-contracts.d.ts +113 -0
- package/dist/op/activity-contracts.d.ts.map +1 -0
- package/dist/op/audit-op.d.ts +38 -0
- package/dist/op/audit-op.d.ts.map +1 -0
- package/dist/op/builders.d.ts +57 -0
- package/dist/op/builders.d.ts.map +1 -0
- package/dist/op/slo-rule-tests.d.ts +44 -0
- package/dist/op/slo-rule-tests.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/rules/prom-helpers.ts +38 -3
- package/package.json +19 -3
- package/src/api/alertmanager.ts +122 -0
- package/src/api/apply.test.ts +218 -0
- package/src/api/apply.ts +375 -0
- package/src/api/bind.ts +87 -0
- package/src/api/client.ts +138 -0
- package/src/api/evaluated.test.ts +78 -0
- package/src/api/evaluated.ts +106 -0
- package/src/api/fake-servers.ts +209 -0
- package/src/api/index.ts +29 -0
- package/src/api/ruler.ts +168 -0
- package/src/api/testdata/am-status-original.yml +85 -0
- package/src/composites/alert-routing.test.ts +163 -0
- package/src/composites/alert-routing.ts +277 -0
- package/src/composites/catalog.test.ts +10 -2
- package/src/composites/catalog.ts +214 -0
- package/src/composites/index.ts +14 -0
- package/src/composites/red-alerts.test.ts +182 -0
- package/src/composites/red-alerts.ts +214 -0
- package/src/composites/watchdog.test.ts +85 -0
- package/src/composites/watchdog.ts +142 -0
- package/src/config.test.ts +93 -0
- package/src/config.ts +288 -0
- package/src/describe-resources.test.ts +325 -0
- package/src/describe-resources.ts +328 -0
- package/src/export-resources.test.ts +253 -0
- package/src/export-resources.ts +193 -0
- package/src/import/alertmanager-live.test.ts +66 -0
- package/src/import/alertmanager-live.ts +291 -0
- package/src/import/parser.ts +18 -1
- package/src/import/roundtrip.test.ts +1 -0
- package/src/index.ts +37 -0
- package/src/lint/post-synth/prom-helpers.ts +38 -3
- package/src/op/activities/index.ts +31 -0
- package/src/op/activities/promtool.test.ts +107 -0
- package/src/op/activities/promtool.ts +149 -0
- package/src/op/activities/rule-audit.test.ts +89 -0
- package/src/op/activities/rule-audit.ts +168 -0
- package/src/op/activities/rules-loaded.test.ts +61 -0
- package/src/op/activities/rules-loaded.ts +98 -0
- package/src/op/activities/silence.test.ts +107 -0
- package/src/op/activities/silence.ts +175 -0
- package/src/op/activity-contracts.test.ts +141 -0
- package/src/op/activity-contracts.ts +96 -0
- package/src/op/audit-op.ts +54 -0
- package/src/op/builders.ts +82 -0
- package/src/op/slo-rule-tests.test.ts +84 -0
- package/src/op/slo-rule-tests.ts +173 -0
- package/src/plugin.ts +16 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RedAlerts` over a spanmetrics connector's names: the expressions are the
|
|
3
|
+
* ones `RedDashboard` runs (both from the otel lexicon's
|
|
4
|
+
* `spanMetricsRedQueries()`), the rules pass the lexicon's checks, and over
|
|
5
|
+
* fixed series the alerts fire for the service that is failing or slow and
|
|
6
|
+
* for no other. `promtool check rules` runs when promtool is on PATH.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, test } from "vitest";
|
|
9
|
+
import { spanMetricsNames, spanMetricsRedQueries, type SpanMetricsNames } from "@intentius/chant-lexicon-otel/metric-names";
|
|
10
|
+
import { RedMetrics } from "@intentius/chant-lexicon-otel/composites/red-metrics";
|
|
11
|
+
import { RedAlerts, redAlertRules, RED_ALERT_NAMES, type RedAlertsProps } from "./red-alerts";
|
|
12
|
+
import { ruleGroupConfig } from "../rules";
|
|
13
|
+
import { emitYaml } from "../build";
|
|
14
|
+
import { RuleEvaluator, type Labels, type FiringAlert } from "../rule-eval";
|
|
15
|
+
import { validateRuleFile } from "../validate-config";
|
|
16
|
+
import { hasTool, promtoolCheckRules } from "../tools";
|
|
17
|
+
import { isAlertingRuleConfig, type AlertingRuleConfig, type RuleGroupConfig } from "../model";
|
|
18
|
+
|
|
19
|
+
const MIN = 60_000;
|
|
20
|
+
const hasPromtool = hasTool(process.env.PROMTOOL ?? "promtool");
|
|
21
|
+
const names = spanMetricsNames({ namespace: "shop", histogram: { unit: "s" } });
|
|
22
|
+
|
|
23
|
+
function groupOf(props: Partial<RedAlertsProps> = {}): RuleGroupConfig {
|
|
24
|
+
return ruleGroupConfig(RedAlerts({ spanMetrics: names, ...props }).rules);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function alerts(group: RuleGroupConfig): AlertingRuleConfig[] {
|
|
28
|
+
return group.rules.filter(isAlertingRuleConfig);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Per-minute increments. Service `a` serves 100 spans a minute, 10 of them in
|
|
33
|
+
* error, half under 0.5s, 60 under 1s and the rest under 2.5s. Service `b`
|
|
34
|
+
* serves 100, none in error, all under 0.5s, and makes 50 failing client
|
|
35
|
+
* calls a minute, which the alerts don't count.
|
|
36
|
+
*/
|
|
37
|
+
function feed(n: SpanMetricsNames): (ev: RuleEvaluator, minute: number) => void {
|
|
38
|
+
const series: Array<{ labels: Labels; perMin: number }> = [];
|
|
39
|
+
const span = (service: string, kind: string, status: string) => ({
|
|
40
|
+
service_name: service,
|
|
41
|
+
span_name: "GET /",
|
|
42
|
+
span_kind: kind,
|
|
43
|
+
status_code: status,
|
|
44
|
+
});
|
|
45
|
+
const calls = n.calls.prometheus;
|
|
46
|
+
const buckets = `${n.duration!.prometheus}_bucket`;
|
|
47
|
+
series.push({ labels: { __name__: calls, ...span("a", "SPAN_KIND_SERVER", "STATUS_CODE_UNSET") }, perMin: 90 });
|
|
48
|
+
series.push({ labels: { __name__: calls, ...span("a", "SPAN_KIND_SERVER", "STATUS_CODE_ERROR") }, perMin: 10 });
|
|
49
|
+
series.push({ labels: { __name__: calls, ...span("b", "SPAN_KIND_SERVER", "STATUS_CODE_UNSET") }, perMin: 100 });
|
|
50
|
+
series.push({ labels: { __name__: calls, ...span("b", "SPAN_KIND_CLIENT", "STATUS_CODE_ERROR") }, perMin: 50 });
|
|
51
|
+
const bucket = (service: string, le: string, perMin: number) =>
|
|
52
|
+
series.push({ labels: { __name__: buckets, ...span(service, "SPAN_KIND_SERVER", "STATUS_CODE_UNSET"), le }, perMin });
|
|
53
|
+
for (const [le, a, b] of [
|
|
54
|
+
["0.5", 50, 100],
|
|
55
|
+
["1", 60, 100],
|
|
56
|
+
["2.5", 100, 100],
|
|
57
|
+
["+Inf", 100, 100],
|
|
58
|
+
] as const) {
|
|
59
|
+
bucket("a", le, a);
|
|
60
|
+
bucket("b", le, b);
|
|
61
|
+
}
|
|
62
|
+
return (ev, minute) => {
|
|
63
|
+
for (const s of series) ev.add(s.labels, minute * MIN, s.perMin * minute);
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function run(group: RuleGroupConfig, minutes = 15): Map<number, FiringAlert[]> {
|
|
68
|
+
const ev = new RuleEvaluator([group]);
|
|
69
|
+
const push = feed(names);
|
|
70
|
+
const firing = new Map<number, FiringAlert[]>();
|
|
71
|
+
for (let minute = 0; minute <= minutes; minute++) {
|
|
72
|
+
push(ev, minute);
|
|
73
|
+
firing.set(minute, ev.step(minute * MIN));
|
|
74
|
+
}
|
|
75
|
+
return firing;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const fired = (list: FiringAlert[] | undefined) => (list ?? []).map((a) => `${a.labels.alertname}{${a.labels.service_name}}`).sort();
|
|
79
|
+
|
|
80
|
+
describe("RedAlerts defaults", () => {
|
|
81
|
+
const group = groupOf();
|
|
82
|
+
|
|
83
|
+
test("one group, red, with the error-ratio and latency alerts, warning, for 10m", () => {
|
|
84
|
+
expect(group.name).toBe("red");
|
|
85
|
+
expect(alerts(group).map((r) => [r.alert, r.for, r.labels?.severity])).toEqual([
|
|
86
|
+
[RED_ALERT_NAMES.errorRatio, "10m", "warning"],
|
|
87
|
+
[RED_ALERT_NAMES.latency, "10m", "warning"],
|
|
88
|
+
]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("the expressions are spanMetricsRedQueries' over 5m, the same builder RedDashboard reads", () => {
|
|
92
|
+
const q = spanMetricsRedQueries(names, { range: "5m", quantiles: [0.95] });
|
|
93
|
+
const [err, lat] = alerts(group);
|
|
94
|
+
expect(err.expr).toBe(`${q.errorRatio} > 0.05`);
|
|
95
|
+
expect(lat.expr).toBe(`${q.duration[0].expr} > 1`);
|
|
96
|
+
expect(err.expr).toContain('span_kind=~"SPAN_KIND_SERVER|SPAN_KIND_CONSUMER"');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("the rule file passes the lexicon's checks", () => {
|
|
100
|
+
expect(validateRuleFile({ groups: [group] })).toEqual([]);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("fires for the failing, slow service only, once `for` has held", () => {
|
|
104
|
+
const firing = run(group);
|
|
105
|
+
expect(fired(firing.get(5))).toEqual([]);
|
|
106
|
+
expect(fired(firing.get(15))).toEqual([`${RED_ALERT_NAMES.errorRatio}{a}`, `${RED_ALERT_NAMES.latency}{a}`]);
|
|
107
|
+
const err = firing.get(15)!.find((a) => a.labels.alertname === RED_ALERT_NAMES.errorRatio)!;
|
|
108
|
+
expect(err.value).toBeCloseTo(0.1, 6);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test.skipIf(!hasPromtool)("promtool check rules passes", () => {
|
|
112
|
+
const result = promtoolCheckRules(emitYaml({ groups: [group] }));
|
|
113
|
+
expect(result.ran).toBe(true);
|
|
114
|
+
expect(result.ok, result.output).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe("RedAlerts options", () => {
|
|
119
|
+
test("thresholds, quantile, severity, window and labels", () => {
|
|
120
|
+
const group = groupOf({
|
|
121
|
+
errorRatio: { threshold: 0.2, severity: "critical", annotations: { runbook_url: "https://runbooks.example.com/red" } },
|
|
122
|
+
latency: { quantile: 0.99, thresholdSeconds: 3, for: "5m" },
|
|
123
|
+
rateWindow: "10m",
|
|
124
|
+
name: "shop-red",
|
|
125
|
+
labels: { team: "shop" },
|
|
126
|
+
});
|
|
127
|
+
expect(group.name).toBe("shop-red");
|
|
128
|
+
expect(group.labels).toEqual({ team: "shop" });
|
|
129
|
+
const [err, lat] = alerts(group);
|
|
130
|
+
expect(err.expr.endsWith("> 0.2")).toBe(true);
|
|
131
|
+
expect(err.expr).toContain("[10m]");
|
|
132
|
+
expect(err.labels?.severity).toBe("critical");
|
|
133
|
+
expect(err.annotations?.runbook_url).toBe("https://runbooks.example.com/red");
|
|
134
|
+
expect(lat.expr.startsWith("histogram_quantile(0.99,")).toBe(true);
|
|
135
|
+
expect(lat.expr.endsWith("> 3")).toBe(true);
|
|
136
|
+
expect(lat.for).toBe("5m");
|
|
137
|
+
// 0.2 is above a's 0.1, and a's p99 (about 2.46s) is under 3s.
|
|
138
|
+
expect(fired(run(group).get(15))).toEqual([]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("a millisecond histogram gets the threshold in milliseconds", () => {
|
|
142
|
+
const ms = spanMetricsNames({});
|
|
143
|
+
const [, lat] = redAlertRules({ spanMetrics: ms, latency: { thresholdSeconds: 0.25 } });
|
|
144
|
+
expect(lat.expr).toContain("traces_span_metrics_duration_milliseconds_bucket");
|
|
145
|
+
expect(lat.expr.endsWith("> 250")).toBe(true);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("minRate keeps quiet services quiet", () => {
|
|
149
|
+
const group = groupOf({ minRate: 10 });
|
|
150
|
+
expect(alerts(group)[0].expr).toContain("\nand on (service_name)\n");
|
|
151
|
+
expect(validateRuleFile({ groups: [group] })).toEqual([]);
|
|
152
|
+
// a sees 100 spans a minute, under 10 a second.
|
|
153
|
+
expect(fired(run(group).get(15))).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("either alert can be left out; without a histogram the latency alert is left out unless asked for", () => {
|
|
157
|
+
expect(alerts(groupOf({ latency: false })).map((r) => r.alert)).toEqual([RED_ALERT_NAMES.errorRatio]);
|
|
158
|
+
expect(alerts(groupOf({ errorRatio: false })).map((r) => r.alert)).toEqual([RED_ALERT_NAMES.latency]);
|
|
159
|
+
const noHistogram = spanMetricsNames({ histogram: { disable: true } });
|
|
160
|
+
expect(redAlertRules({ spanMetrics: noHistogram }).map((r) => r.alert)).toEqual([RED_ALERT_NAMES.errorRatio]);
|
|
161
|
+
expect(() => redAlertRules({ spanMetrics: noHistogram, latency: true })).toThrow(/RedAlerts: latency needs the connector's duration histogram/);
|
|
162
|
+
expect(() => RedAlerts({ spanMetrics: names, errorRatio: false, latency: false })).toThrow(/nothing to alert on/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("reads the names from RedMetrics' connector and exporter", () => {
|
|
166
|
+
const red = RedMetrics({ spanMetrics: { namespace: "edge" } });
|
|
167
|
+
const [err] = redAlertRules({ spanMetrics: red.spanMetrics, exporter: red.exporter });
|
|
168
|
+
expect(err.expr).toContain("edge_calls_total");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("bad props are refused", () => {
|
|
172
|
+
expect(() => redAlertRules({} as RedAlertsProps)).toThrow(/spanMetrics is required/);
|
|
173
|
+
expect(() => groupOf({ errorRatio: { threshold: 1 } })).toThrow(/errorRatio.threshold/);
|
|
174
|
+
expect(() => groupOf({ latency: { quantile: 1 } })).toThrow(/latency.quantile/);
|
|
175
|
+
expect(() => groupOf({ latency: { thresholdSeconds: 0 } })).toThrow(/latency.thresholdSeconds/);
|
|
176
|
+
expect(() => groupOf({ rateWindow: "five" })).toThrow(/rateWindow/);
|
|
177
|
+
expect(() => groupOf({ groupBy: ["a-b"] })).toThrow(/groupBy/);
|
|
178
|
+
expect(() => groupOf({ minRate: 0 })).toThrow(/minRate/);
|
|
179
|
+
expect(() => groupOf({ name: "a b" })).toThrow(/name "a b"/);
|
|
180
|
+
expect(() => groupOf({ errorRatio: { for: "soon" } })).toThrow(/errorRatio.for/);
|
|
181
|
+
});
|
|
182
|
+
});
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `RedAlerts`: error-ratio and latency alerts per service over the span
|
|
3
|
+
* metrics a `spanmetrics` connector emits.
|
|
4
|
+
*
|
|
5
|
+
* The names come from the connector's declaration through the otel lexicon's
|
|
6
|
+
* `spanMetricsNames()`, and the expressions from its
|
|
7
|
+
* `spanMetricsRedQueries()`, the same builder the grafana lexicon's
|
|
8
|
+
* `RedDashboard` runs its panels on. The alert and the panel a responder
|
|
9
|
+
* opens read one expression, with `rateWindow` in place of Grafana's
|
|
10
|
+
* `$__rate_interval`.
|
|
11
|
+
*
|
|
12
|
+
* Both alerts count server and consumer spans only by default, like the
|
|
13
|
+
* dashboard: a service's outgoing calls and internal spans don't dilute its
|
|
14
|
+
* error ratio. Both are on by default; the latency alert needs the
|
|
15
|
+
* connector's duration histogram and is left out without one unless asked
|
|
16
|
+
* for.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Composite, type CompositeInstance } from "@intentius/chant/composite";
|
|
20
|
+
import {
|
|
21
|
+
promNumber,
|
|
22
|
+
spanMetricsNames,
|
|
23
|
+
spanMetricsRedQueries,
|
|
24
|
+
type PrometheusNaming,
|
|
25
|
+
type SpanMetricsKind,
|
|
26
|
+
type SpanMetricsNames,
|
|
27
|
+
} from "@intentius/chant-lexicon-otel/metric-names";
|
|
28
|
+
import type { OTelComponent } from "@intentius/chant-lexicon-otel/define";
|
|
29
|
+
import type { SpanMetricsConnectorConfig } from "@intentius/chant-lexicon-otel/components/connectors";
|
|
30
|
+
import type { PrometheusExporterConfig } from "@intentius/chant-lexicon-otel/components/exporters";
|
|
31
|
+
import { RuleGroup, type AlertingRule, type RuleGroupEntity } from "../rules";
|
|
32
|
+
import type { LabelSet } from "../model";
|
|
33
|
+
import { durationMs, isValidDuration } from "../duration";
|
|
34
|
+
|
|
35
|
+
/** The fields both alerts take. */
|
|
36
|
+
export interface RedAlertOptions {
|
|
37
|
+
/** How long the condition must hold (default `10m`). */
|
|
38
|
+
for?: string;
|
|
39
|
+
/** The `severity` label, which Alertmanager routes on (default `warning`). */
|
|
40
|
+
severity?: string;
|
|
41
|
+
/** More labels on the alert. */
|
|
42
|
+
labels?: LabelSet;
|
|
43
|
+
/** More annotations on the alert, e.g. `runbook_url`. */
|
|
44
|
+
annotations?: LabelSet;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface RedErrorRatioAlert extends RedAlertOptions {
|
|
48
|
+
/** The share of counted spans ending in error the alert fires above, between 0 and 1 (default 0.05). */
|
|
49
|
+
threshold?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface RedLatencyAlert extends RedAlertOptions {
|
|
53
|
+
/** The duration quantile compared (default 0.95). */
|
|
54
|
+
quantile?: number;
|
|
55
|
+
/** The duration, in seconds, the quantile fires above (default 1). Converted to the histogram's unit. */
|
|
56
|
+
thresholdSeconds?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RedAlertsProps {
|
|
60
|
+
/** The `spanmetrics` connector the metrics come from (the otel lexicon's `RedMetrics` has it as `spanMetrics`), or the names `spanMetricsNames()` returned for it. */
|
|
61
|
+
spanMetrics: OTelComponent<"connector", "spanmetrics", SpanMetricsConnectorConfig> | SpanMetricsNames;
|
|
62
|
+
/** The `prometheus` exporter that serves them, when its `namespace` or `add_metric_suffixes` changes the names. */
|
|
63
|
+
exporter?: OTelComponent<"exporter", "prometheus", PrometheusExporterConfig> | PrometheusNaming;
|
|
64
|
+
/** Error ratio per service (default: on, above 0.05). `false` leaves it out. */
|
|
65
|
+
errorRatio?: boolean | RedErrorRatioAlert;
|
|
66
|
+
/** A duration quantile per service (default: on, p95 above 1s, when the connector has a histogram). `false` leaves it out. */
|
|
67
|
+
latency?: boolean | RedLatencyAlert;
|
|
68
|
+
/** The span kinds counted (default server and consumer spans); `[]` counts every kind. */
|
|
69
|
+
spanKinds?: SpanMetricsKind[];
|
|
70
|
+
/** The range every `rate` reads (default `5m`). */
|
|
71
|
+
rateWindow?: string;
|
|
72
|
+
/** Spans per second a service must see for its alerts to fire, so a handful of calls can't page (default: no floor). */
|
|
73
|
+
minRate?: number;
|
|
74
|
+
/** More Prometheus labels the alerts are split by besides the service, e.g. `deployment_environment`. They must be connector dimensions. */
|
|
75
|
+
groupBy?: string[];
|
|
76
|
+
/** The rule group's name (default `red`). */
|
|
77
|
+
name?: string;
|
|
78
|
+
/** Labels added to every rule, e.g. `team`. */
|
|
79
|
+
labels?: LabelSet;
|
|
80
|
+
/** Evaluation interval of the group (default: Prometheus's `evaluation_interval`). */
|
|
81
|
+
interval?: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export type RedAlertsMembers = {
|
|
85
|
+
/** The alerting rules. */
|
|
86
|
+
rules: RuleGroupEntity;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
/** What `RedAlerts(...)` returns: its rule group, as `rules`. */
|
|
90
|
+
export type RedAlertsInstance = CompositeInstance<RedAlertsMembers> & RedAlertsMembers;
|
|
91
|
+
|
|
92
|
+
const LABEL = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
93
|
+
const GROUP_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
94
|
+
|
|
95
|
+
/** The alert names `RedAlerts` writes. */
|
|
96
|
+
export const RED_ALERT_NAMES = Object.freeze({ errorRatio: "ServiceErrorRatioHigh", latency: "ServiceLatencyHigh" });
|
|
97
|
+
|
|
98
|
+
const DEFAULTS = { threshold: 0.05, quantile: 0.95, thresholdSeconds: 1, for: "10m", severity: "warning" };
|
|
99
|
+
|
|
100
|
+
function fail(message: string): never {
|
|
101
|
+
throw new Error(`RedAlerts: ${message}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function isNames(x: unknown): x is SpanMetricsNames {
|
|
105
|
+
return typeof x === "object" && x !== null && "calls" in x && "labels" in x && "errorStatus" in x;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function options<T extends RedAlertOptions>(value: boolean | T | undefined): T | undefined {
|
|
109
|
+
if (value === false) return undefined;
|
|
110
|
+
return value === undefined || value === true ? ({} as T) : value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function checkOptions(at: string, a: RedAlertOptions): void {
|
|
114
|
+
if (a.for !== undefined && !isValidDuration(a.for)) fail(`${at}.for "${a.for}" is not a Prometheus duration`);
|
|
115
|
+
if (a.severity !== undefined && (typeof a.severity !== "string" || a.severity === "")) fail(`${at}.severity must be a non-empty string`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** The alerting rules `RedAlerts` builds, without the group: for a group of your own. */
|
|
119
|
+
export function redAlertRules(props: RedAlertsProps): AlertingRule[] {
|
|
120
|
+
if (!props || typeof props !== "object") fail("props are required");
|
|
121
|
+
if (!props.spanMetrics) fail("spanMetrics is required (a spanmetrics connector)");
|
|
122
|
+
const names = isNames(props.spanMetrics) ? props.spanMetrics : spanMetricsNames(props.spanMetrics, props.exporter);
|
|
123
|
+
const window = props.rateWindow ?? "5m";
|
|
124
|
+
if (!isValidDuration(window) || !durationMs(window)) fail(`rateWindow "${window}" is not a positive Prometheus duration`);
|
|
125
|
+
const groupBy = props.groupBy ?? [];
|
|
126
|
+
for (const l of groupBy) if (!LABEL.test(l)) fail(`groupBy label "${l}" is not a Prometheus label name`);
|
|
127
|
+
if (props.minRate !== undefined && !(typeof props.minRate === "number" && Number.isFinite(props.minRate) && props.minRate > 0)) {
|
|
128
|
+
fail(`minRate must be above 0, got ${String(props.minRate)}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const err = options(props.errorRatio);
|
|
132
|
+
// The latency alert is on by default only when there is a histogram to read.
|
|
133
|
+
const lat = props.latency === undefined && !names.duration ? undefined : options(props.latency);
|
|
134
|
+
if (lat && !names.duration) fail("latency needs the connector's duration histogram, and it is disabled");
|
|
135
|
+
if (err) {
|
|
136
|
+
checkOptions("errorRatio", err);
|
|
137
|
+
const t = err.threshold;
|
|
138
|
+
if (t !== undefined && !(typeof t === "number" && t > 0 && t < 1)) fail(`errorRatio.threshold must be above 0 and below 1, got ${String(t)}`);
|
|
139
|
+
}
|
|
140
|
+
if (lat) {
|
|
141
|
+
checkOptions("latency", lat);
|
|
142
|
+
const q = lat.quantile;
|
|
143
|
+
if (q !== undefined && !(typeof q === "number" && q > 0 && q < 1)) fail(`latency.quantile must be between 0 and 1, got ${String(q)}`);
|
|
144
|
+
const s = lat.thresholdSeconds;
|
|
145
|
+
if (s !== undefined && !(typeof s === "number" && Number.isFinite(s) && s > 0)) fail(`latency.thresholdSeconds must be above 0, got ${String(s)}`);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const quantile = lat?.quantile ?? DEFAULTS.quantile;
|
|
149
|
+
const q = spanMetricsRedQueries(names, { range: window, quantiles: [quantile], spanKinds: props.spanKinds, by: groupBy, owner: "RedAlerts" });
|
|
150
|
+
const by = [q.service, ...groupBy];
|
|
151
|
+
const floor = props.minRate !== undefined ? `\nand on (${by.join(", ")})\n${q.rate} > ${promNumber(props.minRate)}` : "";
|
|
152
|
+
const svc = `{{ $labels.${q.service} }}`;
|
|
153
|
+
const out: AlertingRule[] = [];
|
|
154
|
+
const build = (o: RedAlertOptions, alert: string, expr: string, summary: string, description: string) => {
|
|
155
|
+
out.push({
|
|
156
|
+
alert,
|
|
157
|
+
expr,
|
|
158
|
+
for: o.for ?? DEFAULTS.for,
|
|
159
|
+
labels: { ...(o.labels ?? {}), severity: o.severity ?? DEFAULTS.severity },
|
|
160
|
+
annotations: { summary, description, ...(o.annotations ?? {}) },
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
if (err) {
|
|
165
|
+
const threshold = err.threshold ?? DEFAULTS.threshold;
|
|
166
|
+
build(
|
|
167
|
+
err,
|
|
168
|
+
RED_ALERT_NAMES.errorRatio,
|
|
169
|
+
`${q.errorRatio} > ${promNumber(threshold)}${floor}`,
|
|
170
|
+
`Error ratio above ${promNumber(threshold * 100)}% for ${svc}`,
|
|
171
|
+
`{{ $value | humanizePercentage }} of ${svc} spans ended in error over the last ${window}.`,
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (lat && names.duration) {
|
|
175
|
+
const seconds = lat.thresholdSeconds ?? DEFAULTS.thresholdSeconds;
|
|
176
|
+
const inUnit = names.duration.unit === "s" ? seconds : seconds * 1000;
|
|
177
|
+
const qName = `p${promNumber(quantile * 100)}`;
|
|
178
|
+
const shown = names.duration.unit === "s" ? "{{ $value | humanizeDuration }}" : "{{ $value | printf \"%.0f\" }}ms";
|
|
179
|
+
build(
|
|
180
|
+
lat,
|
|
181
|
+
RED_ALERT_NAMES.latency,
|
|
182
|
+
`${q.duration[0].expr} > ${promNumber(inUnit)}${floor}`,
|
|
183
|
+
`${qName} duration above ${promNumber(seconds)}s for ${svc}`,
|
|
184
|
+
`${qName} duration of ${svc} is ${shown} over the last ${window}.`,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
return out;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Error-ratio and latency alerts per service, from a `spanmetrics` connector.
|
|
192
|
+
*
|
|
193
|
+
* @example
|
|
194
|
+
* ```ts
|
|
195
|
+
* import { RedMetrics } from "@intentius/chant-lexicon-otel";
|
|
196
|
+
* import { RedAlerts } from "@intentius/chant-lexicon-prometheus";
|
|
197
|
+
*
|
|
198
|
+
* export const red = RedMetrics({ spanMetrics: { namespace: "shop", histogram: { unit: "s" } } });
|
|
199
|
+
* export const redAlerts = RedAlerts({ spanMetrics: red.spanMetrics, exporter: red.exporter, latency: { thresholdSeconds: 0.5 } });
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export const RedAlerts = Composite<RedAlertsProps, RedAlertsMembers>((props) => {
|
|
203
|
+
const rules = redAlertRules(props);
|
|
204
|
+
const name = props.name ?? "red";
|
|
205
|
+
if (!GROUP_NAME.test(name)) fail(`name "${name}" must be letters, digits, '.', '_' or '-'`);
|
|
206
|
+
if (rules.length === 0) fail("errorRatio and latency are both off, so there is nothing to alert on");
|
|
207
|
+
const group = new RuleGroup({
|
|
208
|
+
name,
|
|
209
|
+
...(props.interval !== undefined ? { interval: props.interval } : {}),
|
|
210
|
+
...(props.labels !== undefined ? { labels: props.labels } : {}),
|
|
211
|
+
rules,
|
|
212
|
+
});
|
|
213
|
+
return { rules: group };
|
|
214
|
+
}, "RedAlerts");
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Watchdog`: the always-firing alert, its heartbeat route and receiver, and
|
|
3
|
+
* that the rule file and `alertmanager.yml` built with it pass the lexicon's
|
|
4
|
+
* checks once the route is nested under a root.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, test } from "vitest";
|
|
7
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
8
|
+
import { Watchdog, WATCHDOG_URL_FILE } from "./watchdog";
|
|
9
|
+
import { AlertRouting } from "./alert-routing";
|
|
10
|
+
import { Receiver } from "../alertmanager";
|
|
11
|
+
import { buildAlertmanagerConfig, buildRuleFile } from "../build";
|
|
12
|
+
import { validateAlertmanagerConfig, validateRuleFile, validateSeverityRouting } from "../validate-config";
|
|
13
|
+
import { RuleEvaluator } from "../rule-eval";
|
|
14
|
+
import { ruleGroupConfig } from "../rules";
|
|
15
|
+
|
|
16
|
+
const values = (...instances: Array<{ members: Record<string, unknown> }>) =>
|
|
17
|
+
instances.flatMap((i) => Object.values(i.members)) as Declarable[];
|
|
18
|
+
|
|
19
|
+
describe("Watchdog defaults", () => {
|
|
20
|
+
const watchdog = Watchdog({});
|
|
21
|
+
|
|
22
|
+
test("one alert, Watchdog, on vector(1) with severity none, in group watchdog", () => {
|
|
23
|
+
const group = ruleGroupConfig(watchdog.rules);
|
|
24
|
+
expect(group.name).toBe("watchdog");
|
|
25
|
+
expect(group.rules).toHaveLength(1);
|
|
26
|
+
expect(group.rules[0]).toMatchObject({ alert: "Watchdog", expr: "vector(1)", labels: { severity: "none" } });
|
|
27
|
+
expect(validateRuleFile(buildRuleFile([watchdog.rules]).config)).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("it fires from the first evaluation", () => {
|
|
31
|
+
const ev = new RuleEvaluator([ruleGroupConfig(watchdog.rules)]);
|
|
32
|
+
expect(ev.step(0).map((a) => a.labels.alertname)).toEqual(["Watchdog"]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test("the route matches the alert and sends it to a heartbeat webhook reading its URL from a file", () => {
|
|
36
|
+
expect(watchdog.route.props).toMatchObject({
|
|
37
|
+
matchers: ['alertname="Watchdog"', 'severity="none"'],
|
|
38
|
+
group_wait: "0s",
|
|
39
|
+
group_interval: "1m",
|
|
40
|
+
repeat_interval: "1m",
|
|
41
|
+
});
|
|
42
|
+
expect(watchdog.receiver?.props).toEqual({ name: "heartbeat", webhook_configs: [{ url_file: WATCHDOG_URL_FILE, send_resolved: false }] });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("nested under AlertRouting, alertmanager.yml passes the checks and the watchdog route comes first", () => {
|
|
46
|
+
const routing = AlertRouting({ routes: [watchdog.route] });
|
|
47
|
+
const built = buildAlertmanagerConfig(values(watchdog, routing));
|
|
48
|
+
expect(built.warnings).toEqual([]);
|
|
49
|
+
expect(built.config.route?.routes?.[0]).toMatchObject({ receiver: "heartbeat", matchers: ['alertname="Watchdog"', 'severity="none"'] });
|
|
50
|
+
expect(validateAlertmanagerConfig(built.config)).toEqual([]);
|
|
51
|
+
expect(validateSeverityRouting([buildRuleFile([watchdog.rules]).config], built.config)).toEqual([]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("left unnested, it is a second root and the build says so", () => {
|
|
55
|
+
const routing = AlertRouting({});
|
|
56
|
+
expect(buildAlertmanagerConfig(values(watchdog, routing)).warnings.join("\n")).toMatch(/2 root Routes/);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
describe("Watchdog options", () => {
|
|
61
|
+
test("a receiver of your own, by entity, props or name; and the alert's name, severity and repeat", () => {
|
|
62
|
+
const pd = new Receiver({ name: "pd-heartbeat", webhook_configs: [{ url_file: "/secrets/pd" }] });
|
|
63
|
+
const byEntity = Watchdog({ receiver: pd, alert: "DeadMansSwitch", severity: "heartbeat", repeatInterval: "5m" });
|
|
64
|
+
expect(byEntity.receiver).toBeUndefined();
|
|
65
|
+
expect(byEntity.route.props).toMatchObject({ receiver: pd, matchers: ['alertname="DeadMansSwitch"', 'severity="heartbeat"'], repeat_interval: "5m" });
|
|
66
|
+
expect(Watchdog({ receiver: "elsewhere" }).route.props.receiver).toBe("elsewhere");
|
|
67
|
+
expect(Watchdog({ receiver: { name: "hc", webhook_configs: [{ url_file: "/secrets/hc" }] } }).receiver?.props.name).toBe("hc");
|
|
68
|
+
expect(Watchdog({ urlFile: "/secrets/x" }).receiver?.props.webhook_configs?.[0].url_file).toBe("/secrets/x");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("a repeat under a minute brings the group interval down with it (PROM223)", () => {
|
|
72
|
+
const fast = Watchdog({ repeatInterval: "30s" });
|
|
73
|
+
expect(fast.route.props).toMatchObject({ group_interval: "30s", repeat_interval: "30s" });
|
|
74
|
+
const built = buildAlertmanagerConfig(values(fast, AlertRouting({ routes: [fast.route] })));
|
|
75
|
+
expect(validateAlertmanagerConfig(built.config)).toEqual([]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("bad props are refused", () => {
|
|
79
|
+
expect(() => Watchdog({ alert: "dead man" })).toThrow(/Watchdog: alert "dead man"/);
|
|
80
|
+
expect(() => Watchdog({ repeatInterval: "often" })).toThrow(/repeatInterval/);
|
|
81
|
+
expect(() => Watchdog({ urlFile: "/x", receiver: "y" })).toThrow(/urlFile configures the default receiver/);
|
|
82
|
+
expect(() => Watchdog({ severity: "" })).toThrow(/severity/);
|
|
83
|
+
expect(() => Watchdog({ group: "a b" })).toThrow(/group "a b"/);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Watchdog`: an alert that always fires, routed to a heartbeat receiver.
|
|
3
|
+
*
|
|
4
|
+
* The alert's expression is `vector(1)`, so it fires from the first rule
|
|
5
|
+
* evaluation for as long as Prometheus evaluates rules and Alertmanager
|
|
6
|
+
* delivers notifications. The route sends it to a heartbeat service
|
|
7
|
+
* (healthchecks.io, Dead Man's Snitch, a PagerDuty or Opsgenie heartbeat),
|
|
8
|
+
* which raises its own alarm when the notifications stop: the whole alerting
|
|
9
|
+
* path, not just Prometheus, is down.
|
|
10
|
+
*
|
|
11
|
+
* The route is a child route, matched on the alert's name and severity. Put
|
|
12
|
+
* it first under the root route: `AlertRouting({ routes: [watchdog.route] })`,
|
|
13
|
+
* or the `routes` of a root `Route` of your own. Left unnested it is a second
|
|
14
|
+
* root, and the build warns.
|
|
15
|
+
*
|
|
16
|
+
* By default the receiver is a webhook that reads its URL from a file
|
|
17
|
+
* (`url_file`), since a heartbeat URL is a credential; mount the file into
|
|
18
|
+
* Alertmanager.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { Composite, type CompositeInstance } from "@intentius/chant/composite";
|
|
22
|
+
import { Receiver, Route, isReceiver, type ReceiverEntity, type ReceiverProps, type RouteEntity } from "../alertmanager";
|
|
23
|
+
import { RuleGroup, type RuleGroupEntity } from "../rules";
|
|
24
|
+
import type { LabelSet } from "../model";
|
|
25
|
+
import { durationMs, isValidDuration } from "../duration";
|
|
26
|
+
|
|
27
|
+
export interface WatchdogProps {
|
|
28
|
+
/**
|
|
29
|
+
* Where the heartbeat goes: a declared `Receiver`, a receiver's props
|
|
30
|
+
* (the composite declares it), or the name of one declared elsewhere.
|
|
31
|
+
* Default: a `heartbeat` receiver with one webhook reading `urlFile`.
|
|
32
|
+
*/
|
|
33
|
+
receiver?: ReceiverEntity | ReceiverProps | string;
|
|
34
|
+
/** The file the default receiver's webhook reads its URL from (default `/etc/alertmanager/secrets/heartbeat-url`). */
|
|
35
|
+
urlFile?: string;
|
|
36
|
+
/** The alert's name (default `Watchdog`). */
|
|
37
|
+
alert?: string;
|
|
38
|
+
/** The alert's `severity` label (default `none`): no severity route takes it. */
|
|
39
|
+
severity?: string;
|
|
40
|
+
/** How often the heartbeat is sent again while the alert fires (default `1m`). Keep it under the heartbeat service's period. */
|
|
41
|
+
repeatInterval?: string;
|
|
42
|
+
/** The rule group's name (default `watchdog`). */
|
|
43
|
+
group?: string;
|
|
44
|
+
/** More labels on the alert. */
|
|
45
|
+
labels?: LabelSet;
|
|
46
|
+
/** More annotations on the alert. */
|
|
47
|
+
annotations?: LabelSet;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export type WatchdogMembers = {
|
|
51
|
+
/** The always-firing alert. */
|
|
52
|
+
rules: RuleGroupEntity;
|
|
53
|
+
/** The child route that sends it to the heartbeat receiver. */
|
|
54
|
+
route: RouteEntity;
|
|
55
|
+
/** The heartbeat receiver, when this composite declares it. */
|
|
56
|
+
receiver?: ReceiverEntity;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type WatchdogInstance = CompositeInstance<WatchdogMembers> & WatchdogMembers;
|
|
60
|
+
|
|
61
|
+
const ALERT_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
62
|
+
const GROUP_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
63
|
+
|
|
64
|
+
/** The default heartbeat URL file. */
|
|
65
|
+
export const WATCHDOG_URL_FILE = "/etc/alertmanager/secrets/heartbeat-url";
|
|
66
|
+
|
|
67
|
+
function fail(message: string): never {
|
|
68
|
+
throw new Error(`Watchdog: ${message}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* An always-firing alert and the route that sends it to a heartbeat receiver.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* import { AlertRouting, Watchdog } from "@intentius/chant-lexicon-prometheus";
|
|
77
|
+
*
|
|
78
|
+
* export const watchdog = Watchdog({ urlFile: "/etc/alertmanager/secrets/healthchecks-url" });
|
|
79
|
+
* export const routing = AlertRouting({ routes: [watchdog.route] });
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
export const Watchdog = Composite<WatchdogProps, WatchdogMembers>((input) => {
|
|
83
|
+
const props = input ?? {};
|
|
84
|
+
const alert = props.alert ?? "Watchdog";
|
|
85
|
+
if (!ALERT_NAME.test(alert)) fail(`alert "${alert}" is not a valid alert name`);
|
|
86
|
+
const severity = props.severity ?? "none";
|
|
87
|
+
if (typeof severity !== "string" || severity === "") fail("severity must be a non-empty string");
|
|
88
|
+
const repeat = props.repeatInterval ?? "1m";
|
|
89
|
+
if (!isValidDuration(repeat) || !durationMs(repeat)) fail(`repeatInterval "${repeat}" is not a positive duration`);
|
|
90
|
+
// repeat_interval under group_interval is PROM223; a faster heartbeat brings the group interval down with it.
|
|
91
|
+
const groupInterval = (durationMs(repeat) ?? 0) < 60_000 ? repeat : "1m";
|
|
92
|
+
const groupName = props.group ?? "watchdog";
|
|
93
|
+
if (!GROUP_NAME.test(groupName)) fail(`group "${groupName}" must be letters, digits, '.', '_' or '-'`);
|
|
94
|
+
if (props.urlFile !== undefined && props.receiver !== undefined) fail("urlFile configures the default receiver; leave it out when receiver is set");
|
|
95
|
+
if (props.urlFile !== undefined && props.urlFile.trim() === "") fail("urlFile must be a path");
|
|
96
|
+
|
|
97
|
+
let receiver: ReceiverEntity | undefined;
|
|
98
|
+
let ref: ReceiverEntity | string;
|
|
99
|
+
if (typeof props.receiver === "string") {
|
|
100
|
+
if (props.receiver === "") fail("receiver must name a receiver");
|
|
101
|
+
ref = props.receiver;
|
|
102
|
+
} else if (isReceiver(props.receiver)) {
|
|
103
|
+
ref = props.receiver;
|
|
104
|
+
} else {
|
|
105
|
+
const p: ReceiverProps = props.receiver ?? {
|
|
106
|
+
name: "heartbeat",
|
|
107
|
+
webhook_configs: [{ url_file: props.urlFile ?? WATCHDOG_URL_FILE, send_resolved: false }],
|
|
108
|
+
};
|
|
109
|
+
if (!p.name) fail("receiver.name is required");
|
|
110
|
+
receiver = new Receiver(p);
|
|
111
|
+
ref = receiver;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const rules = new RuleGroup({
|
|
115
|
+
name: groupName,
|
|
116
|
+
rules: [
|
|
117
|
+
{
|
|
118
|
+
alert,
|
|
119
|
+
expr: "vector(1)",
|
|
120
|
+
labels: { ...(props.labels ?? {}), severity },
|
|
121
|
+
annotations: {
|
|
122
|
+
summary: "Always firing, to show the alerting pipeline works",
|
|
123
|
+
description:
|
|
124
|
+
"This alert fires all the time. A heartbeat service receives it every few minutes and raises its own alarm when it stops arriving, which means Prometheus, Alertmanager or the path between them is down.",
|
|
125
|
+
...(props.annotations ?? {}),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
],
|
|
129
|
+
});
|
|
130
|
+
const route = new Route({
|
|
131
|
+
receiver: ref,
|
|
132
|
+
matchers: [`alertname=${JSON.stringify(alert)}`, `severity=${JSON.stringify(severity)}`],
|
|
133
|
+
group_by: ["alertname"],
|
|
134
|
+
group_wait: "0s",
|
|
135
|
+
group_interval: groupInterval,
|
|
136
|
+
repeat_interval: repeat,
|
|
137
|
+
continue: false,
|
|
138
|
+
});
|
|
139
|
+
const members: WatchdogMembers = { rules, route };
|
|
140
|
+
if (receiver) members.receiver = receiver;
|
|
141
|
+
return members;
|
|
142
|
+
}, "Watchdog");
|