@intentius/chant-lexicon-prometheus 0.124.31 → 0.124.32
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
package/dist/catalog.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entity catalog: every class this package exports, keyed by class name.
|
|
3
|
+
* It feeds the packaged registry (`dist/meta.json`), LSP completions and
|
|
4
|
+
* hover, and the docs.
|
|
5
|
+
*/
|
|
6
|
+
import { RULE_GROUP_TYPE } from "./rules.js";
|
|
7
|
+
import { PROMETHEUS_CONFIG_TYPE, SCRAPE_CONFIG_TYPE } from "./prometheus-config.js";
|
|
8
|
+
import { INHIBIT_RULE_TYPE, RECEIVER_TYPE, ROUTE_TYPE, SETTINGS_TYPE, TIME_INTERVAL_TYPE } from "./alertmanager.js";
|
|
9
|
+
export const CATALOG = [
|
|
10
|
+
{
|
|
11
|
+
className: "RuleGroup",
|
|
12
|
+
entityType: RULE_GROUP_TYPE,
|
|
13
|
+
file: "rule file",
|
|
14
|
+
description: "A group of recording and alerting rules, evaluated together on one interval",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
className: "ScrapeConfig",
|
|
18
|
+
entityType: SCRAPE_CONFIG_TYPE,
|
|
19
|
+
file: "prometheus.yml",
|
|
20
|
+
description: "One scrape job: static or discovered targets, scrape settings, and relabelling",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
className: "PrometheusConfig",
|
|
24
|
+
entityType: PROMETHEUS_CONFIG_TYPE,
|
|
25
|
+
file: "prometheus.yml",
|
|
26
|
+
description: "The global, alerting, rule_files, remote_write, remote_read and otlp sections of prometheus.yml",
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
className: "Route",
|
|
30
|
+
entityType: ROUTE_TYPE,
|
|
31
|
+
file: "alertmanager.yml",
|
|
32
|
+
description: "A node in Alertmanager's routing tree: matchers, grouping and timing, and the receiver alerts go to",
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
className: "Receiver",
|
|
36
|
+
entityType: RECEIVER_TYPE,
|
|
37
|
+
file: "alertmanager.yml",
|
|
38
|
+
description: "A notification receiver with webhook, email, Slack and PagerDuty integrations",
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
className: "InhibitRule",
|
|
42
|
+
entityType: INHIBIT_RULE_TYPE,
|
|
43
|
+
file: "alertmanager.yml",
|
|
44
|
+
description: "Mutes alerts matching target_matchers while an alert matching source_matchers fires",
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
className: "TimeInterval",
|
|
48
|
+
entityType: TIME_INTERVAL_TYPE,
|
|
49
|
+
file: "alertmanager.yml",
|
|
50
|
+
description: "A named set of time periods a route can mute or activate on",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
className: "AlertmanagerSettings",
|
|
54
|
+
entityType: SETTINGS_TYPE,
|
|
55
|
+
file: "alertmanager.yml",
|
|
56
|
+
description: "The global block (SMTP, Slack and PagerDuty defaults, resolve_timeout) and the templates list",
|
|
57
|
+
},
|
|
58
|
+
];
|
|
59
|
+
/** The catalog as a chant lexicon registry, keyed by class name. */
|
|
60
|
+
export function lexiconRegistry() {
|
|
61
|
+
const out = {};
|
|
62
|
+
for (const e of [...CATALOG].sort((a, b) => a.className.localeCompare(b.className))) {
|
|
63
|
+
out[e.className] = { resourceType: e.entityType, kind: "resource", lexicon: "prometheus" };
|
|
64
|
+
}
|
|
65
|
+
return out;
|
|
66
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Documentation generation for the prometheus lexicon: the generated
|
|
3
|
+
* reference pages from the core docs pipeline, plus the authored pages in
|
|
4
|
+
* docs/pages/.
|
|
5
|
+
*/
|
|
6
|
+
import { dirname, join } from "path";
|
|
7
|
+
import { fileURLToPath } from "url";
|
|
8
|
+
import { docsPipeline, writeDocsSite } from "@intentius/chant/codegen/docs";
|
|
9
|
+
function serviceFromType(resourceType) {
|
|
10
|
+
const parts = resourceType.split("::");
|
|
11
|
+
return parts.length >= 2 ? parts[1] : "Prometheus";
|
|
12
|
+
}
|
|
13
|
+
const overview = `The prometheus lexicon types [Prometheus](https://prometheus.io/) recording and
|
|
14
|
+
alerting rules and [Alertmanager](https://prometheus.io/docs/alerting/latest/alertmanager/)
|
|
15
|
+
routing. \`chant build\` emits the rule file Prometheus loads through
|
|
16
|
+
\`rule_files:\`, and the \`alertmanager.yml\` Alertmanager reads.
|
|
17
|
+
|
|
18
|
+
\`\`\`ts
|
|
19
|
+
import { RuleGroup, Route, Receiver } from "@intentius/chant-lexicon-prometheus";
|
|
20
|
+
|
|
21
|
+
export const api = new RuleGroup({
|
|
22
|
+
name: "api",
|
|
23
|
+
rules: [
|
|
24
|
+
{ record: "job:http_errors:ratio5m", expr: 'sum by (job) (rate(http_requests_total{code=~"5.."}[5m])) / sum by (job) (rate(http_requests_total[5m]))' },
|
|
25
|
+
{ alert: "ApiErrors", expr: "job:http_errors:ratio5m > 0.05", for: "10m", labels: { severity: "page" }, annotations: { summary: "5xx ratio above 5%" } },
|
|
26
|
+
],
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
export const oncall = new Receiver({ name: "oncall", pagerduty_configs: [{ routing_key_file: "/etc/alertmanager/secrets/pd-key" }] });
|
|
30
|
+
export const fallback = new Receiver({ name: "fallback", webhook_configs: [{ url: "http://alert-sink:8080/" }] });
|
|
31
|
+
export const root = new Route({ receiver: fallback, group_by: ["alertname", "job"], routes: [{ matchers: ['severity="page"'], receiver: oncall }] });
|
|
32
|
+
\`\`\`
|
|
33
|
+
|
|
34
|
+
The same \`RuleGroup\` renders into a Prometheus Operator \`PrometheusRule\`
|
|
35
|
+
through the k8s lexicon, so rules are declared once whichever way Prometheus
|
|
36
|
+
loads them.
|
|
37
|
+
|
|
38
|
+
The \`Slo\` composite builds a service level objective to a \`RuleGroup\`:
|
|
39
|
+
error ratios per window, the error budget left, and the SRE Workbook's
|
|
40
|
+
multiwindow multi-burn-rate alerts. See [SLOs](./slos/).
|
|
41
|
+
|
|
42
|
+
The \`GenAiRules\` composite builds recording rules, opt-in alerts and spend
|
|
43
|
+
from a declared price table for the otel lexicon's GenAI collector preset,
|
|
44
|
+
reading every metric name from its \`GenAiMetrics\`. See
|
|
45
|
+
[GenAI Rules](./genai-rules/).
|
|
46
|
+
|
|
47
|
+
Checks run at build time: PromQL syntax in every \`expr\` (PROM104), unique
|
|
48
|
+
group names (PROM101), valid durations (PROM103), routes that name
|
|
49
|
+
declared receivers (PROM201), and every alert severity routed by some route
|
|
50
|
+
(PROM202). \`promtool check rules\` and \`amtool check-config\` are wrapped as
|
|
51
|
+
helpers for tests and CI, and run when installed.
|
|
52
|
+
|
|
53
|
+
\`chant import\` turns an existing rule file or \`alertmanager.yml\` into this
|
|
54
|
+
TypeScript, and \`chant build\` on the result gives the file back (see
|
|
55
|
+
Importing Rule Files and alertmanager.yml).
|
|
56
|
+
`;
|
|
57
|
+
const outputFormat = `The prometheus lexicon serializes two files:
|
|
58
|
+
|
|
59
|
+
- **The rule file**, from every \`RuleGroup\` in the build: \`groups:\`, each
|
|
60
|
+
with its \`name\`, optional \`interval\`, \`query_offset\`, \`limit\` and
|
|
61
|
+
\`labels\`, and its \`rules\`. This is the file \`rule_files:\` points at and
|
|
62
|
+
\`promtool check rules\` reads.
|
|
63
|
+
- **\`alertmanager.yml\`**, from the \`AlertmanagerSettings\`, \`Route\`,
|
|
64
|
+
\`InhibitRule\`, \`Receiver\` and \`TimeInterval\` entities, in that section
|
|
65
|
+
order: \`global\`, \`templates\`, \`route\`, \`inhibit_rules\`, \`receivers\`,
|
|
66
|
+
\`time_intervals\`, \`tracing\`.
|
|
67
|
+
|
|
68
|
+
The rule file is the primary output when the build declares any rule
|
|
69
|
+
groups, and \`alertmanager.yml\` is written beside it (\`-o dist/rules.yml\`
|
|
70
|
+
also writes \`dist/alertmanager.yml\`). A build with only Alertmanager
|
|
71
|
+
entities emits \`alertmanager.yml\` as its primary output.
|
|
72
|
+
|
|
73
|
+
- Rules inside a group and child routes keep the order they are written in,
|
|
74
|
+
which is the order Prometheus evaluates and Alertmanager matches them.
|
|
75
|
+
Groups, receivers and time intervals, whose order carries no meaning, are
|
|
76
|
+
sorted by name, so the files are the same however they were built. Keys
|
|
77
|
+
inside a rule come out in the order Prometheus documents them (\`record\`
|
|
78
|
+
or \`alert\`, \`expr\`, \`for\`, \`keep_firing_for\`, \`labels\`,
|
|
79
|
+
\`annotations\`).
|
|
80
|
+
- A route references its receiver and time intervals by name in the output,
|
|
81
|
+
whether the declaration used the entity or a string.
|
|
82
|
+
- The root route is the one \`Route\` no other route lists as a child.
|
|
83
|
+
- Neither file has a metadata channel, so no ownership marker is stamped.
|
|
84
|
+
`;
|
|
85
|
+
export async function generateDocs(opts) {
|
|
86
|
+
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
87
|
+
const config = {
|
|
88
|
+
name: "prometheus",
|
|
89
|
+
displayName: "Prometheus",
|
|
90
|
+
description: "Typed Prometheus rule groups and Alertmanager routing",
|
|
91
|
+
distDir: join(pkgDir, "dist"),
|
|
92
|
+
outDir: join(pkgDir, "docs"),
|
|
93
|
+
basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/prometheus/",
|
|
94
|
+
overview,
|
|
95
|
+
outputFormat,
|
|
96
|
+
serviceFromType,
|
|
97
|
+
srcDir: join(pkgDir, "src"),
|
|
98
|
+
examplesDir: join(pkgDir, "examples"),
|
|
99
|
+
};
|
|
100
|
+
const result = docsPipeline(config);
|
|
101
|
+
writeDocsSite(config, result);
|
|
102
|
+
if (opts?.verbose) {
|
|
103
|
+
console.error(`Generated ${result.pages.size} documentation pages`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The prometheus lexicon's "generation" step.
|
|
3
|
+
*
|
|
4
|
+
* There is no upstream schema to fetch: Prometheus and Alertmanager define
|
|
5
|
+
* their config files as Go structs and publish no machine-readable schema for
|
|
6
|
+
* them. The types are written by hand against the releases in
|
|
7
|
+
* `PROMETHEUS_PIN` (src/pin.ts), and this step derives the registry from the
|
|
8
|
+
* entity catalog, so the packaged `dist/meta.json` can never disagree with
|
|
9
|
+
* the classes the package exports.
|
|
10
|
+
*/
|
|
11
|
+
import { dirname, join } from "path";
|
|
12
|
+
import { fileURLToPath } from "url";
|
|
13
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
14
|
+
import { CATALOG, lexiconRegistry } from "../catalog.js";
|
|
15
|
+
/** The packaged types entry re-exports the compiled declarations, which carry every config type. */
|
|
16
|
+
const TYPES_DTS = `// The prometheus lexicon's entity classes and config types are hand-written in
|
|
17
|
+
// src/ and compiled to dist/index.d.ts; this entry re-exports them.
|
|
18
|
+
export * from "../index.js";
|
|
19
|
+
`;
|
|
20
|
+
export async function generate(opts) {
|
|
21
|
+
const registry = lexiconRegistry();
|
|
22
|
+
const lexiconJSON = `${JSON.stringify(registry, null, 2)}\n`;
|
|
23
|
+
if (opts?.verbose) {
|
|
24
|
+
console.error(`prometheus: ${CATALOG.length} entities (RuleGroup and the Alertmanager kinds)`);
|
|
25
|
+
}
|
|
26
|
+
return {
|
|
27
|
+
lexiconJSON,
|
|
28
|
+
typesDTS: TYPES_DTS,
|
|
29
|
+
indexTS: "",
|
|
30
|
+
resources: CATALOG.length,
|
|
31
|
+
properties: 0,
|
|
32
|
+
enums: 0,
|
|
33
|
+
warnings: [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
/** Write the registry to src/generated/lexicon-prometheus.json, for tools that read it from there. */
|
|
37
|
+
export function writeGeneratedFiles(result) {
|
|
38
|
+
const pkgDir = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
|
|
39
|
+
const dir = join(pkgDir, "src", "generated");
|
|
40
|
+
mkdirSync(dir, { recursive: true });
|
|
41
|
+
writeFileSync(join(dir, "lexicon-prometheus.json"), result.lexiconJSON);
|
|
42
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prometheus lexicon packaging, delegating to core's packagePipeline.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from "fs";
|
|
5
|
+
import { join, dirname } from "path";
|
|
6
|
+
import { moduleSourceDir } from "@intentius/chant/module-source";
|
|
7
|
+
import { packagePipeline, collectSkills, } from "@intentius/chant/codegen/package";
|
|
8
|
+
import { generate } from "./generate.js";
|
|
9
|
+
import { prometheusSkills } from "../skill-defs.js";
|
|
10
|
+
// src/codegen/package.ts -> src/
|
|
11
|
+
const srcDir = dirname(moduleSourceDir(import.meta.url));
|
|
12
|
+
export async function packageLexicon(opts = {}) {
|
|
13
|
+
const pkgJson = JSON.parse(readFileSync(join(srcDir, "..", "package.json"), "utf-8"));
|
|
14
|
+
return packagePipeline({
|
|
15
|
+
generate: (genOpts) => generate({ verbose: genOpts.verbose, force: genOpts.force }),
|
|
16
|
+
buildManifest: () => ({
|
|
17
|
+
name: "prometheus",
|
|
18
|
+
version: pkgJson.version ?? "0.0.1",
|
|
19
|
+
chantVersion: ">=0.1.0",
|
|
20
|
+
namespace: "Prometheus",
|
|
21
|
+
intrinsics: [],
|
|
22
|
+
pseudoParameters: {},
|
|
23
|
+
}),
|
|
24
|
+
srcDir,
|
|
25
|
+
collectSkills: () => collectSkills(prometheusSkills()),
|
|
26
|
+
version: pkgJson.version ?? "0.0.1",
|
|
27
|
+
}, opts);
|
|
28
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
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
|
+
import { buildCollectorConfig } from "@intentius/chant-lexicon-otel/collector";
|
|
23
|
+
import { isOTelComponent } from "@intentius/chant-lexicon-otel/define";
|
|
24
|
+
import { collectorConfigs } from "@intentius/chant-lexicon-otel/lint/post-synth/otel-helpers";
|
|
25
|
+
import { prometheusLabel, prometheusMetricName, serviceGraphNames, spanMetricsNames, SERVICEGRAPH_NAMESPACE, SPANMETRICS_DEFAULT_NAMESPACE, } from "@intentius/chant-lexicon-otel/metric-names";
|
|
26
|
+
import { canonicalTypeOf } from "@intentius/chant-lexicon-otel/model";
|
|
27
|
+
import { parsePromql, selectors } from "./promql-analysis.js";
|
|
28
|
+
/** Labels the `prometheus` exporter, or the scrape, adds to every series: `job` and `instance` from the resource, and the scope. */
|
|
29
|
+
const ADDED_LABELS = ["job", "instance", "otel_scope_name", "otel_scope_version", "otel_scope_schema_url"];
|
|
30
|
+
function asObj(v) {
|
|
31
|
+
return v && typeof v === "object" && !Array.isArray(v) ? v : {};
|
|
32
|
+
}
|
|
33
|
+
/** A component's type, with a renamed built-in's new name read as the old one (`span_metrics/genai` is `spanmetrics`). */
|
|
34
|
+
function typeOf(kind, id) {
|
|
35
|
+
return canonicalTypeOf(kind, id);
|
|
36
|
+
}
|
|
37
|
+
function sanitize(name) {
|
|
38
|
+
return name.replace(/[^A-Za-z0-9_:]/g, "_");
|
|
39
|
+
}
|
|
40
|
+
/** The common dotted prefix of a connector's metric names (`genai.tokens` for `genai.tokens.input` and `.output`). */
|
|
41
|
+
function commonPrefix(names) {
|
|
42
|
+
if (names.length === 0)
|
|
43
|
+
return undefined;
|
|
44
|
+
const split = names.map((n) => n.split("."));
|
|
45
|
+
const out = [];
|
|
46
|
+
for (let i = 0; i < split[0].length - 1; i++) {
|
|
47
|
+
if (split.every((s) => s.length - 1 > i && s[i] === split[0][i]))
|
|
48
|
+
out.push(split[0][i]);
|
|
49
|
+
else
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
if (out.length === 0 && names.length === 1 && split[0].length > 1)
|
|
53
|
+
return split[0].slice(0, -1).join(".");
|
|
54
|
+
return out.length > 0 ? out.join(".") : undefined;
|
|
55
|
+
}
|
|
56
|
+
/** Every collector config in the build: rebuilt from its otel entities, and found in its output. */
|
|
57
|
+
export function buildCollectorConfigs(ctx) {
|
|
58
|
+
const configs = collectorConfigs(ctx).map((f) => f.config);
|
|
59
|
+
const otel = [...(ctx.entities?.values() ?? [])].filter((e) => e?.lexicon === "otel");
|
|
60
|
+
if (otel.some(isOTelComponent))
|
|
61
|
+
configs.push(buildCollectorConfig(otel).config);
|
|
62
|
+
return configs;
|
|
63
|
+
}
|
|
64
|
+
/** The metrics the collector configs emit; undefined when there are none. */
|
|
65
|
+
export function collectorMetrics(configs) {
|
|
66
|
+
if (configs.length === 0)
|
|
67
|
+
return undefined;
|
|
68
|
+
const namespaces = new Set();
|
|
69
|
+
const metrics = new Map();
|
|
70
|
+
for (const config of configs) {
|
|
71
|
+
const exporters = Object.entries(asObj(config.exporters)).filter(([id]) => typeOf("exporter", id) === "prometheus" || typeOf("exporter", id) === "prometheusremotewrite");
|
|
72
|
+
const namings = exporters.length > 0
|
|
73
|
+
? exporters.map(([, c]) => {
|
|
74
|
+
const e = asObj(c);
|
|
75
|
+
return {
|
|
76
|
+
naming: { ...(typeof e.namespace === "string" ? { namespace: e.namespace } : {}), ...(e.add_metric_suffixes === false ? { add_metric_suffixes: false } : {}) },
|
|
77
|
+
openLabels: asObj(e.resource_to_telemetry_conversion).enabled === true,
|
|
78
|
+
};
|
|
79
|
+
})
|
|
80
|
+
: [{ naming: {}, openLabels: false }];
|
|
81
|
+
for (const { naming, openLabels } of namings) {
|
|
82
|
+
const prefix = (ns) => `${naming.namespace ? `${sanitize(naming.namespace)}_` : ""}${sanitize(ns)}_`;
|
|
83
|
+
// The two connectors' defaults are theirs in every build that has a collector config.
|
|
84
|
+
namespaces.add(prefix(SPANMETRICS_DEFAULT_NAMESPACE));
|
|
85
|
+
namespaces.add(prefix(SERVICEGRAPH_NAMESPACE));
|
|
86
|
+
const add = (connector, m, extra = []) => {
|
|
87
|
+
const labels = openLabels ? undefined : new Set([...m.dimensions.map(prometheusLabel), ...extra.map(prometheusLabel), ...ADDED_LABELS]);
|
|
88
|
+
// A classic histogram is served as three series whatever add_metric_suffixes says.
|
|
89
|
+
const series = m.type === "histogram"
|
|
90
|
+
? [
|
|
91
|
+
[`${m.prometheus}_bucket`, labels ? new Set([...labels, "le"]) : undefined],
|
|
92
|
+
[`${m.prometheus}_sum`, labels],
|
|
93
|
+
[`${m.prometheus}_count`, labels],
|
|
94
|
+
]
|
|
95
|
+
: [[m.prometheus, labels]];
|
|
96
|
+
for (const [name, l] of series) {
|
|
97
|
+
metrics.set(name, [...(metrics.get(name) ?? []), { connector, ...(l ? { labels: l } : {}) }]);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
for (const [id, raw] of Object.entries(asObj(config.connectors))) {
|
|
101
|
+
const c = asObj(raw);
|
|
102
|
+
switch (typeOf("connector", id)) {
|
|
103
|
+
case "spanmetrics": {
|
|
104
|
+
const n = spanMetricsNames(c, naming);
|
|
105
|
+
if (n.namespace !== "")
|
|
106
|
+
namespaces.add(prefix(n.namespace));
|
|
107
|
+
for (const m of [n.calls, n.duration, n.events])
|
|
108
|
+
if (m)
|
|
109
|
+
add(id, m);
|
|
110
|
+
break;
|
|
111
|
+
}
|
|
112
|
+
case "servicegraph": {
|
|
113
|
+
const n = serviceGraphNames(c, naming);
|
|
114
|
+
for (const m of [n.requests, n.failed, n.serverDuration, n.clientDuration])
|
|
115
|
+
add(id, m);
|
|
116
|
+
break;
|
|
117
|
+
}
|
|
118
|
+
case "sum": {
|
|
119
|
+
const names = [];
|
|
120
|
+
for (const signal of ["spans", "spanevents", "metrics", "datapoints", "logs"]) {
|
|
121
|
+
for (const [name, def] of Object.entries(asObj(c[signal]))) {
|
|
122
|
+
const attrs = (Array.isArray(asObj(def).attributes) ? asObj(def).attributes : []).map((a) => String(asObj(a).key ?? ""));
|
|
123
|
+
names.push(name);
|
|
124
|
+
add(id, { name, prometheus: prometheusMetricName(name, "sum", undefined, naming), type: "sum", dimensions: attrs.filter(Boolean) });
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const ns = commonPrefix(names);
|
|
128
|
+
if (ns)
|
|
129
|
+
namespaces.add(prefix(ns));
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "signaltometrics": {
|
|
133
|
+
const names = [];
|
|
134
|
+
for (const signal of ["spans", "datapoints", "logs", "profiles"]) {
|
|
135
|
+
for (const def of Array.isArray(c[signal]) ? c[signal] : []) {
|
|
136
|
+
const m = asObj(def);
|
|
137
|
+
if (typeof m.name !== "string")
|
|
138
|
+
continue;
|
|
139
|
+
const type = m.histogram !== undefined || m.exponential_histogram !== undefined ? "histogram" : m.sum !== undefined ? "sum" : undefined;
|
|
140
|
+
if (!type)
|
|
141
|
+
continue;
|
|
142
|
+
const unit = typeof m.unit === "string" ? m.unit : undefined;
|
|
143
|
+
const attrs = [
|
|
144
|
+
...(Array.isArray(m.attributes) ? m.attributes : []),
|
|
145
|
+
...(Array.isArray(m.include_resource_attributes) ? m.include_resource_attributes : []),
|
|
146
|
+
].map((a) => String(asObj(a).key ?? ""));
|
|
147
|
+
names.push(m.name);
|
|
148
|
+
add(id, { name: m.name, prometheus: prometheusMetricName(m.name, type, unit, naming), type, ...(unit ? { unit } : {}), dimensions: attrs.filter(Boolean) });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
const ns = commonPrefix(names);
|
|
152
|
+
if (ns)
|
|
153
|
+
namespaces.add(prefix(ns));
|
|
154
|
+
break;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { namespaces: [...namespaces], metrics };
|
|
161
|
+
}
|
|
162
|
+
// ── PromQL ──────────────────────────────────────────────────────────
|
|
163
|
+
/** Functions that write a label of their own, so an aggregation over them can group by it. */
|
|
164
|
+
const RELABEL = new Set(["label_replace", "label_join"]);
|
|
165
|
+
/** Every `by (...)` aggregation in the expression: its labels and the text it aggregates. */
|
|
166
|
+
function byAggregations(top, src) {
|
|
167
|
+
const out = [];
|
|
168
|
+
const walk = (n) => {
|
|
169
|
+
if (n.name === "AggregateExpr") {
|
|
170
|
+
const mod = n.getChild("AggregateModifier");
|
|
171
|
+
const body = n.getChild("FunctionCallBody");
|
|
172
|
+
if (mod?.getChild("By") && body) {
|
|
173
|
+
const labels = (mod.getChild("GroupingLabels")?.getChildren("LabelName") ?? []).map((l) => src.slice(l.from, l.to));
|
|
174
|
+
out.push({ labels, body: src.slice(body.from + 1, body.to - 1) });
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (let c = n.firstChild; c; c = c.nextSibling)
|
|
178
|
+
walk(c);
|
|
179
|
+
};
|
|
180
|
+
walk(top);
|
|
181
|
+
return out;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* What a PromQL expression reads under the build's connector namespaces and
|
|
185
|
+
* the build does not emit: a metric name no connector writes, and a
|
|
186
|
+
* `by (...)` label none of the aggregated metrics carries. A name outside
|
|
187
|
+
* every namespace is not read. An aggregation is checked only when every
|
|
188
|
+
* series under it is an emitted metric and nothing under it adds a label
|
|
189
|
+
* (`label_replace`, `label_join`). Selectors come from the lexicon's
|
|
190
|
+
* `promql-analysis.ts`.
|
|
191
|
+
*/
|
|
192
|
+
export function collectorMetricIssues(expr, emitted) {
|
|
193
|
+
const top = parsePromql(expr);
|
|
194
|
+
if (!top)
|
|
195
|
+
return [];
|
|
196
|
+
const issues = [];
|
|
197
|
+
const owned = (name) => emitted.namespaces.some((p) => name.startsWith(p));
|
|
198
|
+
const seen = new Set();
|
|
199
|
+
for (const { name } of selectors(expr)) {
|
|
200
|
+
if (!name || seen.has(name) || !owned(name) || emitted.metrics.has(name))
|
|
201
|
+
continue;
|
|
202
|
+
seen.add(name);
|
|
203
|
+
const p = emitted.namespaces.filter((x) => name.startsWith(x)).sort((a, b) => b.length - a.length)[0];
|
|
204
|
+
issues.push({ kind: "name", metric: name, message: `reads ${name}, which no collector config in the build emits under ${p.slice(0, -1)}` });
|
|
205
|
+
}
|
|
206
|
+
const reported = new Set();
|
|
207
|
+
for (const { labels, body } of byAggregations(top, expr)) {
|
|
208
|
+
const read = selectors(body);
|
|
209
|
+
if (read.length === 0 || read.some((s) => s.within.some((fn) => RELABEL.has(fn))))
|
|
210
|
+
continue;
|
|
211
|
+
const metrics = read.map((s) => (s.name ? emitted.metrics.get(s.name) : undefined));
|
|
212
|
+
if (!metrics.every((m) => m !== undefined && m.every((e) => e.labels)))
|
|
213
|
+
continue;
|
|
214
|
+
const carried = new Set(metrics.flatMap((m) => m.flatMap((e) => [...e.labels])));
|
|
215
|
+
const shown = [...new Set(read.map((s) => s.name))].join(", ");
|
|
216
|
+
for (const label of labels) {
|
|
217
|
+
const key = `${label} ${shown}`;
|
|
218
|
+
if (carried.has(label) || reported.has(key))
|
|
219
|
+
continue;
|
|
220
|
+
reported.add(key);
|
|
221
|
+
issues.push({ kind: "label", metric: shown, label, message: `groups ${shown} by ${label}, which is not a dimension the collector config declares for it` });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return issues;
|
|
225
|
+
}
|