@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.
Files changed (114) hide show
  1. package/dist/alertmanager.js +61 -0
  2. package/dist/api/alertmanager.js +98 -0
  3. package/dist/api/apply.js +306 -0
  4. package/dist/api/bind.js +66 -0
  5. package/dist/api/client.js +110 -0
  6. package/dist/api/evaluated.js +86 -0
  7. package/dist/api/fake-servers.js +165 -0
  8. package/dist/api/index.js +17 -0
  9. package/dist/api/ruler.js +122 -0
  10. package/dist/build.js +240 -0
  11. package/dist/catalog.js +66 -0
  12. package/dist/codegen/docs-cli.js +3 -0
  13. package/dist/codegen/docs.js +105 -0
  14. package/dist/codegen/generate-cli.js +7 -0
  15. package/dist/codegen/generate.js +42 -0
  16. package/dist/codegen/package.js +28 -0
  17. package/dist/collector-metrics.js +225 -0
  18. package/dist/composites/alert-routing.js +210 -0
  19. package/dist/composites/catalog.js +344 -0
  20. package/dist/composites/genai.js +454 -0
  21. package/dist/composites/index.js +8 -0
  22. package/dist/composites/red-alerts.js +135 -0
  23. package/dist/composites/slo.js +353 -0
  24. package/dist/composites/watchdog.js +110 -0
  25. package/dist/config-model.js +52 -0
  26. package/dist/config.js +225 -0
  27. package/dist/describe-resources.js +299 -0
  28. package/dist/detect.js +12 -0
  29. package/dist/duration.js +54 -0
  30. package/dist/export-resources.js +163 -0
  31. package/dist/import/alertmanager-live.js +309 -0
  32. package/dist/import/embedded.js +100 -0
  33. package/dist/import/generator.js +683 -0
  34. package/dist/import/parser.js +428 -0
  35. package/dist/import/slo.js +207 -0
  36. package/dist/index.js +33 -0
  37. package/dist/init-templates.js +119 -0
  38. package/dist/integrations.js +33 -0
  39. package/dist/integrity.json +2 -2
  40. package/dist/lint/audit-catalog.js +60 -0
  41. package/dist/lint/audit-lineage.js +42 -0
  42. package/dist/lint/post-synth/index.js +68 -0
  43. package/dist/lint/post-synth/prom-helpers.js +157 -0
  44. package/dist/lint/post-synth/prom101.js +13 -0
  45. package/dist/lint/post-synth/prom102.js +13 -0
  46. package/dist/lint/post-synth/prom103.js +13 -0
  47. package/dist/lint/post-synth/prom104.js +13 -0
  48. package/dist/lint/post-synth/prom105.js +13 -0
  49. package/dist/lint/post-synth/prom106.js +13 -0
  50. package/dist/lint/post-synth/prom107.js +13 -0
  51. package/dist/lint/post-synth/prom201.js +13 -0
  52. package/dist/lint/post-synth/prom202.js +13 -0
  53. package/dist/lint/post-synth/prom203.js +13 -0
  54. package/dist/lint/post-synth/prom204.js +13 -0
  55. package/dist/lint/post-synth/prom205.js +13 -0
  56. package/dist/lint/post-synth/prom206.js +13 -0
  57. package/dist/lint/post-synth/prom207.js +13 -0
  58. package/dist/lint/post-synth/prom208.js +13 -0
  59. package/dist/lint/post-synth/prom209.js +13 -0
  60. package/dist/lint/post-synth/prom210.js +13 -0
  61. package/dist/lint/post-synth/prom211.js +17 -0
  62. package/dist/lint/post-synth/prom212.js +14 -0
  63. package/dist/lint/post-synth/prom213.js +16 -0
  64. package/dist/lint/post-synth/prom214.js +14 -0
  65. package/dist/lint/post-synth/prom215.js +14 -0
  66. package/dist/lint/post-synth/prom216.js +15 -0
  67. package/dist/lint/post-synth/prom217.js +14 -0
  68. package/dist/lint/post-synth/prom218.js +15 -0
  69. package/dist/lint/post-synth/prom219.js +14 -0
  70. package/dist/lint/post-synth/prom220.js +14 -0
  71. package/dist/lint/post-synth/prom221.js +15 -0
  72. package/dist/lint/post-synth/prom222.js +14 -0
  73. package/dist/lint/post-synth/prom223.js +15 -0
  74. package/dist/lint/post-synth/prom224.js +14 -0
  75. package/dist/lint/post-synth/prom225.js +15 -0
  76. package/dist/lint/post-synth/prom301.js +19 -0
  77. package/dist/lint/rules/index.js +8 -0
  78. package/dist/lint/rules/literal-credential.js +127 -0
  79. package/dist/lint/rules/prom-ast.js +65 -0
  80. package/dist/lint/rules/promql-literal.js +65 -0
  81. package/dist/lint/rules/slo-literal.js +79 -0
  82. package/dist/lsp/completions.js +11 -0
  83. package/dist/lsp/hover.js +21 -0
  84. package/dist/manifest.json +1 -1
  85. package/dist/matchers.js +131 -0
  86. package/dist/model.js +108 -0
  87. package/dist/op/activities/index.js +20 -0
  88. package/dist/op/activities/promtool.js +87 -0
  89. package/dist/op/activities/rule-audit.js +128 -0
  90. package/dist/op/activities/rules-loaded.js +73 -0
  91. package/dist/op/activities/silence.js +123 -0
  92. package/dist/op/activity-contracts.js +51 -0
  93. package/dist/op/audit-op.js +28 -0
  94. package/dist/op/builders.js +57 -0
  95. package/dist/op/slo-rule-tests.js +148 -0
  96. package/dist/package-cli.js +14 -0
  97. package/dist/pin.js +13 -0
  98. package/dist/plugin.js +143 -0
  99. package/dist/prometheus-config.js +38 -0
  100. package/dist/promql-analysis.js +343 -0
  101. package/dist/promql.js +44 -0
  102. package/dist/rule-eval.js +466 -0
  103. package/dist/rules.js +97 -0
  104. package/dist/serializer.js +59 -0
  105. package/dist/skill-defs.js +41 -0
  106. package/dist/tools.js +85 -0
  107. package/dist/validate-cli.js +7 -0
  108. package/dist/validate-config.js +556 -0
  109. package/dist/validate-integrations.js +388 -0
  110. package/dist/validate-relabel.js +52 -0
  111. package/dist/validate-security.js +135 -0
  112. package/dist/validate.js +69 -0
  113. package/package.json +10 -10
  114. package/src/codegen/package.ts +2 -2
@@ -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,3 @@
1
+ #!/usr/bin/env tsx
2
+ import { generateDocs } from "./docs.js";
3
+ await generateDocs({ verbose: true });
@@ -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,7 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * CLI entry point for `npm run generate` in lexicon-prometheus.
4
+ */
5
+ import { generate, writeGeneratedFiles } from "./generate.js";
6
+ const result = await generate({ verbose: true });
7
+ writeGeneratedFiles(result);
@@ -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
+ }