@intentius/chant-lexicon-prometheus 0.124.31 → 0.124.33
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/dist/alertmanager.js +61 -0
- package/dist/api/alertmanager.js +98 -0
- package/dist/api/apply.js +306 -0
- package/dist/api/bind.js +66 -0
- package/dist/api/client.js +110 -0
- package/dist/api/evaluated.js +86 -0
- package/dist/api/fake-servers.js +165 -0
- package/dist/api/index.js +17 -0
- package/dist/api/ruler.js +122 -0
- package/dist/build.js +240 -0
- package/dist/catalog.js +66 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +105 -0
- package/dist/codegen/generate-cli.js +7 -0
- package/dist/codegen/generate.js +42 -0
- package/dist/codegen/package.js +28 -0
- package/dist/collector-metrics.js +225 -0
- package/dist/composites/alert-routing.js +210 -0
- package/dist/composites/catalog.js +344 -0
- package/dist/composites/genai.js +454 -0
- package/dist/composites/index.js +8 -0
- package/dist/composites/red-alerts.js +135 -0
- package/dist/composites/slo.js +353 -0
- package/dist/composites/watchdog.js +110 -0
- package/dist/config-model.js +52 -0
- package/dist/config.js +225 -0
- package/dist/describe-resources.js +299 -0
- package/dist/detect.js +12 -0
- package/dist/duration.js +54 -0
- package/dist/export-resources.js +163 -0
- package/dist/import/alertmanager-live.js +309 -0
- package/dist/import/embedded.js +100 -0
- package/dist/import/generator.js +683 -0
- package/dist/import/parser.js +428 -0
- package/dist/import/slo.js +207 -0
- package/dist/index.js +33 -0
- package/dist/init-templates.js +119 -0
- package/dist/integrations.js +33 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +60 -0
- package/dist/lint/audit-lineage.js +42 -0
- package/dist/lint/post-synth/index.js +68 -0
- package/dist/lint/post-synth/prom-helpers.js +157 -0
- package/dist/lint/post-synth/prom101.js +13 -0
- package/dist/lint/post-synth/prom102.js +13 -0
- package/dist/lint/post-synth/prom103.js +13 -0
- package/dist/lint/post-synth/prom104.js +13 -0
- package/dist/lint/post-synth/prom105.js +13 -0
- package/dist/lint/post-synth/prom106.js +13 -0
- package/dist/lint/post-synth/prom107.js +13 -0
- package/dist/lint/post-synth/prom201.js +13 -0
- package/dist/lint/post-synth/prom202.js +13 -0
- package/dist/lint/post-synth/prom203.js +13 -0
- package/dist/lint/post-synth/prom204.js +13 -0
- package/dist/lint/post-synth/prom205.js +13 -0
- package/dist/lint/post-synth/prom206.js +13 -0
- package/dist/lint/post-synth/prom207.js +13 -0
- package/dist/lint/post-synth/prom208.js +13 -0
- package/dist/lint/post-synth/prom209.js +13 -0
- package/dist/lint/post-synth/prom210.js +13 -0
- package/dist/lint/post-synth/prom211.js +17 -0
- package/dist/lint/post-synth/prom212.js +14 -0
- package/dist/lint/post-synth/prom213.js +16 -0
- package/dist/lint/post-synth/prom214.js +14 -0
- package/dist/lint/post-synth/prom215.js +14 -0
- package/dist/lint/post-synth/prom216.js +15 -0
- package/dist/lint/post-synth/prom217.js +14 -0
- package/dist/lint/post-synth/prom218.js +15 -0
- package/dist/lint/post-synth/prom219.js +14 -0
- package/dist/lint/post-synth/prom220.js +14 -0
- package/dist/lint/post-synth/prom221.js +15 -0
- package/dist/lint/post-synth/prom222.js +14 -0
- package/dist/lint/post-synth/prom223.js +15 -0
- package/dist/lint/post-synth/prom224.js +14 -0
- package/dist/lint/post-synth/prom225.js +15 -0
- package/dist/lint/post-synth/prom301.js +19 -0
- package/dist/lint/rules/index.js +8 -0
- package/dist/lint/rules/literal-credential.js +127 -0
- package/dist/lint/rules/prom-ast.js +65 -0
- package/dist/lint/rules/promql-literal.js +65 -0
- package/dist/lint/rules/slo-literal.js +79 -0
- package/dist/lsp/completions.js +11 -0
- package/dist/lsp/hover.js +21 -0
- package/dist/manifest.json +1 -1
- package/dist/matchers.js +131 -0
- package/dist/model.js +108 -0
- package/dist/op/activities/index.js +20 -0
- package/dist/op/activities/promtool.js +87 -0
- package/dist/op/activities/rule-audit.js +128 -0
- package/dist/op/activities/rules-loaded.js +73 -0
- package/dist/op/activities/silence.js +123 -0
- package/dist/op/activity-contracts.js +51 -0
- package/dist/op/audit-op.js +28 -0
- package/dist/op/builders.js +57 -0
- package/dist/op/slo-rule-tests.js +148 -0
- package/dist/package-cli.js +14 -0
- package/dist/pin.js +13 -0
- package/dist/plugin.js +143 -0
- package/dist/prometheus-config.js +38 -0
- package/dist/promql-analysis.js +343 -0
- package/dist/promql.js +44 -0
- package/dist/rule-eval.js +466 -0
- package/dist/rules.js +97 -0
- package/dist/serializer.js +59 -0
- package/dist/skill-defs.js +41 -0
- package/dist/tools.js +85 -0
- package/dist/validate-cli.js +7 -0
- package/dist/validate-config.js +556 -0
- package/dist/validate-integrations.js +388 -0
- package/dist/validate-relabel.js +52 -0
- package/dist/validate-security.js +135 -0
- package/dist/validate.js +69 -0
- package/package.json +10 -10
- package/src/codegen/package.ts +2 -2
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GenAiRules`: recording rules and opt-in alerts for the otel lexicon's
|
|
3
|
+
* GenAI collector preset, and spend from a price table the project declares.
|
|
4
|
+
*
|
|
5
|
+
* Every metric and label name comes from the `GenAiMetrics` the collector was
|
|
6
|
+
* built with (`genAiMetrics(options)` or `genAiComponents(options).metrics`)
|
|
7
|
+
* and from the otel lexicon's attribute keys, so a collector with another
|
|
8
|
+
* namespace, or with the conventions' client metrics switched on, moves every
|
|
9
|
+
* expression here with it.
|
|
10
|
+
*
|
|
11
|
+
* Two sources describe model calls:
|
|
12
|
+
*
|
|
13
|
+
* - `client`: the conventions' `gen_ai.client.operation.duration` and
|
|
14
|
+
* `gen_ai.client.token.usage` (#3041). Requests are the duration
|
|
15
|
+
* histogram's `_count`, errors the ones with an `error.type`, tokens the
|
|
16
|
+
* token histogram's `_sum` split by `gen_ai.token.type`.
|
|
17
|
+
* - `spans`: the preset's own `genai.calls`, `genai.duration` and token sums.
|
|
18
|
+
* Errors are spans with `status.code` `STATUS_CODE_ERROR`. The token sums
|
|
19
|
+
* carry the model only, so cost series take the provider from the price
|
|
20
|
+
* table.
|
|
21
|
+
*
|
|
22
|
+
* The default is `client` when the metrics have it. Per-tool rules always
|
|
23
|
+
* read the span metrics: the conventions' client metrics carry no tool name.
|
|
24
|
+
*
|
|
25
|
+
* Everything is in one group, recording rules first, because Prometheus
|
|
26
|
+
* evaluates a group's rules in order and the later rules and the alerts read
|
|
27
|
+
* the series the earlier ones record in the same evaluation.
|
|
28
|
+
*
|
|
29
|
+
* Cost is a rate in the price's currency per second, one series per priced
|
|
30
|
+
* model and token type, labelled with the currency. A model the table does
|
|
31
|
+
* not price gets no cost series, never a cost of zero. The lexicon ships no
|
|
32
|
+
* prices. `currency` and `source` are required on every price, the same two
|
|
33
|
+
* fields a workspace run's cost record carries (#3033), so the two can be
|
|
34
|
+
* compared; chant converts no currency.
|
|
35
|
+
*/
|
|
36
|
+
import { Composite } from "@intentius/chant/composite";
|
|
37
|
+
import { GENAI_ATTRIBUTES, GENAI_TOKEN_TYPES } from "@intentius/chant-lexicon-otel/genai";
|
|
38
|
+
import { prometheusLabel, SPAN_STATUS_ERROR } from "@intentius/chant-lexicon-otel/metric-names";
|
|
39
|
+
import { RuleGroup } from "../rules.js";
|
|
40
|
+
import { durationMs, isValidDuration } from "../duration.js";
|
|
41
|
+
const GENAI_RULES_METRICS = Symbol.for("chant.prometheus.genai");
|
|
42
|
+
const PREFIX = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
43
|
+
const LABEL = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
44
|
+
const GROUP_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
45
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
46
|
+
const QUANTILES = ["0.5", "0.95", "0.99"];
|
|
47
|
+
const ALERT_QUANTILE = "0.95";
|
|
48
|
+
const CURRENCY_LABEL = "currency";
|
|
49
|
+
const QUANTILE_LABEL = "quantile";
|
|
50
|
+
const BUDGET_SECONDS = { hour: 3600, day: 86400 };
|
|
51
|
+
const BUDGET_RANGE = { hour: "1h", day: "1d" };
|
|
52
|
+
const DEFAULTS = {
|
|
53
|
+
errorRatio: 0.05,
|
|
54
|
+
latencySeconds: 30,
|
|
55
|
+
toolErrorRatio: 0.1,
|
|
56
|
+
for: "10m",
|
|
57
|
+
severity: "warning",
|
|
58
|
+
};
|
|
59
|
+
function fail(message) {
|
|
60
|
+
throw new Error(`GenAiRules: ${message}`);
|
|
61
|
+
}
|
|
62
|
+
/** A number as PromQL writes it, without float noise. */
|
|
63
|
+
function num(n) {
|
|
64
|
+
return String(Number(n.toPrecision(12)));
|
|
65
|
+
}
|
|
66
|
+
/** A label value inside a PromQL string literal. */
|
|
67
|
+
function quote(value) {
|
|
68
|
+
return JSON.stringify(value);
|
|
69
|
+
}
|
|
70
|
+
function selector(metric, matchers) {
|
|
71
|
+
if (matchers.length === 0)
|
|
72
|
+
return metric;
|
|
73
|
+
return `${metric}{${matchers.map(([l, op, v]) => `${l}${op}${quote(v)}`).join(", ")}}`;
|
|
74
|
+
}
|
|
75
|
+
function sumBy(labels, inner) {
|
|
76
|
+
return `sum by (${labels.join(", ")}) (${inner})`;
|
|
77
|
+
}
|
|
78
|
+
function metricsOf(genAi) {
|
|
79
|
+
const m = genAi?.metrics ?? genAi;
|
|
80
|
+
if (!m || !m.calls || !m.duration || !m.inputTokens || !m.outputTokens) {
|
|
81
|
+
fail("genAi must be genAiMetrics(...) or genAiComponents(...) from the otel lexicon");
|
|
82
|
+
}
|
|
83
|
+
return m;
|
|
84
|
+
}
|
|
85
|
+
function has(metric, attribute) {
|
|
86
|
+
return metric.dimensions.includes(attribute);
|
|
87
|
+
}
|
|
88
|
+
function need(metric, attribute) {
|
|
89
|
+
if (!has(metric, attribute))
|
|
90
|
+
fail(`${metric.name} has no ${attribute} attribute, so the rules can't be split by it`);
|
|
91
|
+
return prometheusLabel(attribute);
|
|
92
|
+
}
|
|
93
|
+
function seconds(metric) {
|
|
94
|
+
if (metric.type !== "histogram")
|
|
95
|
+
fail(`${metric.name} is a ${metric.type}, not a histogram`);
|
|
96
|
+
if (metric.unit !== "s")
|
|
97
|
+
fail(`${metric.name} is in ${metric.unit ?? "no unit"}; the latency rules read seconds`);
|
|
98
|
+
}
|
|
99
|
+
const A = GENAI_ATTRIBUTES;
|
|
100
|
+
function clientSource(m) {
|
|
101
|
+
const c = m.client;
|
|
102
|
+
if (!c)
|
|
103
|
+
fail('source "client" needs genAiMetrics({ clientMetrics: "derive" | "passthrough" })');
|
|
104
|
+
const d = c.operationDuration;
|
|
105
|
+
const t = c.tokenUsage;
|
|
106
|
+
seconds(d);
|
|
107
|
+
const provider = need(d, A.providerName);
|
|
108
|
+
const model = need(d, A.requestModel);
|
|
109
|
+
const operation = need(d, A.operationName);
|
|
110
|
+
const errorType = need(d, A.errorType);
|
|
111
|
+
const tokenType = need(t, A.tokenType);
|
|
112
|
+
const tokenLabels = [need(t, A.providerName), need(t, A.requestModel)];
|
|
113
|
+
return {
|
|
114
|
+
name: "client",
|
|
115
|
+
requests: `${d.prometheus}_count`,
|
|
116
|
+
errorMatchers: [[errorType, "!=", ""]],
|
|
117
|
+
buckets: `${d.prometheus}_bucket`,
|
|
118
|
+
modelLabels: [provider, model, operation],
|
|
119
|
+
provider,
|
|
120
|
+
tokenExpr: (type, by, window) => sumBy(by, `rate(${selector(`${t.prometheus}_sum`, [[tokenType, "=", type]])}[${window}])`),
|
|
121
|
+
tokenLabels,
|
|
122
|
+
tokensHaveProvider: true,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function spanSource(m) {
|
|
126
|
+
seconds(m.duration);
|
|
127
|
+
const status = need(m.calls, "status.code");
|
|
128
|
+
const model = need(m.calls, A.requestModel);
|
|
129
|
+
const operation = need(m.calls, A.operationName);
|
|
130
|
+
need(m.calls, A.errorType);
|
|
131
|
+
for (const a of [A.requestModel, A.operationName])
|
|
132
|
+
need(m.duration, a);
|
|
133
|
+
const provider = has(m.calls, A.providerName) && has(m.duration, A.providerName) ? prometheusLabel(A.providerName) : undefined;
|
|
134
|
+
const tokenModel = need(m.inputTokens, A.requestModel);
|
|
135
|
+
need(m.outputTokens, A.requestModel);
|
|
136
|
+
const tokensHaveProvider = has(m.inputTokens, A.providerName) && has(m.outputTokens, A.providerName);
|
|
137
|
+
const tokenLabels = tokensHaveProvider ? [prometheusLabel(A.providerName), tokenModel] : [tokenModel];
|
|
138
|
+
const byType = { [GENAI_TOKEN_TYPES.input]: m.inputTokens, [GENAI_TOKEN_TYPES.output]: m.outputTokens };
|
|
139
|
+
return {
|
|
140
|
+
name: "spans",
|
|
141
|
+
requests: m.calls.prometheus,
|
|
142
|
+
errorMatchers: [[status, "=", SPAN_STATUS_ERROR]],
|
|
143
|
+
buckets: `${m.duration.prometheus}_bucket`,
|
|
144
|
+
modelLabels: provider ? [provider, model, operation] : [model, operation],
|
|
145
|
+
provider,
|
|
146
|
+
tokenExpr: (type, by, window) => sumBy(by, `rate(${byType[type].prometheus}[${window}])`),
|
|
147
|
+
tokenLabels,
|
|
148
|
+
tokensHaveProvider,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function checkAlertOptions(at, a) {
|
|
152
|
+
if (a.for !== undefined && !isValidDuration(a.for))
|
|
153
|
+
fail(`${at}.for "${a.for}" is not a Prometheus duration`);
|
|
154
|
+
if (a.severity !== undefined && (typeof a.severity !== "string" || a.severity === ""))
|
|
155
|
+
fail(`${at}.severity must be a non-empty string`);
|
|
156
|
+
}
|
|
157
|
+
function checkRatio(at, value) {
|
|
158
|
+
if (!(typeof value === "number" && value > 0 && value < 1))
|
|
159
|
+
fail(`${at} must be above 0 and below 1, got ${String(value)}`);
|
|
160
|
+
}
|
|
161
|
+
function resolve(props) {
|
|
162
|
+
if (!props || typeof props !== "object")
|
|
163
|
+
fail("props are required");
|
|
164
|
+
const metrics = metricsOf(props.genAi);
|
|
165
|
+
const sourceName = props.source ?? (metrics.client ? "client" : "spans");
|
|
166
|
+
if (sourceName !== "client" && sourceName !== "spans")
|
|
167
|
+
fail(`source must be "client" or "spans", got ${JSON.stringify(sourceName)}`);
|
|
168
|
+
const src = sourceName === "client" ? clientSource(metrics) : spanSource(metrics);
|
|
169
|
+
const prefix = props.prefix ?? "gen_ai";
|
|
170
|
+
if (!PREFIX.test(prefix))
|
|
171
|
+
fail(`prefix "${prefix}" must be letters, digits and '_', not starting with a digit`);
|
|
172
|
+
const window = props.rateWindow ?? "5m";
|
|
173
|
+
if (!isValidDuration(window) || !durationMs(window))
|
|
174
|
+
fail(`rateWindow "${window}" is not a positive Prometheus duration`);
|
|
175
|
+
const name = props.name ?? "genai";
|
|
176
|
+
if (!GROUP_NAME.test(name))
|
|
177
|
+
fail(`name "${name}" must be letters, digits, '.', '_' or '-'`);
|
|
178
|
+
const groupBy = props.groupBy ?? [];
|
|
179
|
+
for (const l of groupBy)
|
|
180
|
+
if (!LABEL.test(l))
|
|
181
|
+
fail(`groupBy label "${l}" is not a Prometheus label name`);
|
|
182
|
+
let tool;
|
|
183
|
+
if (has(metrics.calls, A.toolName) && has(metrics.duration, A.toolName) && has(metrics.calls, "status.code")) {
|
|
184
|
+
seconds(metrics.duration);
|
|
185
|
+
tool = { label: prometheusLabel(A.toolName), status: prometheusLabel("status.code") };
|
|
186
|
+
}
|
|
187
|
+
const prices = props.prices ?? [];
|
|
188
|
+
if (!Array.isArray(prices))
|
|
189
|
+
fail("prices must be a list");
|
|
190
|
+
const seen = new Map();
|
|
191
|
+
prices.forEach((p, i) => {
|
|
192
|
+
const at = `prices[${i}]`;
|
|
193
|
+
for (const k of ["provider", "model", "currency", "source"]) {
|
|
194
|
+
if (typeof p?.[k] !== "string" || p[k].trim() === "")
|
|
195
|
+
fail(`${at}.${k} is required`);
|
|
196
|
+
}
|
|
197
|
+
for (const k of ["inputPerMTok", "outputPerMTok"]) {
|
|
198
|
+
const v = p[k];
|
|
199
|
+
if (!(typeof v === "number" && Number.isFinite(v) && v >= 0))
|
|
200
|
+
fail(`${at}.${k} must be a number at or above 0, got ${String(v)}`);
|
|
201
|
+
}
|
|
202
|
+
if (p.asOf !== undefined && !DATE.test(p.asOf))
|
|
203
|
+
fail(`${at}.asOf must be a date, YYYY-MM-DD, got ${JSON.stringify(p.asOf)}`);
|
|
204
|
+
// Without a provider on the token series, one model priced twice can't be told apart.
|
|
205
|
+
const k = src.tokensHaveProvider ? `${p.provider}\u0000${p.model}` : p.model;
|
|
206
|
+
const before = seen.get(k);
|
|
207
|
+
if (before !== undefined) {
|
|
208
|
+
fail(src.tokensHaveProvider
|
|
209
|
+
? `${at} prices ${p.provider} ${p.model} again (first at ${before})`
|
|
210
|
+
: `${at} prices model ${p.model} again (first at ${before}); the ${src.name} token metrics have no provider to tell them apart`);
|
|
211
|
+
}
|
|
212
|
+
seen.set(k, at);
|
|
213
|
+
});
|
|
214
|
+
const alerts = props.alerts ?? {};
|
|
215
|
+
const ratioAlert = (key) => {
|
|
216
|
+
const a = alerts[key];
|
|
217
|
+
if (a === undefined)
|
|
218
|
+
return;
|
|
219
|
+
if (a !== true) {
|
|
220
|
+
checkAlertOptions(`alerts.${key}`, a);
|
|
221
|
+
if (a.threshold !== undefined)
|
|
222
|
+
checkRatio(`alerts.${key}.threshold`, a.threshold);
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
ratioAlert("errorRatio");
|
|
226
|
+
ratioAlert("toolErrorRatio");
|
|
227
|
+
if (alerts.toolErrorRatio !== undefined && !tool)
|
|
228
|
+
fail(`alerts.toolErrorRatio needs span metrics with a ${A.toolName} attribute`);
|
|
229
|
+
if (alerts.latency !== undefined && alerts.latency !== true) {
|
|
230
|
+
checkAlertOptions("alerts.latency", alerts.latency);
|
|
231
|
+
const s = alerts.latency.thresholdSeconds;
|
|
232
|
+
if (s !== undefined && !(typeof s === "number" && Number.isFinite(s) && s > 0))
|
|
233
|
+
fail(`alerts.latency.thresholdSeconds must be above 0, got ${String(s)}`);
|
|
234
|
+
}
|
|
235
|
+
const currencies = new Set(prices.map((p) => p.currency));
|
|
236
|
+
const budgets = new Map();
|
|
237
|
+
if (alerts.budgets !== undefined && !Array.isArray(alerts.budgets))
|
|
238
|
+
fail("alerts.budgets must be a list");
|
|
239
|
+
(alerts.budgets ?? []).forEach((b, i) => {
|
|
240
|
+
const at = `alerts.budgets[${i}]`;
|
|
241
|
+
const k = `${b?.per} ${b?.currency}`;
|
|
242
|
+
if (budgets.has(k))
|
|
243
|
+
fail(`${at} sets a second budget per ${b.per} in ${b.currency} (first at ${budgets.get(k)})`);
|
|
244
|
+
budgets.set(k, at);
|
|
245
|
+
checkAlertOptions(at, b);
|
|
246
|
+
if (!(typeof b.amount === "number" && Number.isFinite(b.amount) && b.amount > 0))
|
|
247
|
+
fail(`${at}.amount must be above 0, got ${String(b.amount)}`);
|
|
248
|
+
if (b.per !== "hour" && b.per !== "day")
|
|
249
|
+
fail(`${at}.per must be "hour" or "day", got ${JSON.stringify(b.per)}`);
|
|
250
|
+
if (!currencies.has(b.currency)) {
|
|
251
|
+
fail(`${at}.currency ${JSON.stringify(b.currency)} is the currency of no price, so nothing could be spent in it`);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
return { props, metrics, src, prefix, window, groupBy, tool, prices };
|
|
255
|
+
}
|
|
256
|
+
function metricsFor(r) {
|
|
257
|
+
const { prefix: p, window: w, src, groupBy, tool, prices, props } = r;
|
|
258
|
+
const rec = (what) => `${p}:${what}:rate${w}`;
|
|
259
|
+
const alerts = props.alerts ?? {};
|
|
260
|
+
const info = [];
|
|
261
|
+
const sev = (a) => (a === true ? undefined : a.severity) ?? DEFAULTS.severity;
|
|
262
|
+
if (alerts.errorRatio !== undefined) {
|
|
263
|
+
const a = alerts.errorRatio;
|
|
264
|
+
info.push({ alert: "GenAiErrorRatioHigh", kind: "errorRatio", severity: sev(a), threshold: (a === true ? undefined : a.threshold) ?? DEFAULTS.errorRatio });
|
|
265
|
+
}
|
|
266
|
+
if (alerts.latency !== undefined) {
|
|
267
|
+
const a = alerts.latency;
|
|
268
|
+
info.push({ alert: "GenAiLatencyHigh", kind: "latency", severity: sev(a), threshold: (a === true ? undefined : a.thresholdSeconds) ?? DEFAULTS.latencySeconds });
|
|
269
|
+
}
|
|
270
|
+
if (alerts.toolErrorRatio !== undefined) {
|
|
271
|
+
const a = alerts.toolErrorRatio;
|
|
272
|
+
info.push({ alert: "GenAiToolErrorRatioHigh", kind: "toolErrorRatio", severity: sev(a), threshold: (a === true ? undefined : a.threshold) ?? DEFAULTS.toolErrorRatio });
|
|
273
|
+
}
|
|
274
|
+
for (const b of alerts.budgets ?? []) {
|
|
275
|
+
info.push({ alert: "GenAiSpendOverBudget", kind: "budget", severity: b.severity ?? DEFAULTS.severity, threshold: b.amount, currency: b.currency, per: b.per });
|
|
276
|
+
}
|
|
277
|
+
return {
|
|
278
|
+
group: props.name ?? "genai",
|
|
279
|
+
source: src.name,
|
|
280
|
+
rateWindow: w,
|
|
281
|
+
labels: {
|
|
282
|
+
...(src.provider ? { provider: src.provider } : {}),
|
|
283
|
+
model: prometheusLabel(A.requestModel),
|
|
284
|
+
operation: prometheusLabel(A.operationName),
|
|
285
|
+
errorType: prometheusLabel(A.errorType),
|
|
286
|
+
tokenType: prometheusLabel(A.tokenType),
|
|
287
|
+
...(tool ? { tool: tool.label } : {}),
|
|
288
|
+
currency: CURRENCY_LABEL,
|
|
289
|
+
quantile: QUANTILE_LABEL,
|
|
290
|
+
},
|
|
291
|
+
modelLabels: [...src.modelLabels, ...groupBy],
|
|
292
|
+
tokenLabels: [...src.tokenLabels, ...groupBy],
|
|
293
|
+
requests: rec("requests"),
|
|
294
|
+
errors: rec("errors"),
|
|
295
|
+
errorRatio: rec("error_ratio"),
|
|
296
|
+
errorRatioByType: rec("error_ratio_by_type"),
|
|
297
|
+
latency: { record: `${p}:operation_duration_seconds:quantile_rate${w}`, quantiles: [...QUANTILES] },
|
|
298
|
+
tokens: rec("tokens"),
|
|
299
|
+
...(prices.length > 0 ? { cost: rec("cost") } : {}),
|
|
300
|
+
...(tool
|
|
301
|
+
? {
|
|
302
|
+
tool: {
|
|
303
|
+
calls: rec("tool_calls"),
|
|
304
|
+
errors: rec("tool_errors"),
|
|
305
|
+
errorRatio: rec("tool_error_ratio"),
|
|
306
|
+
latency: { record: `${p}:tool_duration_seconds:quantile_rate${w}`, quantiles: [ALERT_QUANTILE] },
|
|
307
|
+
},
|
|
308
|
+
}
|
|
309
|
+
: {}),
|
|
310
|
+
prices: prices.map((x) => ({ provider: x.provider, model: x.model, currency: x.currency, source: x.source, ...(x.asOf ? { asOf: x.asOf } : {}) })),
|
|
311
|
+
currencies: [...new Set(prices.map((x) => x.currency))],
|
|
312
|
+
alerts: info,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
function recordingRules(r, m) {
|
|
316
|
+
const { src, window: w, metrics, tool, prices } = r;
|
|
317
|
+
const model = m.labels.model;
|
|
318
|
+
const errorType = m.labels.errorType;
|
|
319
|
+
const tokenType = m.labels.tokenType;
|
|
320
|
+
// Spans without a model (tool calls, in-process agent steps) aren't model requests.
|
|
321
|
+
const scope = [[model, "!=", ""]];
|
|
322
|
+
const rate = (metric, matchers) => `rate(${selector(metric, matchers)}[${w}])`;
|
|
323
|
+
const rules = [];
|
|
324
|
+
rules.push({ record: m.requests, expr: sumBy(m.modelLabels, rate(src.requests, scope)) });
|
|
325
|
+
rules.push({ record: m.errors, expr: sumBy([...m.modelLabels, errorType], rate(src.requests, [...scope, ...src.errorMatchers])) });
|
|
326
|
+
// `or 0 * requests` makes the ratio 0, not absent, for a model with no errors yet.
|
|
327
|
+
rules.push({ record: m.errorRatio, expr: `(\n sum without (${errorType}) (${m.errors})\n or\n 0 * ${m.requests}\n)\n/\n${m.requests}` });
|
|
328
|
+
rules.push({ record: m.errorRatioByType, expr: `${m.errors}\n/ ignoring (${errorType}) group_left\n${m.requests}` });
|
|
329
|
+
for (const q of QUANTILES) {
|
|
330
|
+
rules.push({
|
|
331
|
+
record: m.latency.record,
|
|
332
|
+
expr: `histogram_quantile(${q}, ${sumBy([...m.modelLabels, "le"], rate(src.buckets, scope))})`,
|
|
333
|
+
labels: { [QUANTILE_LABEL]: q },
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
for (const type of [GENAI_TOKEN_TYPES.input, GENAI_TOKEN_TYPES.output]) {
|
|
337
|
+
rules.push({ record: m.tokens, expr: src.tokenExpr(type, m.tokenLabels, w), labels: { [tokenType]: type } });
|
|
338
|
+
}
|
|
339
|
+
if (m.cost) {
|
|
340
|
+
const provider = prometheusLabel(A.providerName);
|
|
341
|
+
for (const price of prices) {
|
|
342
|
+
const of = (type, perMTok) => {
|
|
343
|
+
const ms = [[model, "=", price.model], [tokenType, "=", type]];
|
|
344
|
+
if (src.tokensHaveProvider)
|
|
345
|
+
ms.unshift([provider, "=", price.provider]);
|
|
346
|
+
return `${selector(m.tokens, ms)} * ${num(perMTok)} / 1000000`;
|
|
347
|
+
};
|
|
348
|
+
rules.push({
|
|
349
|
+
record: m.cost,
|
|
350
|
+
expr: `${of(GENAI_TOKEN_TYPES.input, price.inputPerMTok)}\nor\n${of(GENAI_TOKEN_TYPES.output, price.outputPerMTok)}`,
|
|
351
|
+
// Provider and model as rule labels too: they tell the cost rules apart (PROM102), and
|
|
352
|
+
// carry the provider onto span token sums, which have none.
|
|
353
|
+
labels: { [provider]: price.provider, [model]: price.model, [CURRENCY_LABEL]: price.currency },
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
if (tool && m.tool) {
|
|
358
|
+
const t = m.tool;
|
|
359
|
+
const toolScope = [[tool.label, "!=", ""]];
|
|
360
|
+
const by = [tool.label, ...r.groupBy];
|
|
361
|
+
rules.push({ record: t.calls, expr: sumBy(by, rate(metrics.calls.prometheus, toolScope)) });
|
|
362
|
+
rules.push({ record: t.errors, expr: sumBy(by, rate(metrics.calls.prometheus, [...toolScope, [tool.status, "=", SPAN_STATUS_ERROR]])) });
|
|
363
|
+
rules.push({ record: t.errorRatio, expr: `(\n ${t.errors}\n or\n 0 * ${t.calls}\n)\n/\n${t.calls}` });
|
|
364
|
+
rules.push({
|
|
365
|
+
record: t.latency.record,
|
|
366
|
+
expr: `histogram_quantile(${ALERT_QUANTILE}, ${sumBy([...by, "le"], rate(`${metrics.duration.prometheus}_bucket`, toolScope))})`,
|
|
367
|
+
labels: { [QUANTILE_LABEL]: ALERT_QUANTILE },
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
return rules;
|
|
371
|
+
}
|
|
372
|
+
function alertRules(r, m) {
|
|
373
|
+
const alerts = r.props.alerts ?? {};
|
|
374
|
+
const l = m.labels;
|
|
375
|
+
const who = `{{ $labels.${l.model} }}${l.provider ? ` ({{ $labels.${l.provider} }})` : ""} {{ $labels.${l.operation} }}`;
|
|
376
|
+
const out = [];
|
|
377
|
+
const build = (a, info, expr, summary, description, extra = {}) => {
|
|
378
|
+
const o = a === true ? {} : a;
|
|
379
|
+
out.push({
|
|
380
|
+
alert: info.alert,
|
|
381
|
+
expr,
|
|
382
|
+
for: o.for ?? DEFAULTS.for,
|
|
383
|
+
labels: { ...(o.labels ?? {}), ...extra, severity: info.severity },
|
|
384
|
+
annotations: { summary, description, ...(o.annotations ?? {}) },
|
|
385
|
+
});
|
|
386
|
+
};
|
|
387
|
+
const byKind = (k) => m.alerts.filter((x) => x.kind === k);
|
|
388
|
+
if (alerts.errorRatio !== undefined) {
|
|
389
|
+
const info = byKind("errorRatio")[0];
|
|
390
|
+
build(alerts.errorRatio, info, `${m.errorRatio} > ${num(info.threshold)}`, `GenAI error ratio above ${num(info.threshold * 100)}% for ${who}`, `{{ $value | humanizePercentage }} of ${who} requests failed over the last ${r.window}.`);
|
|
391
|
+
}
|
|
392
|
+
if (alerts.latency !== undefined) {
|
|
393
|
+
const info = byKind("latency")[0];
|
|
394
|
+
build(alerts.latency, info, `${selector(m.latency.record, [[QUANTILE_LABEL, "=", ALERT_QUANTILE]])} > ${num(info.threshold)}`, `GenAI p95 latency above ${num(info.threshold)}s for ${who}`, `p95 latency of ${who} is {{ $value | humanizeDuration }} over the last ${r.window}.`);
|
|
395
|
+
}
|
|
396
|
+
if (alerts.toolErrorRatio !== undefined && m.tool) {
|
|
397
|
+
const info = byKind("toolErrorRatio")[0];
|
|
398
|
+
const tool = `{{ $labels.${l.tool} }}`;
|
|
399
|
+
build(alerts.toolErrorRatio, info, `${m.tool.errorRatio} > ${num(info.threshold)}`, `Tool ${tool} error ratio above ${num(info.threshold * 100)}%`, `{{ $value | humanizePercentage }} of calls to tool ${tool} failed over the last ${r.window}.`);
|
|
400
|
+
}
|
|
401
|
+
(alerts.budgets ?? []).forEach((b, i) => {
|
|
402
|
+
const info = byKind("budget")[i];
|
|
403
|
+
// The average of the recorded per-second rate over the window, times its length, is what the window cost.
|
|
404
|
+
const spend = `sum by (${CURRENCY_LABEL}) (avg_over_time(${selector(m.cost, [[CURRENCY_LABEL, "=", b.currency]])}[${BUDGET_RANGE[b.per]}])) * ${BUDGET_SECONDS[b.per]}`;
|
|
405
|
+
build({ for: "0s", ...b }, info, `${spend} > ${num(b.amount)}`, `GenAI spend over the ${b.per === "hour" ? "hourly" : "daily"} budget of ${num(b.amount)} ${b.currency}`, `Model calls cost {{ $value | printf "%.2f" }} ${b.currency} over the last ${BUDGET_RANGE[b.per]}, above the budget of ${num(b.amount)}. Models without a price are not counted.`, { budget: b.per, [CURRENCY_LABEL]: b.currency });
|
|
406
|
+
});
|
|
407
|
+
// A for of 0s says nothing; leave it out.
|
|
408
|
+
for (const rule of out)
|
|
409
|
+
if (rule.for === "0s")
|
|
410
|
+
delete rule.for;
|
|
411
|
+
return out;
|
|
412
|
+
}
|
|
413
|
+
/**
|
|
414
|
+
* Recording rules and opt-in alerts for GenAI calls, from the otel preset's metrics.
|
|
415
|
+
*
|
|
416
|
+
* @example
|
|
417
|
+
* ```ts
|
|
418
|
+
* import { genAiMetrics } from "@intentius/chant-lexicon-otel";
|
|
419
|
+
* import { GenAiRules } from "@intentius/chant-lexicon-prometheus";
|
|
420
|
+
*
|
|
421
|
+
* export const genai = GenAiRules({
|
|
422
|
+
* genAi: genAiMetrics({ clientMetrics: "derive" }),
|
|
423
|
+
* prices: [{ provider: "anthropic", model: "claude-x", inputPerMTok: 3, outputPerMTok: 15, currency: "USD", source: "https://example.com/pricing", asOf: "2026-09-29" }],
|
|
424
|
+
* alerts: { errorRatio: true, budgets: [{ amount: 50, currency: "USD", per: "day" }] },
|
|
425
|
+
* });
|
|
426
|
+
* // genai.rules is a RuleGroup; genAiRuleMetrics(genai) names its series.
|
|
427
|
+
* ```
|
|
428
|
+
*/
|
|
429
|
+
export const GenAiRules = Composite((props) => {
|
|
430
|
+
const r = resolve(props);
|
|
431
|
+
const m = metricsFor(r);
|
|
432
|
+
const group = new RuleGroup({
|
|
433
|
+
name: m.group,
|
|
434
|
+
...(props.interval !== undefined ? { interval: props.interval } : {}),
|
|
435
|
+
...(props.labels !== undefined ? { labels: props.labels } : {}),
|
|
436
|
+
rules: [...recordingRules(r, m), ...alertRules(r, m)],
|
|
437
|
+
});
|
|
438
|
+
Object.defineProperty(group, GENAI_RULES_METRICS, { value: m, enumerable: false });
|
|
439
|
+
return { rules: group };
|
|
440
|
+
}, "GenAiRules");
|
|
441
|
+
/**
|
|
442
|
+
* The series a `GenAiRules` records and the alerts it builds. Pass the
|
|
443
|
+
* `GenAiRules(...)` result, its rule group, or the props it was built from;
|
|
444
|
+
* a dashboard reads names from here instead of repeating them.
|
|
445
|
+
*/
|
|
446
|
+
export function genAiRuleMetrics(rules) {
|
|
447
|
+
const stashed = (x) => typeof x === "object" && x !== null ? x[GENAI_RULES_METRICS] : undefined;
|
|
448
|
+
const direct = stashed(rules) ?? stashed(rules?.rules);
|
|
449
|
+
if (direct)
|
|
450
|
+
return structuredClone(direct);
|
|
451
|
+
if (typeof rules === "object" && rules !== null && "genAi" in rules)
|
|
452
|
+
return metricsFor(resolve(rules));
|
|
453
|
+
throw new Error("genAiRuleMetrics: pass a GenAiRules(...) result, its rule group, or GenAiRules props");
|
|
454
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prometheus lexicon composites: declarations that expand to rule groups.
|
|
3
|
+
*/
|
|
4
|
+
export { Slo, sloMetrics, sloPropsProblem, sliExprProblem, eventCount, DEFAULT_BURN_RATES, SLO_WINDOW_PLACEHOLDER, } from "./slo.js";
|
|
5
|
+
export { GenAiRules, genAiRuleMetrics } from "./genai.js";
|
|
6
|
+
export { RedAlerts, redAlertRules, RED_ALERT_NAMES } from "./red-alerts.js";
|
|
7
|
+
export { Watchdog, WATCHDOG_URL_FILE } from "./watchdog.js";
|
|
8
|
+
export { AlertRouting, ALERT_ROUTING_LEVELS } from "./alert-routing.js";
|
|
@@ -0,0 +1,135 @@
|
|
|
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
|
+
import { Composite } from "@intentius/chant/composite";
|
|
19
|
+
import { promNumber, spanMetricsNames, spanMetricsRedQueries, } from "@intentius/chant-lexicon-otel/metric-names";
|
|
20
|
+
import { RuleGroup } from "../rules.js";
|
|
21
|
+
import { durationMs, isValidDuration } from "../duration.js";
|
|
22
|
+
const LABEL = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
23
|
+
const GROUP_NAME = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
|
|
24
|
+
/** The alert names `RedAlerts` writes. */
|
|
25
|
+
export const RED_ALERT_NAMES = Object.freeze({ errorRatio: "ServiceErrorRatioHigh", latency: "ServiceLatencyHigh" });
|
|
26
|
+
const DEFAULTS = { threshold: 0.05, quantile: 0.95, thresholdSeconds: 1, for: "10m", severity: "warning" };
|
|
27
|
+
function fail(message) {
|
|
28
|
+
throw new Error(`RedAlerts: ${message}`);
|
|
29
|
+
}
|
|
30
|
+
function isNames(x) {
|
|
31
|
+
return typeof x === "object" && x !== null && "calls" in x && "labels" in x && "errorStatus" in x;
|
|
32
|
+
}
|
|
33
|
+
function options(value) {
|
|
34
|
+
if (value === false)
|
|
35
|
+
return undefined;
|
|
36
|
+
return value === undefined || value === true ? {} : value;
|
|
37
|
+
}
|
|
38
|
+
function checkOptions(at, a) {
|
|
39
|
+
if (a.for !== undefined && !isValidDuration(a.for))
|
|
40
|
+
fail(`${at}.for "${a.for}" is not a Prometheus duration`);
|
|
41
|
+
if (a.severity !== undefined && (typeof a.severity !== "string" || a.severity === ""))
|
|
42
|
+
fail(`${at}.severity must be a non-empty string`);
|
|
43
|
+
}
|
|
44
|
+
/** The alerting rules `RedAlerts` builds, without the group: for a group of your own. */
|
|
45
|
+
export function redAlertRules(props) {
|
|
46
|
+
if (!props || typeof props !== "object")
|
|
47
|
+
fail("props are required");
|
|
48
|
+
if (!props.spanMetrics)
|
|
49
|
+
fail("spanMetrics is required (a spanmetrics connector)");
|
|
50
|
+
const names = isNames(props.spanMetrics) ? props.spanMetrics : spanMetricsNames(props.spanMetrics, props.exporter);
|
|
51
|
+
const window = props.rateWindow ?? "5m";
|
|
52
|
+
if (!isValidDuration(window) || !durationMs(window))
|
|
53
|
+
fail(`rateWindow "${window}" is not a positive Prometheus duration`);
|
|
54
|
+
const groupBy = props.groupBy ?? [];
|
|
55
|
+
for (const l of groupBy)
|
|
56
|
+
if (!LABEL.test(l))
|
|
57
|
+
fail(`groupBy label "${l}" is not a Prometheus label name`);
|
|
58
|
+
if (props.minRate !== undefined && !(typeof props.minRate === "number" && Number.isFinite(props.minRate) && props.minRate > 0)) {
|
|
59
|
+
fail(`minRate must be above 0, got ${String(props.minRate)}`);
|
|
60
|
+
}
|
|
61
|
+
const err = options(props.errorRatio);
|
|
62
|
+
// The latency alert is on by default only when there is a histogram to read.
|
|
63
|
+
const lat = props.latency === undefined && !names.duration ? undefined : options(props.latency);
|
|
64
|
+
if (lat && !names.duration)
|
|
65
|
+
fail("latency needs the connector's duration histogram, and it is disabled");
|
|
66
|
+
if (err) {
|
|
67
|
+
checkOptions("errorRatio", err);
|
|
68
|
+
const t = err.threshold;
|
|
69
|
+
if (t !== undefined && !(typeof t === "number" && t > 0 && t < 1))
|
|
70
|
+
fail(`errorRatio.threshold must be above 0 and below 1, got ${String(t)}`);
|
|
71
|
+
}
|
|
72
|
+
if (lat) {
|
|
73
|
+
checkOptions("latency", lat);
|
|
74
|
+
const q = lat.quantile;
|
|
75
|
+
if (q !== undefined && !(typeof q === "number" && q > 0 && q < 1))
|
|
76
|
+
fail(`latency.quantile must be between 0 and 1, got ${String(q)}`);
|
|
77
|
+
const s = lat.thresholdSeconds;
|
|
78
|
+
if (s !== undefined && !(typeof s === "number" && Number.isFinite(s) && s > 0))
|
|
79
|
+
fail(`latency.thresholdSeconds must be above 0, got ${String(s)}`);
|
|
80
|
+
}
|
|
81
|
+
const quantile = lat?.quantile ?? DEFAULTS.quantile;
|
|
82
|
+
const q = spanMetricsRedQueries(names, { range: window, quantiles: [quantile], spanKinds: props.spanKinds, by: groupBy, owner: "RedAlerts" });
|
|
83
|
+
const by = [q.service, ...groupBy];
|
|
84
|
+
const floor = props.minRate !== undefined ? `\nand on (${by.join(", ")})\n${q.rate} > ${promNumber(props.minRate)}` : "";
|
|
85
|
+
const svc = `{{ $labels.${q.service} }}`;
|
|
86
|
+
const out = [];
|
|
87
|
+
const build = (o, alert, expr, summary, description) => {
|
|
88
|
+
out.push({
|
|
89
|
+
alert,
|
|
90
|
+
expr,
|
|
91
|
+
for: o.for ?? DEFAULTS.for,
|
|
92
|
+
labels: { ...(o.labels ?? {}), severity: o.severity ?? DEFAULTS.severity },
|
|
93
|
+
annotations: { summary, description, ...(o.annotations ?? {}) },
|
|
94
|
+
});
|
|
95
|
+
};
|
|
96
|
+
if (err) {
|
|
97
|
+
const threshold = err.threshold ?? DEFAULTS.threshold;
|
|
98
|
+
build(err, RED_ALERT_NAMES.errorRatio, `${q.errorRatio} > ${promNumber(threshold)}${floor}`, `Error ratio above ${promNumber(threshold * 100)}% for ${svc}`, `{{ $value | humanizePercentage }} of ${svc} spans ended in error over the last ${window}.`);
|
|
99
|
+
}
|
|
100
|
+
if (lat && names.duration) {
|
|
101
|
+
const seconds = lat.thresholdSeconds ?? DEFAULTS.thresholdSeconds;
|
|
102
|
+
const inUnit = names.duration.unit === "s" ? seconds : seconds * 1000;
|
|
103
|
+
const qName = `p${promNumber(quantile * 100)}`;
|
|
104
|
+
const shown = names.duration.unit === "s" ? "{{ $value | humanizeDuration }}" : "{{ $value | printf \"%.0f\" }}ms";
|
|
105
|
+
build(lat, RED_ALERT_NAMES.latency, `${q.duration[0].expr} > ${promNumber(inUnit)}${floor}`, `${qName} duration above ${promNumber(seconds)}s for ${svc}`, `${qName} duration of ${svc} is ${shown} over the last ${window}.`);
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Error-ratio and latency alerts per service, from a `spanmetrics` connector.
|
|
111
|
+
*
|
|
112
|
+
* @example
|
|
113
|
+
* ```ts
|
|
114
|
+
* import { RedMetrics } from "@intentius/chant-lexicon-otel";
|
|
115
|
+
* import { RedAlerts } from "@intentius/chant-lexicon-prometheus";
|
|
116
|
+
*
|
|
117
|
+
* export const red = RedMetrics({ spanMetrics: { namespace: "shop", histogram: { unit: "s" } } });
|
|
118
|
+
* export const redAlerts = RedAlerts({ spanMetrics: red.spanMetrics, exporter: red.exporter, latency: { thresholdSeconds: 0.5 } });
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export const RedAlerts = Composite((props) => {
|
|
122
|
+
const rules = redAlertRules(props);
|
|
123
|
+
const name = props.name ?? "red";
|
|
124
|
+
if (!GROUP_NAME.test(name))
|
|
125
|
+
fail(`name "${name}" must be letters, digits, '.', '_' or '-'`);
|
|
126
|
+
if (rules.length === 0)
|
|
127
|
+
fail("errorRatio and latency are both off, so there is nothing to alert on");
|
|
128
|
+
const group = new RuleGroup({
|
|
129
|
+
name,
|
|
130
|
+
...(props.interval !== undefined ? { interval: props.interval } : {}),
|
|
131
|
+
...(props.labels !== undefined ? { labels: props.labels } : {}),
|
|
132
|
+
rules,
|
|
133
|
+
});
|
|
134
|
+
return { rules: group };
|
|
135
|
+
}, "RedAlerts");
|