@intentius/chant-lexicon-prometheus 0.101.0 → 0.104.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 +3 -1
- package/dist/alertmanager.d.ts.map +1 -1
- package/dist/collector-metrics.d.ts +58 -0
- package/dist/collector-metrics.d.ts.map +1 -0
- package/dist/composites/genai.d.ts +1 -1
- package/dist/composites/genai.d.ts.map +1 -1
- package/dist/composites/slo.d.ts +1 -1
- package/dist/composites/slo.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +12 -3
- package/dist/import/embedded.d.ts.map +1 -1
- package/dist/import/generator.d.ts +14 -0
- package/dist/import/generator.d.ts.map +1 -1
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/init-templates.d.ts.map +1 -1
- package/dist/integrations.d.ts.map +1 -1
- package/dist/integrity.json +23 -7
- 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 +54 -0
- package/dist/lint/audit-lineage.d.ts.map +1 -0
- 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/prom208.d.ts +1 -1
- package/dist/lint/post-synth/prom209.d.ts +1 -1
- package/dist/lint/post-synth/prom210.d.ts +8 -0
- package/dist/lint/post-synth/prom210.d.ts.map +1 -0
- 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 +16 -0
- package/dist/okf/rules/PROM210.md +11 -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/rule-eval.d.ts.map +1 -1
- package/dist/rules/literal-credential.ts +4 -0
- package/dist/rules/prom-helpers.ts +41 -2
- package/dist/rules/prom208.ts +1 -1
- package/dist/rules/prom209.ts +1 -1
- package/dist/rules/prom210.ts +17 -0
- 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/skill-defs.d.ts +1 -1
- package/dist/skill-defs.d.ts.map +1 -1
- package/dist/skills/chant-prometheus-alertmanager.md +3 -1
- package/dist/validate-config.d.ts +9 -3
- package/dist/validate-config.d.ts.map +1 -1
- package/dist/validate-integrations.d.ts +38 -0
- package/dist/validate-integrations.d.ts.map +1 -0
- package/dist/validate-security.d.ts +13 -0
- package/dist/validate-security.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/collector-metrics.ts +262 -0
- package/src/composites/composites.test.ts +2 -5
- package/src/import/embedded.test.ts +52 -3
- package/src/import/embedded.ts +37 -5
- package/src/import/generator.ts +19 -1
- 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 +115 -2
- package/src/lint/audit-lineage.test.ts +74 -0
- package/src/lint/audit-lineage.ts +97 -0
- package/src/lint/post-synth/checks.test.ts +257 -0
- package/src/lint/post-synth/index.ts +32 -0
- package/src/lint/post-synth/post-synth.test.ts +37 -1
- package/src/lint/post-synth/prom-helpers.ts +41 -2
- package/src/lint/post-synth/prom208.ts +1 -1
- package/src/lint/post-synth/prom209.ts +1 -1
- package/src/lint/post-synth/prom210.ts +17 -0
- 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 +23 -0
- package/src/plugin.ts +14 -3
- package/src/promql-analysis.ts +365 -0
- package/src/skills/chant-prometheus-alertmanager.md +3 -1
- package/src/testdata/integration-cases.ts +96 -0
- package/src/tools.test.ts +6 -0
- package/src/validate-config.ts +279 -33
- package/src/validate-integrations.ts +389 -0
- package/src/validate-security.ts +143 -0
|
@@ -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
|
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { describe, expect, test } from "vitest";
|
|
2
2
|
import { embeddedDocument, type EmbeddedContent } from "@intentius/chant/import/embedded";
|
|
3
3
|
import { prometheusPlugin } from "../plugin";
|
|
4
|
-
import { ruleGroupsImporter } from "./embedded";
|
|
4
|
+
import { alertmanagerImporter, ruleGroupsImporter } from "./embedded";
|
|
5
5
|
|
|
6
6
|
const GROUPS = [
|
|
7
7
|
{ name: "api", rules: [{ alert: "ApiDown", expr: "up{job=\"api\"} == 0", for: "5m" }] },
|
|
@@ -25,8 +25,8 @@ const site = (over: Partial<EmbeddedContent>): EmbeddedContent => ({
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
describe("rule groups embedded in another lexicon's resource (#2962)", () => {
|
|
28
|
-
test("the plugin registers the
|
|
29
|
-
expect(prometheusPlugin.embeddedImporters?.()).toEqual([ruleGroupsImporter]);
|
|
28
|
+
test("the plugin registers the importers", () => {
|
|
29
|
+
expect(prometheusPlugin.embeddedImporters?.()).toEqual([ruleGroupsImporter, alertmanagerImporter]);
|
|
30
30
|
});
|
|
31
31
|
|
|
32
32
|
test("matches a PrometheusRule's groups and a rule file held as text; not an alertmanager.yml", () => {
|
|
@@ -54,3 +54,52 @@ describe("rule groups embedded in another lexicon's resource (#2962)", () => {
|
|
|
54
54
|
expect(out.value.bindings).toEqual([{ from: "rules.ts", name: "api" }]);
|
|
55
55
|
});
|
|
56
56
|
});
|
|
57
|
+
|
|
58
|
+
const ALERTMANAGER = `route:
|
|
59
|
+
receiver: team
|
|
60
|
+
routes:
|
|
61
|
+
- matchers: [severity="critical"]
|
|
62
|
+
receiver: pager
|
|
63
|
+
mute_time_intervals: [nights]
|
|
64
|
+
receivers:
|
|
65
|
+
- name: team
|
|
66
|
+
- name: pager
|
|
67
|
+
time_intervals:
|
|
68
|
+
- name: nights
|
|
69
|
+
time_intervals:
|
|
70
|
+
- times: [{ start_time: "22:00", end_time: "24:00" }]
|
|
71
|
+
inhibit_rules:
|
|
72
|
+
- source_matchers: [severity="critical"]
|
|
73
|
+
target_matchers: [severity="warning"]
|
|
74
|
+
templates: [/etc/alertmanager/*.tmpl]
|
|
75
|
+
`;
|
|
76
|
+
|
|
77
|
+
describe("an alertmanager.yml embedded in another lexicon's resource (#3031)", () => {
|
|
78
|
+
const configMap = (text: string) =>
|
|
79
|
+
site({ hostType: "K8s::Core::ConfigMap", location: 'ConfigMap am data["alertmanager.yml"]', text, document: embeddedDocument(text) });
|
|
80
|
+
|
|
81
|
+
test("matches an alertmanager.yml held as text; not a rule file, nor a selected member", () => {
|
|
82
|
+
expect(alertmanagerImporter.matches(configMap(ALERTMANAGER))).toBe(true);
|
|
83
|
+
expect(alertmanagerImporter.matches(configMap(RULE_FILE))).toBe(false);
|
|
84
|
+
expect(alertmanagerImporter.matches(site({ document: { groups: GROUPS }, select: "groups" }))).toBe(false);
|
|
85
|
+
expect(ruleGroupsImporter.matches(configMap(ALERTMANAGER))).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("becomes alertmanagerYaml over every declaration the standalone import writes", () => {
|
|
89
|
+
const out = alertmanagerImporter.import(configMap(ALERTMANAGER));
|
|
90
|
+
expect(out.files.map((f) => f.path)).toEqual(["receivers.ts", "time-intervals.ts", "routes.ts", "inhibit-rules.ts", "settings.ts"]);
|
|
91
|
+
expect(out.value).toEqual({
|
|
92
|
+
bindings: [
|
|
93
|
+
{ from: "receivers.ts", name: "team" },
|
|
94
|
+
{ from: "receivers.ts", name: "pager" },
|
|
95
|
+
{ from: "time-intervals.ts", name: "nights" },
|
|
96
|
+
{ from: "routes.ts", name: "root" },
|
|
97
|
+
{ from: "inhibit-rules.ts", name: "inhibitRule1" },
|
|
98
|
+
{ from: "settings.ts", name: "settings" },
|
|
99
|
+
],
|
|
100
|
+
shape: "list",
|
|
101
|
+
through: { from: "@intentius/chant-lexicon-prometheus", name: "alertmanagerYaml" },
|
|
102
|
+
});
|
|
103
|
+
expect(out.warnings).toEqual([]);
|
|
104
|
+
});
|
|
105
|
+
});
|
package/src/import/embedded.ts
CHANGED
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* text in a ConfigMap.
|
|
2
|
+
* Prometheus and Alertmanager content embedded in another lexicon's
|
|
3
|
+
* resource, for `chant import`: a k8s `PrometheusRule`'s `spec.groups`, or a
|
|
4
|
+
* rule file held as text in a ConfigMap (#2962), and an `alertmanager.yml`
|
|
5
|
+
* held as text in a ConfigMap (#3031).
|
|
5
6
|
*
|
|
6
7
|
* The groups are imported exactly as `chant import rules.yml` would import
|
|
7
8
|
* them, into a directory of its own. `spec.groups` becomes the list of the
|
|
8
9
|
* declared groups (an `Slo`'s by its `rules` member), which the k8s
|
|
9
10
|
* serializer renders as the same groups; a rule file becomes
|
|
10
11
|
* `ruleFileYaml([...])`, the text the prometheus serializer writes.
|
|
12
|
+
*
|
|
13
|
+
* An `alertmanager.yml` is imported exactly as `chant import alertmanager.yml`
|
|
14
|
+
* would import it, and becomes `alertmanagerYaml([...])` over every receiver,
|
|
15
|
+
* time interval, root route, inhibit rule and settings it declares. The
|
|
16
|
+
* ConfigMap then holds the config as the serializer writes it: the same
|
|
17
|
+
* config, with Alertmanager's deprecated spellings rewritten as the
|
|
18
|
+
* standalone import rewrites them.
|
|
11
19
|
*/
|
|
12
20
|
|
|
13
21
|
import type { EmbeddedContentImporter, EmbeddedImport } from "@intentius/chant/import/embedded";
|
|
14
|
-
import { looksLikeRuleFile } from "../model";
|
|
22
|
+
import { looksLikeAlertmanagerConfig, looksLikeRuleFile } from "../model";
|
|
15
23
|
import { parsePrometheusYaml } from "./parser";
|
|
16
|
-
import { generateRuleFile } from "./generator";
|
|
24
|
+
import { generateAlertmanager, generateRuleFile } from "./generator";
|
|
17
25
|
|
|
18
26
|
const PACKAGE = "@intentius/chant-lexicon-prometheus";
|
|
19
27
|
|
|
@@ -42,3 +50,27 @@ export const ruleGroupsImporter: EmbeddedContentImporter = {
|
|
|
42
50
|
};
|
|
43
51
|
},
|
|
44
52
|
};
|
|
53
|
+
|
|
54
|
+
export const alertmanagerImporter: EmbeddedContentImporter = {
|
|
55
|
+
what: "an Alertmanager config",
|
|
56
|
+
|
|
57
|
+
matches(content) {
|
|
58
|
+
return content.select === undefined && typeof content.text === "string" && looksLikeAlertmanagerConfig(content.document);
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
import(content): EmbeddedImport {
|
|
62
|
+
const parsed = parsePrometheusYaml(content.text!);
|
|
63
|
+
if (parsed.kind !== "alertmanager") throw new Error("this is not an alertmanager.yml");
|
|
64
|
+
const { files, declarations } = generateAlertmanager(parsed.config);
|
|
65
|
+
if (declarations.length === 0) throw new Error("the import declared nothing");
|
|
66
|
+
return {
|
|
67
|
+
files,
|
|
68
|
+
value: {
|
|
69
|
+
bindings: declarations.map((d) => ({ from: d.path, name: d.name })),
|
|
70
|
+
shape: "list",
|
|
71
|
+
through: { from: PACKAGE, name: "alertmanagerYaml" },
|
|
72
|
+
},
|
|
73
|
+
warnings: parsed.warnings,
|
|
74
|
+
};
|
|
75
|
+
},
|
|
76
|
+
};
|
package/src/import/generator.ts
CHANGED
|
@@ -381,6 +381,21 @@ function untypedComment(what: string, keys: string[]): string[] {
|
|
|
381
381
|
|
|
382
382
|
/** Generate the TypeScript declaring one `alertmanager.yml`. */
|
|
383
383
|
export function generateAlertmanagerFiles(config: AlertmanagerConfig): GeneratedFile[] {
|
|
384
|
+
return generateAlertmanager(config).files;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/** A declaration an `alertmanager.yml` import exports: its module and variable. */
|
|
388
|
+
export interface AlertmanagerDeclaration {
|
|
389
|
+
readonly path: string;
|
|
390
|
+
readonly name: string;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Generate the TypeScript declaring one `alertmanager.yml`, and list every
|
|
395
|
+
* declaration it exports, module by module (#3031: a ConfigMap holding the
|
|
396
|
+
* file becomes `alertmanagerYaml([...])` over them).
|
|
397
|
+
*/
|
|
398
|
+
export function generateAlertmanager(config: AlertmanagerConfig): { files: GeneratedFile[]; declarations: AlertmanagerDeclaration[] } {
|
|
384
399
|
const names = new Names([...LEXICON_NAMES, "global", "tracing"]);
|
|
385
400
|
const modules: Module[] = [];
|
|
386
401
|
const receiverVars = new Map<string, { v: string; mod: Module }>();
|
|
@@ -558,7 +573,10 @@ export function generateAlertmanagerFiles(config: AlertmanagerConfig): Generated
|
|
|
558
573
|
mod.exports.push(v);
|
|
559
574
|
}
|
|
560
575
|
|
|
561
|
-
return
|
|
576
|
+
return {
|
|
577
|
+
files: modules.map((m) => ({ path: m.path, content: m.render() })),
|
|
578
|
+
declarations: modules.flatMap((m) => m.exports.map((name) => ({ path: m.path, name }))),
|
|
579
|
+
};
|
|
562
580
|
}
|
|
563
581
|
|
|
564
582
|
/** The rule file and `alertmanager.yml` TypeScript generator `chant import` runs. */
|
|
@@ -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 {
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
* listed for a reader who meets them in a lint report.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { auditRule, type RuleMeta } from "@intentius/chant/audit/catalog";
|
|
12
|
+
import { applyLineage, auditRule, type RuleMeta } from "@intentius/chant/audit/catalog";
|
|
13
|
+
import { prometheusAuditLineage } from "./audit-lineage";
|
|
13
14
|
|
|
14
15
|
function sourceRule(id: string, category: RuleMeta["category"], title: string, remediation: string): RuleMeta {
|
|
15
16
|
return { id, tier: "merge-worthy", fixKind: "guidance", category, title, remediation, yamlBased: false };
|
|
@@ -118,6 +119,118 @@ export const prometheusAuditCatalog: Record<string, RuleMeta> = {
|
|
|
118
119
|
"merge-worthy",
|
|
119
120
|
"correctness",
|
|
120
121
|
"Receiver integration missing its destination or credential",
|
|
121
|
-
"Set the integration's url,
|
|
122
|
+
"Set the integration's destination and credential (url, webhook_url, api_key, routing_key, chat_id, room_id, to/smarthost/from, ...), through its *_file or global equivalent where there is one.",
|
|
123
|
+
),
|
|
124
|
+
PROM210: outputRule(
|
|
125
|
+
"PROM210",
|
|
126
|
+
"merge-worthy",
|
|
127
|
+
"correctness",
|
|
128
|
+
"Integration or global setting Alertmanager rejects",
|
|
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
|
+
),
|
|
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.",
|
|
122
229
|
),
|
|
123
230
|
};
|
|
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
|
+
|
|
235
|
+
// Prior art credits live beside the rules in ./audit-lineage.ts (see core audit/prior-art.ts).
|
|
236
|
+
applyLineage(prometheusAuditCatalog, prometheusAuditLineage);
|