@intentius/chant-lexicon-grafana 0.124.30 → 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 (166) hide show
  1. package/dist/actions/grafana.js +36 -0
  2. package/dist/actions/index.js +1 -0
  3. package/dist/alerting-build.js +262 -0
  4. package/dist/alerting.js +125 -0
  5. package/dist/annotations.js +47 -0
  6. package/dist/api/apply.js +320 -0
  7. package/dist/api/bind.js +44 -0
  8. package/dist/api/client.js +113 -0
  9. package/dist/api/converge.js +61 -0
  10. package/dist/api/dashboards.js +210 -0
  11. package/dist/api/datasources.js +24 -0
  12. package/dist/api/fake-grafana-writes.js +185 -0
  13. package/dist/api/fake-grafana.js +161 -0
  14. package/dist/api/folders.js +290 -0
  15. package/dist/api/library-panels.js +82 -0
  16. package/dist/build.js +767 -0
  17. package/dist/catalog.js +198 -0
  18. package/dist/codegen/docs-cli.js +3 -0
  19. package/dist/codegen/docs.js +130 -0
  20. package/dist/codegen/generate-cli.js +7 -0
  21. package/dist/codegen/generate.js +97 -0
  22. package/dist/codegen/package.js +28 -0
  23. package/dist/codegen/schema-types.js +159 -0
  24. package/dist/composites/agent-dashboard.js +460 -0
  25. package/dist/composites/catalog.js +306 -0
  26. package/dist/composites/index.js +8 -0
  27. package/dist/composites/red-dashboard.js +135 -0
  28. package/dist/composites/shared.js +74 -0
  29. package/dist/composites/slo-alert-rules.js +96 -0
  30. package/dist/composites/slo-dashboard.js +153 -0
  31. package/dist/config.js +110 -0
  32. package/dist/contact-point-check.js +61 -0
  33. package/dist/contact-point-secrets.js +32 -0
  34. package/dist/contact-point-settings.gen.js +421 -0
  35. package/dist/dashboard.js +60 -0
  36. package/dist/datasource-refs.js +170 -0
  37. package/dist/datasource-settings.js +57 -0
  38. package/dist/datasource.js +68 -0
  39. package/dist/deep-observe-hooks.js +266 -0
  40. package/dist/deep-observe.js +171 -0
  41. package/dist/describe-resources.js +249 -0
  42. package/dist/detect.js +119 -0
  43. package/dist/export-resources.js +73 -0
  44. package/dist/folder.js +86 -0
  45. package/dist/import/alerting.js +514 -0
  46. package/dist/import/edits.js +90 -0
  47. package/dist/import/embedded.js +60 -0
  48. package/dist/import/generator.js +656 -0
  49. package/dist/import/live-export.js +246 -0
  50. package/dist/import/mappings.js +317 -0
  51. package/dist/import/model.js +23 -0
  52. package/dist/import/normalize-alerting.js +106 -0
  53. package/dist/import/normalize.js +426 -0
  54. package/dist/import/operator.js +252 -0
  55. package/dist/import/parser.js +1480 -0
  56. package/dist/import/report.js +58 -0
  57. package/dist/import/testdata/fixtures.js +122 -0
  58. package/dist/import/v2.js +814 -0
  59. package/dist/index.js +59 -0
  60. package/dist/init-templates.js +305 -0
  61. package/dist/integrity.json +2 -2
  62. package/dist/k8s.js +548 -0
  63. package/dist/library-panel.js +83 -0
  64. package/dist/lint/audit-catalog.js +40 -0
  65. package/dist/lint/audit-lineage.js +9 -0
  66. package/dist/lint/post-synth/graf101.js +13 -0
  67. package/dist/lint/post-synth/graf102.js +13 -0
  68. package/dist/lint/post-synth/graf103.js +13 -0
  69. package/dist/lint/post-synth/graf104.js +13 -0
  70. package/dist/lint/post-synth/graf105.js +13 -0
  71. package/dist/lint/post-synth/graf106.js +13 -0
  72. package/dist/lint/post-synth/graf107.js +13 -0
  73. package/dist/lint/post-synth/graf108.js +13 -0
  74. package/dist/lint/post-synth/graf109.js +13 -0
  75. package/dist/lint/post-synth/graf110.js +13 -0
  76. package/dist/lint/post-synth/graf111.js +13 -0
  77. package/dist/lint/post-synth/graf112.js +13 -0
  78. package/dist/lint/post-synth/graf113.js +13 -0
  79. package/dist/lint/post-synth/graf114.js +13 -0
  80. package/dist/lint/post-synth/graf115.js +13 -0
  81. package/dist/lint/post-synth/graf116.js +13 -0
  82. package/dist/lint/post-synth/graf117.js +13 -0
  83. package/dist/lint/post-synth/graf118.js +37 -0
  84. package/dist/lint/post-synth/grafana-helpers.js +89 -0
  85. package/dist/lint/post-synth/index.js +38 -0
  86. package/dist/lint/rules/grafana-ast.js +70 -0
  87. package/dist/lint/rules/index.js +6 -0
  88. package/dist/lint/rules/literal-secret.js +109 -0
  89. package/dist/lint/rules/uid-syntax.js +53 -0
  90. package/dist/lsp/completions.js +11 -0
  91. package/dist/lsp/hover.js +29 -0
  92. package/dist/manifest.json +1 -1
  93. package/dist/op/activities/grafana-apply.js +147 -0
  94. package/dist/op/activities/index.js +7 -0
  95. package/dist/op/builders.js +19 -0
  96. package/dist/ownership.js +102 -0
  97. package/dist/package-cli.js +14 -0
  98. package/dist/panel-options.js +12 -0
  99. package/dist/panels.js +296 -0
  100. package/dist/pin.js +113 -0
  101. package/dist/plugin.js +172 -0
  102. package/dist/promql-check.js +176 -0
  103. package/dist/query-models.js +13 -0
  104. package/dist/query-syntax.js +129 -0
  105. package/dist/query.js +227 -0
  106. package/dist/schema/annotationslist.gen.js +5 -0
  107. package/dist/schema/azuremonitor.gen.js +5 -0
  108. package/dist/schema/barchart.gen.js +5 -0
  109. package/dist/schema/bargauge.gen.js +5 -0
  110. package/dist/schema/bigquery.gen.js +5 -0
  111. package/dist/schema/candlestick.gen.js +5 -0
  112. package/dist/schema/canvas.gen.js +5 -0
  113. package/dist/schema/cloudwatch.gen.js +5 -0
  114. package/dist/schema/dashboard.gen.js +6 -0
  115. package/dist/schema/dashboardlist.gen.js +5 -0
  116. package/dist/schema/datagrid.gen.js +5 -0
  117. package/dist/schema/elasticsearch.gen.js +5 -0
  118. package/dist/schema/expr.gen.js +5 -0
  119. package/dist/schema/gauge.gen.js +5 -0
  120. package/dist/schema/geomap.gen.js +5 -0
  121. package/dist/schema/googlecloudmonitoring.gen.js +5 -0
  122. package/dist/schema/grafanapyroscope.gen.js +5 -0
  123. package/dist/schema/heatmap.gen.js +5 -0
  124. package/dist/schema/histogram.gen.js +5 -0
  125. package/dist/schema/index.js +40 -0
  126. package/dist/schema/logs.gen.js +5 -0
  127. package/dist/schema/loki.gen.js +5 -0
  128. package/dist/schema/news.gen.js +5 -0
  129. package/dist/schema/nodegraph.gen.js +5 -0
  130. package/dist/schema/piechart.gen.js +5 -0
  131. package/dist/schema/prometheus.gen.js +5 -0
  132. package/dist/schema/stat.gen.js +5 -0
  133. package/dist/schema/statetimeline.gen.js +5 -0
  134. package/dist/schema/statushistory.gen.js +5 -0
  135. package/dist/schema/table.gen.js +5 -0
  136. package/dist/schema/tempo.gen.js +5 -0
  137. package/dist/schema/text.gen.js +5 -0
  138. package/dist/schema/timeseries.gen.js +5 -0
  139. package/dist/schema/trend.gen.js +5 -0
  140. package/dist/schema/xychart.gen.js +5 -0
  141. package/dist/schema-validate.js +265 -0
  142. package/dist/serializer.js +40 -0
  143. package/dist/skill-defs.js +60 -0
  144. package/dist/spec/fetch-cli.js +26 -0
  145. package/dist/spec/fetch-notifiers-cli.js +53 -0
  146. package/dist/spec/fetch-units-cli.js +40 -0
  147. package/dist/spec/notifiers.js +126 -0
  148. package/dist/spec/overlay.js +92 -0
  149. package/dist/spec/schemas.gen.js +42 -0
  150. package/dist/spec/schemas.js +62 -0
  151. package/dist/spec/units.gen.js +280 -0
  152. package/dist/spec/units.js +72 -0
  153. package/dist/transformations.js +127 -0
  154. package/dist/util.js +34 -0
  155. package/dist/validate-alerting.js +444 -0
  156. package/dist/validate-cli.js +7 -0
  157. package/dist/validate-output.js +635 -0
  158. package/dist/validate.js +122 -0
  159. package/dist/validation.js +13 -0
  160. package/dist/variables.js +70 -0
  161. package/package.json +10 -10
  162. package/src/codegen/package.ts +2 -2
  163. package/src/spec/fetch-notifiers-cli.ts +2 -2
  164. package/src/spec/fetch-units-cli.ts +2 -2
  165. package/src/spec/overlay.ts +2 -2
  166. package/src/spec/schemas.ts +2 -2
@@ -0,0 +1,153 @@
1
+ /**
2
+ * `SloDashboard`: one SLO's SLI, error budget and burn rates, from an `Slo`
3
+ * declaration.
4
+ *
5
+ * Every series name, the objective, the window and the burn-rate pairs come
6
+ * from the prometheus lexicon's `sloMetrics()`, which reads the same
7
+ * declaration the rules are built from. Renaming the SLO, changing its
8
+ * objective or its windows moves the panels and their threshold lines with
9
+ * it.
10
+ */
11
+ import { Composite } from "@intentius/chant/composite";
12
+ import { sloMetrics } from "@intentius/chant-lexicon-prometheus/composites/slo";
13
+ import { Dashboard } from "../dashboard.js";
14
+ import { Row, StatPanel, TimeSeriesPanel } from "../panels.js";
15
+ import { PromQuery } from "../query.js";
16
+ import { slugUid } from "../util.js";
17
+ import { dashboardProps, num, requireDatasource, selector, thresholds } from "./shared.js";
18
+ function isMetrics(x) {
19
+ return typeof x === "object" && x !== null && "errorBudgetRemaining" in x && "burnRates" in x && "selector" in x;
20
+ }
21
+ /** The PromQL the SLO dashboard runs, from `sloMetrics()`. Exposed for tests and for panels of your own. */
22
+ export function sloQueries(m) {
23
+ const s = (series) => `${series}${m.selector}`;
24
+ const budget = num(m.errorBudget);
25
+ return {
26
+ sli: `1 - ${s(m.windowErrorRatio)}`,
27
+ objective: s(m.objectiveRatio),
28
+ errorBudgetRemaining: s(m.errorBudgetRemaining),
29
+ alertsFiring: `sum(${selector("ALERTS", [
30
+ ["alertname", "=", m.alertName],
31
+ ["slo", "=", m.name],
32
+ ["alertstate", "=", "firing"],
33
+ ])}) or vector(0)`,
34
+ burnRates: m.burnRates.map((b) => ({
35
+ ...b,
36
+ longExpr: `${s(b.longRecord)} / ${budget}`,
37
+ shortExpr: `${s(b.shortRecord)} / ${budget}`,
38
+ })),
39
+ };
40
+ }
41
+ const PERCENT = { unit: "percentunit", decimals: 3 };
42
+ /**
43
+ * A dashboard for one SLO: the SLI over its window against the objective,
44
+ * the error budget left, and each burn-rate alert's two windows with the
45
+ * rate it fires at drawn as a threshold line.
46
+ *
47
+ * @example
48
+ * ```ts
49
+ * import { Slo } from "@intentius/chant-lexicon-prometheus";
50
+ * import { SloDashboard } from "@intentius/chant-lexicon-grafana";
51
+ *
52
+ * export const checkout = Slo({ name: "checkout", objective: 0.999, window: "30d", sli: { ... } });
53
+ * export const checkoutSlo = SloDashboard({ slo: checkout, datasource: prometheus });
54
+ * ```
55
+ */
56
+ export const SloDashboard = Composite((props) => {
57
+ requireDatasource("SloDashboard", props.datasource);
58
+ if (!props.slo)
59
+ throw new Error("SloDashboard: slo is required (an Slo(...) declaration)");
60
+ const m = isMetrics(props.slo) ? props.slo : sloMetrics(props.slo);
61
+ const q = sloQueries(m);
62
+ const ds = props.datasource;
63
+ const objective = num(m.objective * 100);
64
+ const sli = new StatPanel({
65
+ title: `SLI over ${m.window}`,
66
+ description: `Share of good events over the last ${m.window}. Green at or above the ${objective}% objective.`,
67
+ datasource: ds,
68
+ gridPos: { w: 6, h: 5 },
69
+ targets: [new PromQuery({ expr: q.sli, legendFormat: "SLI" })],
70
+ options: { colorMode: "background", graphMode: "none", reduceOptions: { calcs: ["lastNotNull"] } },
71
+ fieldConfig: { defaults: { ...PERCENT, thresholds: thresholds("red", [[m.objective, "green"]]) } },
72
+ });
73
+ const target = new StatPanel({
74
+ title: "Objective",
75
+ datasource: ds,
76
+ gridPos: { w: 6, h: 5 },
77
+ targets: [new PromQuery({ expr: q.objective, legendFormat: "objective" })],
78
+ options: { colorMode: "none", graphMode: "none", reduceOptions: { calcs: ["lastNotNull"] } },
79
+ fieldConfig: { defaults: PERCENT },
80
+ });
81
+ const remaining = new StatPanel({
82
+ title: "Error budget remaining",
83
+ description: `Share of the ${m.window} error budget (${num(m.errorBudget * 100)}% of events) not yet spent. Below 0 the objective is missed.`,
84
+ datasource: ds,
85
+ gridPos: { w: 6, h: 5 },
86
+ targets: [new PromQuery({ expr: q.errorBudgetRemaining, legendFormat: "remaining" })],
87
+ options: { colorMode: "background", graphMode: "area", reduceOptions: { calcs: ["lastNotNull"] } },
88
+ fieldConfig: { defaults: { unit: "percentunit", decimals: 1, thresholds: thresholds("red", [[0, "orange"], [0.25, "green"]]) } },
89
+ });
90
+ const firing = new StatPanel({
91
+ title: "Burn-rate alerts firing",
92
+ description: `${m.alertName} alerts for this SLO, any tier.`,
93
+ datasource: ds,
94
+ gridPos: { w: 6, h: 5 },
95
+ targets: [new PromQuery({ expr: q.alertsFiring, legendFormat: "firing" })],
96
+ options: { colorMode: "background", graphMode: "none", reduceOptions: { calcs: ["lastNotNull"] } },
97
+ fieldConfig: { defaults: { unit: "none", decimals: 0, thresholds: thresholds("green", [[1, "red"]]) } },
98
+ });
99
+ const sliOverTime = new TimeSeriesPanel({
100
+ title: `SLI over ${m.window}`,
101
+ description: `The dashed line is the ${objective}% objective.`,
102
+ datasource: ds,
103
+ gridPos: { w: 12, h: 8 },
104
+ targets: [new PromQuery({ expr: q.sli, legendFormat: "SLI" })],
105
+ fieldConfig: {
106
+ defaults: { ...PERCENT, thresholds: thresholds("red", [[m.objective, "green"]]), custom: { thresholdsStyle: { mode: "dashed" } } },
107
+ },
108
+ });
109
+ const budgetOverTime = new TimeSeriesPanel({
110
+ title: "Error budget remaining",
111
+ datasource: ds,
112
+ gridPos: { w: 12, h: 8 },
113
+ targets: [new PromQuery({ expr: q.errorBudgetRemaining, legendFormat: "remaining" })],
114
+ fieldConfig: {
115
+ defaults: { unit: "percentunit", decimals: 1, thresholds: thresholds("red", [[0, "green"]]), custom: { thresholdsStyle: { mode: "dashed" } } },
116
+ },
117
+ });
118
+ const rows = [new Row({ title: "Objective", panels: [sli, target, remaining, firing, sliOverTime, budgetOverTime] })];
119
+ if (q.burnRates.length > 0) {
120
+ const panels = q.burnRates.map((b) => new TimeSeriesPanel({
121
+ title: `Burn rate ${b.long} / ${b.short} (${b.severity})`,
122
+ description: `Error ratio over the budget: 1 spends the ${m.window} budget exactly over ${m.window}. ` +
123
+ `${b.alert} fires with severity ${b.severity} while both windows are above ${num(b.factor)}, which spends the budget in ${b.exhaustsIn}.`,
124
+ datasource: ds,
125
+ gridPos: { w: 12, h: 8 },
126
+ targets: [
127
+ new PromQuery({ expr: b.longExpr, legendFormat: b.long }),
128
+ new PromQuery({ expr: b.shortExpr, legendFormat: b.short }),
129
+ ],
130
+ fieldConfig: {
131
+ defaults: {
132
+ unit: "suffix:x",
133
+ decimals: 2,
134
+ min: 0,
135
+ thresholds: thresholds("green", [[b.factor, "red"]]),
136
+ custom: { thresholdsStyle: { mode: "dashed" } },
137
+ },
138
+ },
139
+ }));
140
+ rows.push(new Row({ title: "Burn rate by alert window", panels }));
141
+ }
142
+ const dashboard = new Dashboard({
143
+ ...dashboardProps(props, {
144
+ title: `SLO: ${m.name}`,
145
+ uid: slugUid(`slo-${m.name}`),
146
+ description: `${objective}% over ${m.window}: SLI, error budget and burn rates for the ${m.name} SLO.`,
147
+ tags: ["slo", m.name],
148
+ }),
149
+ time: props.time ?? { from: "now-7d", to: "now" },
150
+ panels: rows,
151
+ });
152
+ return { dashboard };
153
+ }, "SloDashboard");
package/dist/config.js ADDED
@@ -0,0 +1,110 @@
1
+ /**
2
+ * The `grafana` namespace in `chant.config.ts` (#2946): which Grafana each
3
+ * chant environment observes, exports from and (#2948) applies to.
4
+ *
5
+ * ```ts
6
+ * import type { ChantConfig } from "@intentius/chant/config";
7
+ * import "@intentius/chant-lexicon-grafana"; // brings the `grafana` key into ChantConfig
8
+ *
9
+ * export default {
10
+ * lexicons: ["grafana"],
11
+ * grafana: {
12
+ * profiles: {
13
+ * staging: { url: "https://grafana.staging.example.com", token: { env: "GRAFANA_STAGING_TOKEN" } },
14
+ * prod: { url: "https://grafana.example.com", token: { env: "GRAFANA_PROD_TOKEN" }, orgId: 2 },
15
+ * },
16
+ * },
17
+ * } satisfies ChantConfig;
18
+ * ```
19
+ *
20
+ * A profile is keyed by chant environment, as `k8s.profiles.<env>` is.
21
+ * Credentials are named by their environment variable, never written in
22
+ * the config. An environment with no profile falls back to `GRAFANA_URL`
23
+ * with `GRAFANA_TOKEN`, or `GRAFANA_USER` and `GRAFANA_PASSWORD` (see
24
+ * `resolveGrafanaTarget`).
25
+ *
26
+ * One schema, three consumers, as in the fountain lexicon: core validates the
27
+ * namespace against it at load, the type is derived from it, and that type
28
+ * augments `ChantConfig`.
29
+ */
30
+ import { z } from "zod";
31
+ const envRef = z.strictObject({ env: z.string() });
32
+ export const grafanaProfileSchema = z.strictObject({
33
+ /** Base URL of the Grafana instance. */
34
+ url: z.string(),
35
+ /** A service account token, named by its environment variable. */
36
+ token: envRef.optional(),
37
+ /** Basic auth, each half named by its environment variable. For a local instance; use a token elsewhere. */
38
+ basicAuth: z.strictObject({ user: envRef, password: envRef }).optional(),
39
+ /** Organisation id. Defaults to 1. */
40
+ orgId: z.number().int().positive().optional(),
41
+ /** The `dashboard.grafana.app` namespace, when it is not `default` / `org-<id>` (Grafana Cloud: `stacks-<id>`). */
42
+ namespace: z.string().optional(),
43
+ });
44
+ export const grafanaConfigSchema = z.strictObject({
45
+ /** One Grafana per chant environment. */
46
+ profiles: z.record(z.string(), grafanaProfileSchema).optional(),
47
+ });
48
+ function isUnresolved(v) {
49
+ return "reason" in v;
50
+ }
51
+ export { isUnresolved as isUnresolvedTarget };
52
+ /**
53
+ * The Grafana an environment reads from. Pure: `config` and `env` are
54
+ * passed in.
55
+ *
56
+ * 1. `grafana.profiles.<environment>`. A profile that names a credential
57
+ * variable which is not set is `no-credentials`, not an anonymous read:
58
+ * the profile said how to authenticate.
59
+ * 2. `GRAFANA_URL`, with `GRAFANA_TOKEN`, or `GRAFANA_USER` and
60
+ * `GRAFANA_PASSWORD`, or neither (an instance with anonymous access; a
61
+ * refused read is then `no-credentials`).
62
+ * 3. Otherwise `no-binding`.
63
+ */
64
+ export function resolveGrafanaTarget(input) {
65
+ const env = input.env ?? process.env;
66
+ const profile = input.environment !== undefined ? input.config?.grafana?.profiles?.[input.environment] : undefined;
67
+ if (profile) {
68
+ const source = `grafana.profiles.${input.environment}`;
69
+ let auth;
70
+ if (profile.token) {
71
+ const token = env[profile.token.env];
72
+ if (!token)
73
+ return { reason: "no-credentials", detail: `${source}.token names ${profile.token.env}, which is not set` };
74
+ auth = { token };
75
+ }
76
+ else if (profile.basicAuth) {
77
+ const user = env[profile.basicAuth.user.env];
78
+ const password = env[profile.basicAuth.password.env];
79
+ if (!user || !password) {
80
+ const missing = [!user ? profile.basicAuth.user.env : undefined, !password ? profile.basicAuth.password.env : undefined].filter(Boolean).join(" and ");
81
+ return { reason: "no-credentials", detail: `${source}.basicAuth names ${missing}, which ${missing.includes(" and ") ? "are" : "is"} not set` };
82
+ }
83
+ auth = { user, password };
84
+ }
85
+ return {
86
+ url: profile.url.replace(/\/+$/, ""),
87
+ ...(auth ? { auth } : {}),
88
+ ...(profile.orgId !== undefined ? { orgId: profile.orgId } : {}),
89
+ ...(profile.namespace ? { namespace: profile.namespace } : {}),
90
+ source,
91
+ };
92
+ }
93
+ const url = env.GRAFANA_URL;
94
+ if (!url) {
95
+ const where = input.environment !== undefined ? `grafana.profiles.${input.environment} is not declared and ` : "";
96
+ return { reason: "no-binding", detail: `${where}GRAFANA_URL is not set, so there is no Grafana to read` };
97
+ }
98
+ let auth;
99
+ if (env.GRAFANA_TOKEN)
100
+ auth = { token: env.GRAFANA_TOKEN };
101
+ else if (env.GRAFANA_USER && env.GRAFANA_PASSWORD)
102
+ auth = { user: env.GRAFANA_USER, password: env.GRAFANA_PASSWORD };
103
+ const orgId = env.GRAFANA_ORG_ID ? Number(env.GRAFANA_ORG_ID) : undefined;
104
+ return {
105
+ url: url.replace(/\/+$/, ""),
106
+ ...(auth ? { auth } : {}),
107
+ ...(orgId !== undefined && Number.isInteger(orgId) && orgId > 0 ? { orgId } : {}),
108
+ source: "env GRAFANA_URL",
109
+ };
110
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Checks one contact point receiver's `settings` against the options Grafana
3
+ * lists for its integration (`src/contact-point-settings.gen.ts`): a key the
4
+ * integration does not take, and a required option left out. An integration
5
+ * the table does not know is not checked.
6
+ */
7
+ import { CONTACT_POINT_NOTIFIERS } from "./contact-point-settings.gen.js";
8
+ function distance(a, b) {
9
+ const row = Array.from({ length: b.length + 1 }, (_, i) => i);
10
+ for (let i = 1; i <= a.length; i++) {
11
+ let prev = row[0];
12
+ row[0] = i;
13
+ for (let j = 1; j <= b.length; j++) {
14
+ const tmp = row[j];
15
+ row[j] = Math.min(row[j] + 1, row[j - 1] + 1, prev + (a[i - 1] === b[j - 1] ? 0 : 1));
16
+ prev = tmp;
17
+ }
18
+ }
19
+ return row[b.length];
20
+ }
21
+ function closest(key, known) {
22
+ const lower = key.toLowerCase();
23
+ const exact = known.find((k) => k.toLowerCase() === lower);
24
+ if (exact)
25
+ return exact;
26
+ const best = known.map((k) => [k, distance(lower, k.toLowerCase())]).sort((x, y) => x[1] - y[1])[0];
27
+ return best && best[1] <= Math.max(2, Math.floor(key.length / 3)) ? best[0] : undefined;
28
+ }
29
+ const isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
30
+ function walk(options, value, prefix, out) {
31
+ const known = options.map((o) => o.key);
32
+ for (const key of Object.keys(value)) {
33
+ if (known.includes(key))
34
+ continue;
35
+ const near = closest(key, known);
36
+ out.push({
37
+ severity: "warning",
38
+ path: `${prefix}${key}`,
39
+ message: `${prefix}${key} is not a setting of this integration${near ? `; did you mean ${prefix}${near}?` : ""}`,
40
+ });
41
+ }
42
+ for (const o of options) {
43
+ const v = value[o.key];
44
+ if (o.required && (v === undefined || v === null || v === "")) {
45
+ out.push({ severity: "error", path: `${prefix}${o.key}`, message: `${prefix}${o.key} is required` });
46
+ }
47
+ if (o.options && isObject(v))
48
+ walk(o.options, v, `${prefix}${o.key}.`, out);
49
+ if (o.options && o.kind === "objects" && Array.isArray(v)) {
50
+ v.forEach((item, i) => isObject(item) && walk(o.options, item, `${prefix}${o.key}[${i}].`, out));
51
+ }
52
+ }
53
+ }
54
+ export function checkContactPointSettings(type, settings) {
55
+ const schema = CONTACT_POINT_NOTIFIERS[type];
56
+ if (!schema)
57
+ return [];
58
+ const out = [];
59
+ walk(schema.options, settings, "", out);
60
+ return out;
61
+ }
@@ -0,0 +1,32 @@
1
+ import { CONTACT_POINT_NOTIFIERS } from "./contact-point-settings.gen.js";
2
+ /** The dotted paths of the options marked `secure`, through nested objects (`tlsConfig.clientKey`). */
3
+ function securePaths(options, prefix = "") {
4
+ return options.flatMap((o) => [...(o.secure ? [`${prefix}${o.key}`] : []), ...(o.options ? securePaths(o.options, `${prefix}${o.key}.`) : [])]);
5
+ }
6
+ /**
7
+ * The contact point settings Grafana stores encrypted, per integration: the
8
+ * options marked `secure` by `GET /api/alert-notifiers?version=2` on
9
+ * `grafana/grafana:13.2.2` (the same list on 12.4.11), read from the table
10
+ * `just fetch-notifiers` writes. Nested settings are dotted paths
11
+ * (`tlsConfig.clientKey`).
12
+ *
13
+ * GRAF002 flags a literal at one of these paths, and the importer replaces
14
+ * the `[REDACTED]` that an export without secrets writes there.
15
+ */
16
+ export const CONTACT_POINT_SECRET_SETTINGS = Object.freeze(Object.fromEntries(Object.entries(CONTACT_POINT_NOTIFIERS).map(([type, schema]) => [type, securePaths(schema.options)])));
17
+ /** How Grafana provisioning reads a value from outside the file: `$VAR`, `${VAR}`, `$__env{…}`, `$__file{…}`, `$__vault{…}`. */
18
+ export const EXPANDED_VALUE = /\$(\{[A-Za-z_][A-Za-z0-9_]*\}|[A-Za-z_][A-Za-z0-9_]*|__(env|file|vault)\{[^}]+\})/;
19
+ /** What an export without secrets writes in place of each one. */
20
+ export const REDACTED = "[REDACTED]";
21
+ /** The secret settings of one receiver present in `settings`, with their values: `[path, value]`. */
22
+ export function secretSettings(type, settings) {
23
+ const out = [];
24
+ for (const path of CONTACT_POINT_SECRET_SETTINGS[type] ?? []) {
25
+ let node = settings;
26
+ for (const seg of path.split("."))
27
+ node = node && typeof node === "object" ? node[seg] : undefined;
28
+ if (node !== undefined)
29
+ out.push([path, node]);
30
+ }
31
+ return out;
32
+ }