@intentius/chant-lexicon-prometheus 0.102.0 → 0.104.1
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 +3 -1
- package/dist/collector-metrics.d.ts +58 -0
- package/dist/collector-metrics.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +19 -4
- package/dist/lint/audit-catalog.d.ts +2 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/audit-lineage.d.ts +21 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/prom-helpers.d.ts +9 -2
- package/dist/lint/post-synth/prom-helpers.d.ts.map +1 -1
- package/dist/lint/post-synth/prom211.d.ts +12 -0
- package/dist/lint/post-synth/prom211.d.ts.map +1 -0
- package/dist/lint/post-synth/prom212.d.ts +9 -0
- package/dist/lint/post-synth/prom212.d.ts.map +1 -0
- package/dist/lint/post-synth/prom213.d.ts +11 -0
- package/dist/lint/post-synth/prom213.d.ts.map +1 -0
- package/dist/lint/post-synth/prom214.d.ts +9 -0
- package/dist/lint/post-synth/prom214.d.ts.map +1 -0
- package/dist/lint/post-synth/prom215.d.ts +9 -0
- package/dist/lint/post-synth/prom215.d.ts.map +1 -0
- package/dist/lint/post-synth/prom216.d.ts +10 -0
- package/dist/lint/post-synth/prom216.d.ts.map +1 -0
- package/dist/lint/post-synth/prom217.d.ts +9 -0
- package/dist/lint/post-synth/prom217.d.ts.map +1 -0
- package/dist/lint/post-synth/prom218.d.ts +10 -0
- package/dist/lint/post-synth/prom218.d.ts.map +1 -0
- package/dist/lint/post-synth/prom219.d.ts +9 -0
- package/dist/lint/post-synth/prom219.d.ts.map +1 -0
- package/dist/lint/post-synth/prom220.d.ts +9 -0
- package/dist/lint/post-synth/prom220.d.ts.map +1 -0
- package/dist/lint/post-synth/prom221.d.ts +10 -0
- package/dist/lint/post-synth/prom221.d.ts.map +1 -0
- package/dist/lint/post-synth/prom222.d.ts +9 -0
- package/dist/lint/post-synth/prom222.d.ts.map +1 -0
- package/dist/lint/post-synth/prom223.d.ts +10 -0
- package/dist/lint/post-synth/prom223.d.ts.map +1 -0
- package/dist/lint/post-synth/prom224.d.ts +9 -0
- package/dist/lint/post-synth/prom224.d.ts.map +1 -0
- package/dist/lint/post-synth/prom301.d.ts +14 -0
- package/dist/lint/post-synth/prom301.d.ts.map +1 -0
- package/dist/lint/rules/literal-credential.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +15 -0
- package/dist/okf/rules/PROM211.md +11 -0
- package/dist/okf/rules/PROM212.md +11 -0
- package/dist/okf/rules/PROM213.md +11 -0
- package/dist/okf/rules/PROM214.md +11 -0
- package/dist/okf/rules/PROM215.md +11 -0
- package/dist/okf/rules/PROM216.md +11 -0
- package/dist/okf/rules/PROM217.md +11 -0
- package/dist/okf/rules/PROM218.md +11 -0
- package/dist/okf/rules/PROM219.md +11 -0
- package/dist/okf/rules/PROM220.md +11 -0
- package/dist/okf/rules/PROM221.md +11 -0
- package/dist/okf/rules/PROM222.md +11 -0
- package/dist/okf/rules/PROM223.md +11 -0
- package/dist/okf/rules/PROM224.md +11 -0
- package/dist/okf/rules/PROM301.md +11 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/promql-analysis.d.ts +82 -0
- package/dist/promql-analysis.d.ts.map +1 -0
- package/dist/rules/literal-credential.ts +4 -0
- package/dist/rules/prom-helpers.ts +41 -2
- package/dist/rules/prom211.ts +21 -0
- package/dist/rules/prom212.ts +18 -0
- package/dist/rules/prom213.ts +20 -0
- package/dist/rules/prom214.ts +18 -0
- package/dist/rules/prom215.ts +18 -0
- package/dist/rules/prom216.ts +19 -0
- package/dist/rules/prom217.ts +18 -0
- package/dist/rules/prom218.ts +19 -0
- package/dist/rules/prom219.ts +18 -0
- package/dist/rules/prom220.ts +18 -0
- package/dist/rules/prom221.ts +19 -0
- package/dist/rules/prom222.ts +18 -0
- package/dist/rules/prom223.ts +19 -0
- package/dist/rules/prom224.ts +18 -0
- package/dist/rules/prom301.ts +23 -0
- package/dist/validate-config.d.ts +9 -3
- package/dist/validate-config.d.ts.map +1 -1
- package/dist/validate-integrations.d.ts +1 -0
- package/dist/validate-integrations.d.ts.map +1 -1
- package/dist/validate-security.d.ts +13 -0
- package/dist/validate-security.d.ts.map +1 -0
- package/package.json +2 -2
- package/src/collector-metrics.ts +262 -0
- package/src/composites/composites.test.ts +2 -5
- package/src/import/testdata/alertmanager-integrations.yml +5 -1
- package/src/index.ts +1 -0
- package/src/init-templates.test.ts +3 -1
- package/src/lint/audit-catalog.ts +102 -0
- package/src/lint/audit-lineage.test.ts +74 -0
- package/src/lint/audit-lineage.ts +28 -0
- package/src/lint/post-synth/checks.test.ts +257 -0
- package/src/lint/post-synth/index.ts +30 -0
- package/src/lint/post-synth/post-synth.test.ts +3 -1
- package/src/lint/post-synth/prom-helpers.ts +41 -2
- package/src/lint/post-synth/prom211.ts +21 -0
- package/src/lint/post-synth/prom212.ts +18 -0
- package/src/lint/post-synth/prom213.ts +20 -0
- package/src/lint/post-synth/prom214.ts +18 -0
- package/src/lint/post-synth/prom215.ts +18 -0
- package/src/lint/post-synth/prom216.ts +19 -0
- package/src/lint/post-synth/prom217.ts +18 -0
- package/src/lint/post-synth/prom218.ts +19 -0
- package/src/lint/post-synth/prom219.ts +18 -0
- package/src/lint/post-synth/prom220.ts +18 -0
- package/src/lint/post-synth/prom221.ts +19 -0
- package/src/lint/post-synth/prom222.ts +18 -0
- package/src/lint/post-synth/prom223.ts +19 -0
- package/src/lint/post-synth/prom224.ts +18 -0
- package/src/lint/post-synth/prom301.test.ts +157 -0
- package/src/lint/post-synth/prom301.ts +23 -0
- package/src/lint/rules/literal-credential.ts +4 -0
- package/src/lint/rules/rules.test.ts +16 -0
- package/src/plugin.test.ts +22 -0
- package/src/plugin.ts +12 -1
- package/src/promql-analysis.ts +365 -0
- package/src/validate-config.ts +274 -5
- package/src/validate-integrations.ts +1 -1
- package/src/validate-security.ts +143 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings Alertmanager accepts that weaken how it talks to a receiver
|
|
3
|
+
* (PROM220-PROM222): certificate checks turned off, SMTP credentials sent
|
|
4
|
+
* without TLS, and credentials sent to an `http://` URL. `amtool
|
|
5
|
+
* check-config` passes all three.
|
|
6
|
+
*/
|
|
7
|
+
import type { AlertmanagerGlobalConfig, ReceiverConfig } from "./model.js";
|
|
8
|
+
import type { PrometheusIssue } from "./validate-config.js";
|
|
9
|
+
/** PROM220-PROM222 for one receiver. */
|
|
10
|
+
export declare function validateReceiverSecurity(receiver: ReceiverConfig, global: AlertmanagerGlobalConfig): PrometheusIssue[];
|
|
11
|
+
/** PROM220 for `global:`'s own TLS settings, which every receiver without its own inherits. */
|
|
12
|
+
export declare function validateGlobalSecurity(global: AlertmanagerGlobalConfig): PrometheusIssue[];
|
|
13
|
+
//# sourceMappingURL=validate-security.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validate-security.d.ts","sourceRoot":"","sources":["../src/validate-security.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACxE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AA4DzD,wCAAwC;AACxC,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAE,wBAAwB,GAAG,eAAe,EAAE,CA+DtH;AAED,+FAA+F;AAC/F,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,wBAAwB,GAAG,eAAe,EAAE,CAO1F"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-prometheus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.104.1",
|
|
4
4
|
"description": "Prometheus lexicon for chant — typed recording and alerting rule groups and Alertmanager routing, serialized to a rule file and alertmanager.yml",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
"typescript": "^5.9.3"
|
|
73
73
|
},
|
|
74
74
|
"peerDependencies": {
|
|
75
|
-
"@intentius/chant": "^0.
|
|
75
|
+
"@intentius/chant": "^0.104.1",
|
|
76
76
|
"typescript": "^5.9.3"
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The metric names the collector configs in a build emit through their
|
|
3
|
+
* `spanmetrics`, `servicegraph`, `sum` and `signaltometrics` connectors (the
|
|
4
|
+
* last two are how the otel GenAI preset counts tokens and derives the
|
|
5
|
+
* conventions' client metrics), and the PromQL that reads under those
|
|
6
|
+
* connectors' namespaces without matching one of them (PROM301).
|
|
7
|
+
*
|
|
8
|
+
* Grafana's GRAF118 runs `collectorMetricIssues` over panel queries; the
|
|
9
|
+
* prometheus lexicon runs it over rule files. Names follow the otel lexicon's
|
|
10
|
+
* `metric-names.ts`, with each config's `prometheus` and
|
|
11
|
+
* `prometheusremotewrite` exporters' `namespace` and `add_metric_suffixes`.
|
|
12
|
+
* A component under a renamed type (`span_metrics`, `service_graph`,
|
|
13
|
+
* `signal_to_metrics`) is read as the built-in it names, through the otel
|
|
14
|
+
* lexicon's `canonicalTypeOf`.
|
|
15
|
+
*
|
|
16
|
+
* Collector configs come from two places. `chant build` gives a lexicon's
|
|
17
|
+
* checks only that lexicon's output, but every entity in the build, so the
|
|
18
|
+
* otel lexicon's own config is rebuilt from the build's otel entities with
|
|
19
|
+
* `buildCollectorConfig`. Any output document shaped like a collector config,
|
|
20
|
+
* or a Kubernetes ConfigMap holding one, is read as well.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
24
|
+
import { buildCollectorConfig } from "@intentius/chant-lexicon-otel/collector";
|
|
25
|
+
import { isOTelComponent } from "@intentius/chant-lexicon-otel/define";
|
|
26
|
+
import { collectorConfigs } from "@intentius/chant-lexicon-otel/lint/post-synth/otel-helpers";
|
|
27
|
+
import {
|
|
28
|
+
prometheusLabel,
|
|
29
|
+
prometheusMetricName,
|
|
30
|
+
serviceGraphNames,
|
|
31
|
+
spanMetricsNames,
|
|
32
|
+
SERVICEGRAPH_NAMESPACE,
|
|
33
|
+
SPANMETRICS_DEFAULT_NAMESPACE,
|
|
34
|
+
type CollectorMetric,
|
|
35
|
+
type PrometheusNaming,
|
|
36
|
+
type SpanMetricsNamingConfig,
|
|
37
|
+
} from "@intentius/chant-lexicon-otel/metric-names";
|
|
38
|
+
import { canonicalTypeOf, type CollectorConfig } from "@intentius/chant-lexicon-otel/model";
|
|
39
|
+
import { parsePromql, selectors } from "./promql-analysis";
|
|
40
|
+
|
|
41
|
+
type SyntaxNode = NonNullable<ReturnType<typeof parsePromql>>;
|
|
42
|
+
type Obj = Record<string, unknown>;
|
|
43
|
+
|
|
44
|
+
/** One metric a collector config emits, as Prometheus sees it. */
|
|
45
|
+
export interface EmittedMetric {
|
|
46
|
+
/** The connector that emits it, e.g. `spanmetrics/genai`. */
|
|
47
|
+
connector: string;
|
|
48
|
+
/** The labels a series of it can carry; undefined when the exporter adds every resource attribute. */
|
|
49
|
+
labels?: Set<string>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What the collector configs in a build emit. */
|
|
53
|
+
export interface CollectorMetrics {
|
|
54
|
+
/** The name prefixes the connectors own, e.g. `traces_span_metrics_`. */
|
|
55
|
+
namespaces: string[];
|
|
56
|
+
/** Every emitted Prometheus name. A histogram is its `_bucket`, `_sum` and `_count` series. */
|
|
57
|
+
metrics: Map<string, EmittedMetric[]>;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Labels the `prometheus` exporter, or the scrape, adds to every series: `job` and `instance` from the resource, and the scope. */
|
|
61
|
+
const ADDED_LABELS = ["job", "instance", "otel_scope_name", "otel_scope_version", "otel_scope_schema_url"];
|
|
62
|
+
|
|
63
|
+
function asObj(v: unknown): Obj {
|
|
64
|
+
return v && typeof v === "object" && !Array.isArray(v) ? (v as Obj) : {};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A component's type, with a renamed built-in's new name read as the old one (`span_metrics/genai` is `spanmetrics`). */
|
|
68
|
+
function typeOf(kind: "connector" | "exporter", id: string): string {
|
|
69
|
+
return canonicalTypeOf(kind, id);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function sanitize(name: string): string {
|
|
73
|
+
return name.replace(/[^A-Za-z0-9_:]/g, "_");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** The common dotted prefix of a connector's metric names (`genai.tokens` for `genai.tokens.input` and `.output`). */
|
|
77
|
+
function commonPrefix(names: string[]): string | undefined {
|
|
78
|
+
if (names.length === 0) return undefined;
|
|
79
|
+
const split = names.map((n) => n.split("."));
|
|
80
|
+
const out: string[] = [];
|
|
81
|
+
for (let i = 0; i < split[0].length - 1; i++) {
|
|
82
|
+
if (split.every((s) => s.length - 1 > i && s[i] === split[0][i])) out.push(split[0][i]);
|
|
83
|
+
else break;
|
|
84
|
+
}
|
|
85
|
+
if (out.length === 0 && names.length === 1 && split[0].length > 1) return split[0].slice(0, -1).join(".");
|
|
86
|
+
return out.length > 0 ? out.join(".") : undefined;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Every collector config in the build: rebuilt from its otel entities, and found in its output. */
|
|
90
|
+
export function buildCollectorConfigs(ctx: PostSynthContext): CollectorConfig[] {
|
|
91
|
+
const configs = collectorConfigs(ctx).map((f) => f.config);
|
|
92
|
+
const otel = [...(ctx.entities?.values() ?? [])].filter((e) => e?.lexicon === "otel");
|
|
93
|
+
if (otel.some(isOTelComponent)) configs.push(buildCollectorConfig(otel).config);
|
|
94
|
+
return configs;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** The metrics the collector configs emit; undefined when there are none. */
|
|
98
|
+
export function collectorMetrics(configs: CollectorConfig[]): CollectorMetrics | undefined {
|
|
99
|
+
if (configs.length === 0) return undefined;
|
|
100
|
+
const namespaces = new Set<string>();
|
|
101
|
+
const metrics = new Map<string, EmittedMetric[]>();
|
|
102
|
+
|
|
103
|
+
for (const config of configs) {
|
|
104
|
+
const exporters = Object.entries(asObj(config.exporters)).filter(([id]) => typeOf("exporter", id) === "prometheus" || typeOf("exporter", id) === "prometheusremotewrite");
|
|
105
|
+
const namings: Array<{ naming: PrometheusNaming; openLabels: boolean }> =
|
|
106
|
+
exporters.length > 0
|
|
107
|
+
? exporters.map(([, c]) => {
|
|
108
|
+
const e = asObj(c);
|
|
109
|
+
return {
|
|
110
|
+
naming: { ...(typeof e.namespace === "string" ? { namespace: e.namespace } : {}), ...(e.add_metric_suffixes === false ? { add_metric_suffixes: false } : {}) },
|
|
111
|
+
openLabels: asObj(e.resource_to_telemetry_conversion).enabled === true,
|
|
112
|
+
};
|
|
113
|
+
})
|
|
114
|
+
: [{ naming: {}, openLabels: false }];
|
|
115
|
+
|
|
116
|
+
for (const { naming, openLabels } of namings) {
|
|
117
|
+
const prefix = (ns: string) => `${naming.namespace ? `${sanitize(naming.namespace)}_` : ""}${sanitize(ns)}_`;
|
|
118
|
+
// The two connectors' defaults are theirs in every build that has a collector config.
|
|
119
|
+
namespaces.add(prefix(SPANMETRICS_DEFAULT_NAMESPACE));
|
|
120
|
+
namespaces.add(prefix(SERVICEGRAPH_NAMESPACE));
|
|
121
|
+
|
|
122
|
+
const add = (connector: string, m: CollectorMetric, extra: string[] = []) => {
|
|
123
|
+
const labels = openLabels ? undefined : new Set([...m.dimensions.map(prometheusLabel), ...extra.map(prometheusLabel), ...ADDED_LABELS]);
|
|
124
|
+
// A classic histogram is served as three series whatever add_metric_suffixes says.
|
|
125
|
+
const series: Array<[string, Set<string> | undefined]> =
|
|
126
|
+
m.type === "histogram"
|
|
127
|
+
? [
|
|
128
|
+
[`${m.prometheus}_bucket`, labels ? new Set([...labels, "le"]) : undefined],
|
|
129
|
+
[`${m.prometheus}_sum`, labels],
|
|
130
|
+
[`${m.prometheus}_count`, labels],
|
|
131
|
+
]
|
|
132
|
+
: [[m.prometheus, labels]];
|
|
133
|
+
for (const [name, l] of series) {
|
|
134
|
+
metrics.set(name, [...(metrics.get(name) ?? []), { connector, ...(l ? { labels: l } : {}) }]);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
for (const [id, raw] of Object.entries(asObj(config.connectors))) {
|
|
139
|
+
const c = asObj(raw);
|
|
140
|
+
switch (typeOf("connector", id)) {
|
|
141
|
+
case "spanmetrics": {
|
|
142
|
+
const n = spanMetricsNames(c as SpanMetricsNamingConfig, naming);
|
|
143
|
+
if (n.namespace !== "") namespaces.add(prefix(n.namespace));
|
|
144
|
+
for (const m of [n.calls, n.duration, n.events]) if (m) add(id, m);
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
case "servicegraph": {
|
|
148
|
+
const n = serviceGraphNames(c, naming);
|
|
149
|
+
for (const m of [n.requests, n.failed, n.serverDuration, n.clientDuration]) add(id, m);
|
|
150
|
+
break;
|
|
151
|
+
}
|
|
152
|
+
case "sum": {
|
|
153
|
+
const names: string[] = [];
|
|
154
|
+
for (const signal of ["spans", "spanevents", "metrics", "datapoints", "logs"]) {
|
|
155
|
+
for (const [name, def] of Object.entries(asObj(c[signal]))) {
|
|
156
|
+
const attrs = (Array.isArray(asObj(def).attributes) ? (asObj(def).attributes as unknown[]) : []).map((a) => String(asObj(a).key ?? ""));
|
|
157
|
+
names.push(name);
|
|
158
|
+
add(id, { name, prometheus: prometheusMetricName(name, "sum", undefined, naming), type: "sum", dimensions: attrs.filter(Boolean) });
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
const ns = commonPrefix(names);
|
|
162
|
+
if (ns) namespaces.add(prefix(ns));
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
case "signaltometrics": {
|
|
166
|
+
const names: string[] = [];
|
|
167
|
+
for (const signal of ["spans", "datapoints", "logs", "profiles"]) {
|
|
168
|
+
for (const def of Array.isArray(c[signal]) ? (c[signal] as unknown[]) : []) {
|
|
169
|
+
const m = asObj(def);
|
|
170
|
+
if (typeof m.name !== "string") continue;
|
|
171
|
+
const type = m.histogram !== undefined || m.exponential_histogram !== undefined ? "histogram" : m.sum !== undefined ? "sum" : undefined;
|
|
172
|
+
if (!type) continue;
|
|
173
|
+
const unit = typeof m.unit === "string" ? m.unit : undefined;
|
|
174
|
+
const attrs = [
|
|
175
|
+
...(Array.isArray(m.attributes) ? (m.attributes as unknown[]) : []),
|
|
176
|
+
...(Array.isArray(m.include_resource_attributes) ? (m.include_resource_attributes as unknown[]) : []),
|
|
177
|
+
].map((a) => String(asObj(a).key ?? ""));
|
|
178
|
+
names.push(m.name);
|
|
179
|
+
add(id, { name: m.name, prometheus: prometheusMetricName(m.name, type, unit, naming), type, ...(unit ? { unit } : {}), dimensions: attrs.filter(Boolean) });
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const ns = commonPrefix(names);
|
|
183
|
+
if (ns) namespaces.add(prefix(ns));
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
return { namespaces: [...namespaces], metrics };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// ── PromQL ──────────────────────────────────────────────────────────
|
|
194
|
+
|
|
195
|
+
/** Functions that write a label of their own, so an aggregation over them can group by it. */
|
|
196
|
+
const RELABEL = new Set(["label_replace", "label_join"]);
|
|
197
|
+
|
|
198
|
+
/** Every `by (...)` aggregation in the expression: its labels and the text it aggregates. */
|
|
199
|
+
function byAggregations(top: SyntaxNode, src: string): Array<{ labels: string[]; body: string }> {
|
|
200
|
+
const out: Array<{ labels: string[]; body: string }> = [];
|
|
201
|
+
const walk = (n: SyntaxNode) => {
|
|
202
|
+
if (n.name === "AggregateExpr") {
|
|
203
|
+
const mod = n.getChild("AggregateModifier");
|
|
204
|
+
const body = n.getChild("FunctionCallBody");
|
|
205
|
+
if (mod?.getChild("By") && body) {
|
|
206
|
+
const labels = (mod.getChild("GroupingLabels")?.getChildren("LabelName") ?? []).map((l) => src.slice(l.from, l.to));
|
|
207
|
+
out.push({ labels, body: src.slice(body.from + 1, body.to - 1) });
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
for (let c = n.firstChild; c; c = c.nextSibling) walk(c);
|
|
211
|
+
};
|
|
212
|
+
walk(top);
|
|
213
|
+
return out;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface CollectorMetricIssue {
|
|
217
|
+
kind: "name" | "label";
|
|
218
|
+
metric: string;
|
|
219
|
+
label?: string;
|
|
220
|
+
message: string;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* What a PromQL expression reads under the build's connector namespaces and
|
|
225
|
+
* the build does not emit: a metric name no connector writes, and a
|
|
226
|
+
* `by (...)` label none of the aggregated metrics carries. A name outside
|
|
227
|
+
* every namespace is not read. An aggregation is checked only when every
|
|
228
|
+
* series under it is an emitted metric and nothing under it adds a label
|
|
229
|
+
* (`label_replace`, `label_join`). Selectors come from the lexicon's
|
|
230
|
+
* `promql-analysis.ts`.
|
|
231
|
+
*/
|
|
232
|
+
export function collectorMetricIssues(expr: string, emitted: CollectorMetrics): CollectorMetricIssue[] {
|
|
233
|
+
const top = parsePromql(expr);
|
|
234
|
+
if (!top) return [];
|
|
235
|
+
const issues: CollectorMetricIssue[] = [];
|
|
236
|
+
const owned = (name: string) => emitted.namespaces.some((p) => name.startsWith(p));
|
|
237
|
+
|
|
238
|
+
const seen = new Set<string>();
|
|
239
|
+
for (const { name } of selectors(expr)) {
|
|
240
|
+
if (!name || seen.has(name) || !owned(name) || emitted.metrics.has(name)) continue;
|
|
241
|
+
seen.add(name);
|
|
242
|
+
const p = emitted.namespaces.filter((x) => name.startsWith(x)).sort((a, b) => b.length - a.length)[0];
|
|
243
|
+
issues.push({ kind: "name", metric: name, message: `reads ${name}, which no collector config in the build emits under ${p.slice(0, -1)}` });
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const reported = new Set<string>();
|
|
247
|
+
for (const { labels, body } of byAggregations(top, expr)) {
|
|
248
|
+
const read = selectors(body);
|
|
249
|
+
if (read.length === 0 || read.some((s) => s.within.some((fn) => RELABEL.has(fn)))) continue;
|
|
250
|
+
const metrics = read.map((s) => (s.name ? emitted.metrics.get(s.name) : undefined));
|
|
251
|
+
if (!metrics.every((m) => m !== undefined && m.every((e) => e.labels))) continue;
|
|
252
|
+
const carried = new Set(metrics.flatMap((m) => m!.flatMap((e) => [...e.labels!])));
|
|
253
|
+
const shown = [...new Set(read.map((s) => s.name!))].join(", ");
|
|
254
|
+
for (const label of labels) {
|
|
255
|
+
const key = `${label} ${shown}`;
|
|
256
|
+
if (carried.has(label) || reported.has(key)) continue;
|
|
257
|
+
reported.add(key);
|
|
258
|
+
issues.push({ kind: "label", metric: shown, label, message: `groups ${shown} by ${label}, which is not a dimension the collector config declares for it` });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return issues;
|
|
262
|
+
}
|
|
@@ -184,12 +184,9 @@ describe("the rules an Slo builds", () => {
|
|
|
184
184
|
expect(sloMetrics(direct).burnRates).toEqual([]);
|
|
185
185
|
});
|
|
186
186
|
|
|
187
|
-
test("the same Slo renders inside the rule file promtool accepts", () => {
|
|
187
|
+
test.skipIf(!hasPromtool)("the same Slo renders inside the rule file promtool accepts", () => {
|
|
188
188
|
const r = promtoolCheckRules(ruleFileYaml([slo.rules]), PROMTOOL);
|
|
189
|
-
|
|
190
|
-
expect(r.ran).toBe(false);
|
|
191
|
-
return;
|
|
192
|
-
}
|
|
189
|
+
expect(r.ran).toBe(true);
|
|
193
190
|
expect(r.output).toContain("SUCCESS");
|
|
194
191
|
expect(r.ok).toBe(true);
|
|
195
192
|
});
|
|
@@ -50,12 +50,16 @@ route:
|
|
|
50
50
|
receivers:
|
|
51
51
|
- name: chat
|
|
52
52
|
slack_configs:
|
|
53
|
-
-
|
|
53
|
+
- api_url: https://slack.com/api/chat.postMessage
|
|
54
|
+
channel: '#alerts'
|
|
54
55
|
message_text: '{{ .CommonLabels.alertname }}'
|
|
55
56
|
callback_id: alerts
|
|
56
57
|
image_url: https://example.com/graph.png
|
|
57
58
|
thumb_url: https://example.com/thumb.png
|
|
58
59
|
update_message: true
|
|
60
|
+
http_config:
|
|
61
|
+
authorization:
|
|
62
|
+
credentials_file: /etc/alertmanager/secrets/slack-bot-token
|
|
59
63
|
timeout: 10s
|
|
60
64
|
fields:
|
|
61
65
|
- title: Severity
|
package/src/index.ts
CHANGED
|
@@ -66,6 +66,7 @@ export { parseMatchers, matcherMatches, matcher, type Matcher, type MatchOp, typ
|
|
|
66
66
|
export { checkPromql, PROMQL_GRAMMAR, type PromqlCheck } from "./promql";
|
|
67
67
|
export {
|
|
68
68
|
validateRuleFile,
|
|
69
|
+
validateRunbookUrls,
|
|
69
70
|
validateAlertmanagerConfig,
|
|
70
71
|
validateSeverityRouting,
|
|
71
72
|
alertSeverities,
|
|
@@ -24,7 +24,9 @@ describe("init templates", () => {
|
|
|
24
24
|
try {
|
|
25
25
|
expect(result.errors).toEqual([]);
|
|
26
26
|
expect(result.outputs.get("prometheus")).toBeTruthy();
|
|
27
|
-
|
|
27
|
+
// As `chant build` reports them: through the default (`recommended`) preset.
|
|
28
|
+
const recommended = new Set(prometheusPlugin.lintPresets!().recommended);
|
|
29
|
+
expect(runPostSynthChecks(postSynthChecks, result).filter((d) => recommended.has(d.checkId))).toEqual([]);
|
|
28
30
|
const lint = await lintCommand({ path: join(dir, "src"), format: "stylish", fix: false });
|
|
29
31
|
expect(lint.errorCount + lint.warningCount, lint.output).toBe(0);
|
|
30
32
|
} finally {
|
|
@@ -128,7 +128,109 @@ export const prometheusAuditCatalog: Record<string, RuleMeta> = {
|
|
|
128
128
|
"Integration or global setting Alertmanager rejects",
|
|
129
129
|
"Set one of each value and its *_file, and use a value Alertmanager allows (e.g. message_type text or markdown, parse_mode Markdown, MarkdownV2 or HTML).",
|
|
130
130
|
),
|
|
131
|
+
PROM211: outputRule(
|
|
132
|
+
"PROM211",
|
|
133
|
+
"report-only",
|
|
134
|
+
"best-practice",
|
|
135
|
+
"Alerting rule has no for, or for: 0s",
|
|
136
|
+
"Set for to how long the condition must hold before the alert fires, e.g. 5m.",
|
|
137
|
+
),
|
|
138
|
+
PROM212: outputRule(
|
|
139
|
+
"PROM212",
|
|
140
|
+
"report-only",
|
|
141
|
+
"best-practice",
|
|
142
|
+
"Alerting rule has no runbook_url annotation (opt-in)",
|
|
143
|
+
"Add a runbook_url annotation linking to what the responder should do.",
|
|
144
|
+
),
|
|
145
|
+
PROM213: outputRule(
|
|
146
|
+
"PROM213",
|
|
147
|
+
"merge-worthy",
|
|
148
|
+
"correctness",
|
|
149
|
+
"Alert expression has no comparison",
|
|
150
|
+
"Add the condition the alert fires on, e.g. > 0.05, or use absent() for a missing series.",
|
|
151
|
+
),
|
|
152
|
+
PROM214: outputRule(
|
|
153
|
+
"PROM214",
|
|
154
|
+
"merge-worthy",
|
|
155
|
+
"correctness",
|
|
156
|
+
"Alert template reads a label the expression aggregates away",
|
|
157
|
+
"Keep the label in the aggregation's by (...), or stop reading it in the template.",
|
|
158
|
+
),
|
|
159
|
+
PROM215: outputRule(
|
|
160
|
+
"PROM215",
|
|
161
|
+
"merge-worthy",
|
|
162
|
+
"correctness",
|
|
163
|
+
"rate, irate or increase over a name that is not a counter's",
|
|
164
|
+
"Read a counter (a name ending in _total, _count, _sum or _bucket), or use delta() or deriv() for a gauge.",
|
|
165
|
+
),
|
|
166
|
+
PROM216: outputRule(
|
|
167
|
+
"PROM216",
|
|
168
|
+
"merge-worthy",
|
|
169
|
+
"correctness",
|
|
170
|
+
"histogram_quantile over a series without _bucket, or without le",
|
|
171
|
+
"Pass the histogram's _bucket series, and keep le in the aggregation: sum by (le, ...) (rate(x_bucket[5m])).",
|
|
172
|
+
),
|
|
173
|
+
PROM217: outputRule(
|
|
174
|
+
"PROM217",
|
|
175
|
+
"report-only",
|
|
176
|
+
"best-practice",
|
|
177
|
+
"Recording rule name is not level:metric:operations",
|
|
178
|
+
"Name the recorded series level:metric:operations, e.g. job:http_requests:rate5m.",
|
|
179
|
+
),
|
|
180
|
+
PROM218: outputRule(
|
|
181
|
+
"PROM218",
|
|
182
|
+
"report-only",
|
|
183
|
+
"best-practice",
|
|
184
|
+
"Regex matcher needs no regex, or is anchored",
|
|
185
|
+
'Use = or != for a plain value, and drop ^ and $ from a regex, which Prometheus anchors already (job=~"api|web").',
|
|
186
|
+
),
|
|
187
|
+
PROM219: outputRule("PROM219", "merge-worthy", "correctness", "Alerting rule sets the alertname label", "Remove the alertname label; rename the rule instead."),
|
|
188
|
+
PROM220: outputRule(
|
|
189
|
+
"PROM220",
|
|
190
|
+
"merge-worthy",
|
|
191
|
+
"security",
|
|
192
|
+
"Receiver turns off TLS certificate verification",
|
|
193
|
+
"Remove insecure_skip_verify, and set ca_file to the CA that signed the endpoint's certificate.",
|
|
194
|
+
),
|
|
195
|
+
PROM221: outputRule(
|
|
196
|
+
"PROM221",
|
|
197
|
+
"merge-worthy",
|
|
198
|
+
"security",
|
|
199
|
+
"SMTP credentials sent with require_tls false",
|
|
200
|
+
"Set require_tls (or global.smtp_require_tls) to true, or use force_implicit_tls with an SMTPS port.",
|
|
201
|
+
),
|
|
202
|
+
PROM222: outputRule(
|
|
203
|
+
"PROM222",
|
|
204
|
+
"merge-worthy",
|
|
205
|
+
"security",
|
|
206
|
+
"Credentials sent to an http:// receiver URL",
|
|
207
|
+
"Point the integration at an https:// URL.",
|
|
208
|
+
),
|
|
209
|
+
PROM223: outputRule(
|
|
210
|
+
"PROM223",
|
|
211
|
+
"merge-worthy",
|
|
212
|
+
"correctness",
|
|
213
|
+
"repeat_interval is shorter than group_interval",
|
|
214
|
+
"Set repeat_interval to a multiple of group_interval, or lower group_interval.",
|
|
215
|
+
),
|
|
216
|
+
PROM224: outputRule(
|
|
217
|
+
"PROM224",
|
|
218
|
+
"merge-worthy",
|
|
219
|
+
"correctness",
|
|
220
|
+
"Inhibit rule matches one alert as source and target, with no equal",
|
|
221
|
+
"List the labels source and target must share under equal (e.g. alertname, cluster), or make the matchers exclusive.",
|
|
222
|
+
),
|
|
223
|
+
PROM301: outputRule(
|
|
224
|
+
"PROM301",
|
|
225
|
+
"merge-worthy",
|
|
226
|
+
"correctness",
|
|
227
|
+
"Rule reads a connector metric no collector in the build emits",
|
|
228
|
+
"Read the name spanMetricsNames(), serviceGraphNames() or genAiMetrics() gives for the declared connector, and group by its declared dimensions, or add the dimension to the connector.",
|
|
229
|
+
),
|
|
131
230
|
};
|
|
132
231
|
|
|
232
|
+
/** Post-synth checks the `recommended` lint preset leaves out (#3363); see `prometheusPlugin.lintPresets()`. */
|
|
233
|
+
export const OPT_IN_CHECKS: ReadonlySet<string> = new Set(["PROM212"]);
|
|
234
|
+
|
|
133
235
|
// Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
|
|
134
236
|
applyLineage(prometheusAuditCatalog, prometheusAuditLineage);
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { prometheusAuditCatalog } from "./audit-catalog";
|
|
3
|
+
|
|
4
|
+
describe("prometheus audit lineage (#2916, #3136)", () => {
|
|
5
|
+
const credits = (id: string) => (prometheusAuditCatalog[id].lineage ?? []).map((l) => `${l.tool}:${l.rule ?? ""}:${l.relation}`);
|
|
6
|
+
|
|
7
|
+
test("credits promtool where rulefmt rejects the same file", () => {
|
|
8
|
+
for (const id of ["PROM101", "PROM103", "PROM104", "PROM105"]) {
|
|
9
|
+
expect(credits(id), id).toContain("promtool::equivalent");
|
|
10
|
+
}
|
|
11
|
+
expect(credits("PROM002")).toContain("promtool::equivalent");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("credits pint's named checks", () => {
|
|
15
|
+
expect(credits("PROM002")).toEqual(["promtool::equivalent", "pint:promql/syntax:equivalent"]);
|
|
16
|
+
expect(credits("PROM102")).toEqual(["pint:rule/duplicate:overlaps"]);
|
|
17
|
+
expect(credits("PROM103")).toEqual(["promtool::equivalent", "pint:alerts/for:overlaps"]);
|
|
18
|
+
expect(credits("PROM104")).toEqual(["promtool::equivalent", "pint:promql/syntax:equivalent"]);
|
|
19
|
+
expect(credits("PROM106")).toEqual(["pint:rule/label:overlaps"]);
|
|
20
|
+
expect(credits("PROM107")).toEqual(["pint:alerts/annotation:overlaps"]);
|
|
21
|
+
expect(credits("PROM211")).toEqual(["pint:rule/for:overlaps"]);
|
|
22
|
+
expect(credits("PROM212")).toEqual(["pint:alerts/annotation:overlaps"]);
|
|
23
|
+
expect(credits("PROM213")).toEqual(["pint:alerts/comparison:equivalent"]);
|
|
24
|
+
expect(credits("PROM214")).toEqual(["pint:alerts/template:overlaps"]);
|
|
25
|
+
expect(credits("PROM215")).toEqual(["pint:promql/rate:overlaps"]);
|
|
26
|
+
expect(credits("PROM217")).toEqual(["pint:rule/name:overlaps"]);
|
|
27
|
+
expect(credits("PROM218")).toEqual(["pint:promql/regexp:equivalent"]);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("credits amtool check-config where Alertmanager rejects the same config", () => {
|
|
31
|
+
for (const id of ["PROM201", "PROM203", "PROM204", "PROM205", "PROM206", "PROM208"]) {
|
|
32
|
+
expect(credits(id), id).toEqual(["amtool::equivalent"]);
|
|
33
|
+
}
|
|
34
|
+
expect(credits("PROM209")).toEqual(["amtool::overlaps"]);
|
|
35
|
+
expect(credits("PROM210")).toEqual(["amtool::overlaps"]);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("rules no upstream tool checks carry no credit", () => {
|
|
39
|
+
for (const id of ["PROM001", "PROM003", "PROM202", "PROM207", "PROM216", "PROM219", "PROM220", "PROM221", "PROM222", "PROM223", "PROM224", "PROM301"]) {
|
|
40
|
+
expect(prometheusAuditCatalog[id].lineage, id).toBeUndefined();
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("every rule is either credited above or deliberately left uncredited", () => {
|
|
45
|
+
const credited = Object.keys(prometheusAuditCatalog).filter((id) => prometheusAuditCatalog[id].lineage !== undefined);
|
|
46
|
+
expect(credited.sort()).toEqual(
|
|
47
|
+
[
|
|
48
|
+
"PROM002",
|
|
49
|
+
"PROM101",
|
|
50
|
+
"PROM102",
|
|
51
|
+
"PROM103",
|
|
52
|
+
"PROM104",
|
|
53
|
+
"PROM105",
|
|
54
|
+
"PROM106",
|
|
55
|
+
"PROM107",
|
|
56
|
+
"PROM201",
|
|
57
|
+
"PROM203",
|
|
58
|
+
"PROM204",
|
|
59
|
+
"PROM205",
|
|
60
|
+
"PROM206",
|
|
61
|
+
"PROM208",
|
|
62
|
+
"PROM209",
|
|
63
|
+
"PROM210",
|
|
64
|
+
"PROM211",
|
|
65
|
+
"PROM212",
|
|
66
|
+
"PROM213",
|
|
67
|
+
"PROM214",
|
|
68
|
+
"PROM215",
|
|
69
|
+
"PROM217",
|
|
70
|
+
"PROM218",
|
|
71
|
+
].sort(),
|
|
72
|
+
);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
@@ -21,12 +21,33 @@
|
|
|
21
21
|
* configurable "this label or annotation must exist" checks that pint ships
|
|
22
22
|
* with nothing enabled, so they overlap PROM106 and PROM107 and no more.
|
|
23
23
|
*
|
|
24
|
+
* The PROM211-PROM219 rule-file checks (#3363), against the same list:
|
|
25
|
+
* - alerts/comparison and promql/regexp are the same checks as PROM213 and
|
|
26
|
+
* PROM218, so those are equivalent.
|
|
27
|
+
* - rule/for enforces a configured minimum `for`, and alerts/annotation a
|
|
28
|
+
* configured annotation; with `for` required and runbook_url named they
|
|
29
|
+
* are PROM211 and PROM212. alerts/for is not credited: it rejects bad
|
|
30
|
+
* values and a redundant `for: 0s`, not a missing `for`.
|
|
31
|
+
* - alerts/template covers PROM214's dropped label and checks template
|
|
32
|
+
* syntax besides. promql/rate reads the metric type from a live
|
|
33
|
+
* Prometheus's metadata; PROM215 has only the name to go on. rule/name
|
|
34
|
+
* is a configured name regex; PROM217 fixes it to level:metric:operations.
|
|
35
|
+
* All three overlap.
|
|
36
|
+
*
|
|
24
37
|
* Deliberately without a credit from these tools:
|
|
25
38
|
* - PROM001 and PROM003: a literal credential in TypeScript source, and the
|
|
26
39
|
* Slo objective/window model. Neither tool reads either.
|
|
27
40
|
* - PROM202 (severity not routed) and PROM207 (receiver never routed to):
|
|
28
41
|
* check-config accepts both, and `amtool config routes test` answers a
|
|
29
42
|
* different question (where one given label set goes).
|
|
43
|
+
* - PROM216 (histogram_quantile without _bucket or le) and PROM219
|
|
44
|
+
* (alertname set by hand): no pint check reports either, and promtool's
|
|
45
|
+
* rulefmt accepts both.
|
|
46
|
+
* - PROM220-PROM224: check-config accepts insecure_skip_verify, SMTP auth
|
|
47
|
+
* without TLS, credentials over http://, a repeat_interval under
|
|
48
|
+
* group_interval, and an inhibit rule without equal.
|
|
49
|
+
* - PROM301: no tool here reads a rule file together with the collector
|
|
50
|
+
* config that produces its metrics.
|
|
30
51
|
*/
|
|
31
52
|
import type { Lineage } from "@intentius/chant/audit/catalog";
|
|
32
53
|
|
|
@@ -66,4 +87,11 @@ export const prometheusAuditLineage: Record<string, Lineage[]> = {
|
|
|
66
87
|
PROM208: [amtool("equivalent")],
|
|
67
88
|
PROM209: [amtool("overlaps")],
|
|
68
89
|
PROM210: [amtool("overlaps")],
|
|
90
|
+
PROM211: [{ tool: "pint", rule: "rule/for", url: `${PINT}/rule/for.md`, relation: "overlaps" }],
|
|
91
|
+
PROM212: [{ tool: "pint", rule: "alerts/annotation", url: `${PINT}/alerts/annotation.md`, relation: "overlaps" }],
|
|
92
|
+
PROM213: [{ tool: "pint", rule: "alerts/comparison", url: `${PINT}/alerts/comparison.md`, relation: "equivalent" }],
|
|
93
|
+
PROM214: [{ tool: "pint", rule: "alerts/template", url: `${PINT}/alerts/template.md`, relation: "overlaps" }],
|
|
94
|
+
PROM215: [{ tool: "pint", rule: "promql/rate", url: `${PINT}/promql/rate.md`, relation: "overlaps" }],
|
|
95
|
+
PROM217: [{ tool: "pint", rule: "rule/name", url: `${PINT}/rule/name.md`, relation: "overlaps" }],
|
|
96
|
+
PROM218: [{ tool: "pint", rule: "promql/regexp", url: `${PINT}/promql/regexp.md`, relation: "equivalent" }],
|
|
69
97
|
};
|