@intentius/chant-lexicon-prometheus 0.106.4 → 0.108.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.
Files changed (111) hide show
  1. package/README.md +6 -1
  2. package/dist/api/alertmanager.d.ts +56 -0
  3. package/dist/api/alertmanager.d.ts.map +1 -0
  4. package/dist/api/apply.d.ts +126 -0
  5. package/dist/api/apply.d.ts.map +1 -0
  6. package/dist/api/bind.d.ts +49 -0
  7. package/dist/api/bind.d.ts.map +1 -0
  8. package/dist/api/client.d.ts +76 -0
  9. package/dist/api/client.d.ts.map +1 -0
  10. package/dist/api/evaluated.d.ts +36 -0
  11. package/dist/api/evaluated.d.ts.map +1 -0
  12. package/dist/api/fake-servers.d.ts +74 -0
  13. package/dist/api/fake-servers.d.ts.map +1 -0
  14. package/dist/api/index.d.ts +18 -0
  15. package/dist/api/index.d.ts.map +1 -0
  16. package/dist/api/ruler.d.ts +87 -0
  17. package/dist/api/ruler.d.ts.map +1 -0
  18. package/dist/composites/alert-routing.d.ts +104 -0
  19. package/dist/composites/alert-routing.d.ts.map +1 -0
  20. package/dist/composites/catalog.d.ts.map +1 -1
  21. package/dist/composites/index.d.ts +6 -0
  22. package/dist/composites/index.d.ts.map +1 -1
  23. package/dist/composites/red-alerts.d.ts +96 -0
  24. package/dist/composites/red-alerts.d.ts.map +1 -0
  25. package/dist/composites/watchdog.d.ts +69 -0
  26. package/dist/composites/watchdog.d.ts.map +1 -0
  27. package/dist/config.d.ts +273 -0
  28. package/dist/config.d.ts.map +1 -0
  29. package/dist/describe-resources.d.ts +48 -0
  30. package/dist/describe-resources.d.ts.map +1 -0
  31. package/dist/export-resources.d.ts +41 -0
  32. package/dist/export-resources.d.ts.map +1 -0
  33. package/dist/import/alertmanager-live.d.ts +35 -0
  34. package/dist/import/alertmanager-live.d.ts.map +1 -0
  35. package/dist/import/parser.d.ts +12 -0
  36. package/dist/import/parser.d.ts.map +1 -1
  37. package/dist/index.d.ts +6 -1
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/integrity.json +3 -3
  40. package/dist/lint/post-synth/prom-helpers.d.ts +11 -3
  41. package/dist/lint/post-synth/prom-helpers.d.ts.map +1 -1
  42. package/dist/manifest.json +1 -1
  43. package/dist/op/activities/index.d.ts +25 -0
  44. package/dist/op/activities/index.d.ts.map +1 -0
  45. package/dist/op/activities/promtool.d.ts +78 -0
  46. package/dist/op/activities/promtool.d.ts.map +1 -0
  47. package/dist/op/activities/rule-audit.d.ts +65 -0
  48. package/dist/op/activities/rule-audit.d.ts.map +1 -0
  49. package/dist/op/activities/rules-loaded.d.ts +60 -0
  50. package/dist/op/activities/rules-loaded.d.ts.map +1 -0
  51. package/dist/op/activities/silence.d.ts +80 -0
  52. package/dist/op/activities/silence.d.ts.map +1 -0
  53. package/dist/op/activity-contracts.d.ts +113 -0
  54. package/dist/op/activity-contracts.d.ts.map +1 -0
  55. package/dist/op/audit-op.d.ts +38 -0
  56. package/dist/op/audit-op.d.ts.map +1 -0
  57. package/dist/op/builders.d.ts +57 -0
  58. package/dist/op/builders.d.ts.map +1 -0
  59. package/dist/op/slo-rule-tests.d.ts +44 -0
  60. package/dist/op/slo-rule-tests.d.ts.map +1 -0
  61. package/dist/plugin.d.ts.map +1 -1
  62. package/dist/rules/prom-helpers.ts +38 -3
  63. package/package.json +19 -3
  64. package/src/api/alertmanager.ts +122 -0
  65. package/src/api/apply.test.ts +218 -0
  66. package/src/api/apply.ts +375 -0
  67. package/src/api/bind.ts +87 -0
  68. package/src/api/client.ts +138 -0
  69. package/src/api/evaluated.test.ts +78 -0
  70. package/src/api/evaluated.ts +106 -0
  71. package/src/api/fake-servers.ts +209 -0
  72. package/src/api/index.ts +29 -0
  73. package/src/api/ruler.ts +168 -0
  74. package/src/api/testdata/am-status-original.yml +85 -0
  75. package/src/composites/alert-routing.test.ts +163 -0
  76. package/src/composites/alert-routing.ts +277 -0
  77. package/src/composites/catalog.test.ts +10 -2
  78. package/src/composites/catalog.ts +214 -0
  79. package/src/composites/index.ts +14 -0
  80. package/src/composites/red-alerts.test.ts +182 -0
  81. package/src/composites/red-alerts.ts +214 -0
  82. package/src/composites/watchdog.test.ts +85 -0
  83. package/src/composites/watchdog.ts +142 -0
  84. package/src/config.test.ts +93 -0
  85. package/src/config.ts +288 -0
  86. package/src/describe-resources.test.ts +325 -0
  87. package/src/describe-resources.ts +328 -0
  88. package/src/export-resources.test.ts +253 -0
  89. package/src/export-resources.ts +193 -0
  90. package/src/import/alertmanager-live.test.ts +66 -0
  91. package/src/import/alertmanager-live.ts +291 -0
  92. package/src/import/parser.ts +18 -1
  93. package/src/import/roundtrip.test.ts +1 -0
  94. package/src/index.ts +37 -0
  95. package/src/lint/post-synth/prom-helpers.ts +38 -3
  96. package/src/op/activities/index.ts +31 -0
  97. package/src/op/activities/promtool.test.ts +107 -0
  98. package/src/op/activities/promtool.ts +149 -0
  99. package/src/op/activities/rule-audit.test.ts +89 -0
  100. package/src/op/activities/rule-audit.ts +168 -0
  101. package/src/op/activities/rules-loaded.test.ts +61 -0
  102. package/src/op/activities/rules-loaded.ts +98 -0
  103. package/src/op/activities/silence.test.ts +107 -0
  104. package/src/op/activities/silence.ts +175 -0
  105. package/src/op/activity-contracts.test.ts +141 -0
  106. package/src/op/activity-contracts.ts +96 -0
  107. package/src/op/audit-op.ts +54 -0
  108. package/src/op/builders.ts +82 -0
  109. package/src/op/slo-rule-tests.test.ts +84 -0
  110. package/src/op/slo-rule-tests.ts +173 -0
  111. package/src/plugin.ts +16 -0
@@ -0,0 +1,93 @@
1
+ /** `prometheus.profiles.<env>`: the schema, the target resolution and the namespace boundary (#3371). */
2
+
3
+ import { describe, expect, test } from "vitest";
4
+ import {
5
+ declaredNamespaces,
6
+ namespaceOfGroup,
7
+ prometheusConfigSchema,
8
+ resolveAlertmanagerTarget,
9
+ resolveRulerTarget,
10
+ type RulerTarget,
11
+ } from "./config";
12
+ import { prometheusPlugin } from "./plugin";
13
+
14
+ const config = {
15
+ prometheus: {
16
+ profiles: {
17
+ prod: {
18
+ ruler: { kind: "mimir" as const, url: "https://mimir.example.com/", tenant: "shop", namespace: "shop", groupNamespaces: { infra: "platform" }, token: { env: "MIMIR_TOKEN" } },
19
+ alertmanager: { kind: "mimir" as const, url: "https://mimir.example.com", tenant: "shop" },
20
+ },
21
+ bare: { ruler: { kind: "loki" as const, url: "http://loki:3100" } },
22
+ prom: { ruler: { kind: "prometheus" as const, url: "http://prometheus:9090" } },
23
+ },
24
+ },
25
+ };
26
+
27
+ describe("prometheusConfigSchema", () => {
28
+ test("is the plugin's config schema and accepts the documented profile", () => {
29
+ expect(prometheusPlugin.configSchema).toBe(prometheusConfigSchema);
30
+ expect(prometheusConfigSchema.safeParse(config.prometheus).success).toBe(true);
31
+ });
32
+
33
+ test("refuses an unknown key and an unknown ruler kind", () => {
34
+ expect(prometheusConfigSchema.safeParse({ profiles: { prod: { ruler: { kind: "mimir", url: "x", namespce: "typo" } } } }).success).toBe(false);
35
+ expect(prometheusConfigSchema.safeParse({ profiles: { prod: { ruler: { kind: "thanos", url: "x" } } } }).success).toBe(false);
36
+ });
37
+ });
38
+
39
+ describe("resolveRulerTarget", () => {
40
+ test("a profile resolves with its tenant, namespaces, token and the default prefix", () => {
41
+ const t = resolveRulerTarget({ environment: "prod", config, env: { MIMIR_TOKEN: "t0k" } }) as RulerTarget;
42
+ expect(t).toMatchObject({
43
+ kind: "mimir",
44
+ url: "https://mimir.example.com",
45
+ tenant: "shop",
46
+ namespace: "shop",
47
+ auth: { token: "t0k" },
48
+ prometheusPrefix: "/prometheus",
49
+ source: "prometheus.profiles.prod.ruler",
50
+ });
51
+ });
52
+
53
+ test("a token variable that is not set is no-credentials, not an anonymous read", () => {
54
+ expect(resolveRulerTarget({ environment: "prod", config, env: {} })).toEqual({
55
+ reason: "no-credentials",
56
+ detail: "prometheus.profiles.prod.ruler.token names MIMIR_TOKEN, which is not set",
57
+ });
58
+ });
59
+
60
+ test("a Mimir, Cortex or Loki profile with no namespace is no-binding; a Prometheus one needs none and has no prefix", () => {
61
+ expect(resolveRulerTarget({ environment: "bare", config, env: {} })).toMatchObject({ reason: "no-binding" });
62
+ expect(resolveRulerTarget({ environment: "prom", config, env: {} })).toMatchObject({ kind: "prometheus", prometheusPrefix: "" });
63
+ });
64
+
65
+ test("with no profile, PROMETHEUS_RULER_URL and then PROMETHEUS_URL bind, and neither is no-binding", () => {
66
+ expect(
67
+ resolveRulerTarget({ environment: "dev", config, env: { PROMETHEUS_RULER_URL: "http://cortex", PROMETHEUS_RULER_KIND: "cortex", PROMETHEUS_RULER_TENANT: "t" } }),
68
+ ).toMatchObject({ kind: "cortex", url: "http://cortex", tenant: "t", source: "env PROMETHEUS_RULER_URL" });
69
+ expect(resolveRulerTarget({ environment: "dev", config, env: { PROMETHEUS_URL: "http://p:9090" } })).toMatchObject({ kind: "prometheus", source: "env PROMETHEUS_URL" });
70
+ expect(resolveRulerTarget({ environment: "dev", config, env: {} })).toMatchObject({ reason: "no-binding" });
71
+ expect(resolveRulerTarget({ environment: "dev", config, env: { PROMETHEUS_RULER_URL: "x", PROMETHEUS_RULER_KIND: "thanos" } })).toMatchObject({ reason: "no-binding" });
72
+ });
73
+ });
74
+
75
+ describe("resolveAlertmanagerTarget", () => {
76
+ test("a profile, ALERTMANAGER_URL, or no-binding", () => {
77
+ expect(resolveAlertmanagerTarget({ environment: "prod", config, env: {} })).toMatchObject({ kind: "mimir", tenant: "shop", alertmanagerPrefix: "/alertmanager" });
78
+ expect(resolveAlertmanagerTarget({ environment: "dev", config, env: { ALERTMANAGER_URL: "http://am:9093/" } })).toMatchObject({ kind: "alertmanager", url: "http://am:9093" });
79
+ expect(resolveAlertmanagerTarget({ environment: "bare", config, env: {} })).toMatchObject({ reason: "no-binding" });
80
+ });
81
+ });
82
+
83
+ describe("the namespace boundary", () => {
84
+ const target = { namespace: "shop", groupNamespaces: { infra: "platform", api: "shop" } };
85
+ test("declaredNamespaces is namespace and every groupNamespaces value, once each", () => {
86
+ expect(declaredNamespaces(target)).toEqual(["shop", "platform"]);
87
+ expect(declaredNamespaces({ groupNamespaces: {} })).toEqual([]);
88
+ });
89
+ test("namespaceOfGroup prefers the group's own entry", () => {
90
+ expect(namespaceOfGroup(target, "infra")).toBe("platform");
91
+ expect(namespaceOfGroup(target, "checkout")).toBe("shop");
92
+ });
93
+ });
package/src/config.ts ADDED
@@ -0,0 +1,288 @@
1
+ /**
2
+ * The `prometheus` namespace in `chant.config.ts` (#3371): which ruler and
3
+ * which Alertmanager each chant environment observes and imports from, and
4
+ * (#3372) applies to.
5
+ *
6
+ * ```ts
7
+ * import type { ChantConfig } from "@intentius/chant/config";
8
+ * import "@intentius/chant-lexicon-prometheus"; // brings the `prometheus` key into ChantConfig
9
+ *
10
+ * export default {
11
+ * lexicons: ["prometheus"],
12
+ * prometheus: {
13
+ * profiles: {
14
+ * prod: {
15
+ * ruler: { kind: "mimir", url: "https://mimir.example.com", tenant: "shop", namespace: "shop-rules", token: { env: "MIMIR_TOKEN" } },
16
+ * alertmanager: { kind: "mimir", url: "https://mimir.example.com", tenant: "shop", token: { env: "MIMIR_TOKEN" } },
17
+ * },
18
+ * },
19
+ * },
20
+ * } satisfies ChantConfig;
21
+ * ```
22
+ *
23
+ * A rule file has no namespace and no ownership marker, so the ruler
24
+ * namespaces a profile names are the boundary: `namespace` is where the
25
+ * project's groups live, `groupNamespaces` moves single groups to another,
26
+ * and nothing outside those namespaces is read for import or written by an
27
+ * apply. On a plain Prometheus the "namespace" is the rule file path
28
+ * `/api/v1/rules` reports as `file`, and may be left out.
29
+ *
30
+ * Credentials are named by their environment variable, never written in the
31
+ * config. An environment with no profile falls back to environment
32
+ * variables (see {@link resolveRulerTarget} and
33
+ * {@link resolveAlertmanagerTarget}).
34
+ */
35
+
36
+ import { z } from "zod";
37
+ import type { ChantConfig } from "@intentius/chant/config";
38
+ import type { PromAuth, PromTarget } from "./api/client";
39
+
40
+ const envRef = z.strictObject({ env: z.string() });
41
+ const basicAuth = z.strictObject({ user: envRef, password: envRef });
42
+
43
+ /** The rulers chant reads rule groups from. `prometheus` is a plain Prometheus, read over `/api/v1/rules`. */
44
+ export const RULER_KINDS = ["mimir", "cortex", "loki", "prometheus"] as const;
45
+ export type RulerKind = (typeof RULER_KINDS)[number];
46
+
47
+ /** Where an Alertmanager config is read: a plain Alertmanager's `/api/v2/status`, or Mimir's and Cortex's `/api/v1/alerts`. */
48
+ export const ALERTMANAGER_KINDS = ["alertmanager", "mimir", "cortex"] as const;
49
+ export type AlertmanagerKind = (typeof ALERTMANAGER_KINDS)[number];
50
+
51
+ export const rulerProfileSchema = z.strictObject({
52
+ kind: z.enum(RULER_KINDS),
53
+ /** Base URL: the Mimir, Cortex or Loki endpoint the ruler API is served on, or the Prometheus server. */
54
+ url: z.string(),
55
+ /** `X-Scope-OrgID`. */
56
+ tenant: z.string().optional(),
57
+ /** The ruler namespace the project's rule groups live in (on a plain Prometheus, the rule file path). */
58
+ namespace: z.string().optional(),
59
+ /** Groups that live in another namespace than `namespace`, by group name. */
60
+ groupNamespaces: z.record(z.string(), z.string()).optional(),
61
+ /** Mimir's or Cortex's `-http.prometheus-http-prefix`. Defaults to `/prometheus` (to nothing for a plain Prometheus). */
62
+ prometheusPrefix: z.string().optional(),
63
+ token: envRef.optional(),
64
+ basicAuth: basicAuth.optional(),
65
+ });
66
+
67
+ export const alertmanagerProfileSchema = z.strictObject({
68
+ kind: z.enum(ALERTMANAGER_KINDS).optional(),
69
+ url: z.string(),
70
+ /** `X-Scope-OrgID`, for Mimir and Cortex. */
71
+ tenant: z.string().optional(),
72
+ /** Mimir's or Cortex's `-http.alertmanager-http-prefix`. Defaults to `/alertmanager`. */
73
+ alertmanagerPrefix: z.string().optional(),
74
+ token: envRef.optional(),
75
+ basicAuth: basicAuth.optional(),
76
+ });
77
+
78
+ export const prometheusProfileSchema = z.strictObject({
79
+ ruler: rulerProfileSchema.optional(),
80
+ alertmanager: alertmanagerProfileSchema.optional(),
81
+ });
82
+
83
+ export const prometheusConfigSchema = z.strictObject({
84
+ /** One ruler and one Alertmanager per chant environment. */
85
+ profiles: z.record(z.string(), prometheusProfileSchema).optional(),
86
+ });
87
+
88
+ export type RulerProfile = z.infer<typeof rulerProfileSchema>;
89
+ export type AlertmanagerProfile = z.infer<typeof alertmanagerProfileSchema>;
90
+ export type PrometheusProfile = z.infer<typeof prometheusProfileSchema>;
91
+ export type PrometheusLexiconConfig = z.infer<typeof prometheusConfigSchema>;
92
+
93
+ declare module "@intentius/chant/config" {
94
+ interface ChantConfig {
95
+ prometheus?: PrometheusLexiconConfig;
96
+ }
97
+ }
98
+
99
+ /** Compile-time proof that the augmentation reaches `ChantConfig`. */
100
+ export type PrometheusConfigNamespace = NonNullable<ChantConfig["prometheus"]>;
101
+
102
+ /** Why no target could be resolved, in the observation vocabulary. */
103
+ export interface UnresolvedTarget {
104
+ readonly reason: "no-binding" | "no-credentials";
105
+ readonly detail: string;
106
+ }
107
+
108
+ export function isUnresolvedTarget(v: object): v is UnresolvedTarget {
109
+ return "reason" in v;
110
+ }
111
+
112
+ /** A resolved ruler. */
113
+ export interface RulerTarget extends PromTarget {
114
+ readonly kind: RulerKind;
115
+ /** Where the project's groups live; absent only for an ad-hoc read with no namespace named. */
116
+ readonly namespace?: string;
117
+ readonly groupNamespaces: Readonly<Record<string, string>>;
118
+ readonly prometheusPrefix: string;
119
+ }
120
+
121
+ /** A resolved Alertmanager. */
122
+ export interface AlertmanagerTarget extends PromTarget {
123
+ readonly kind: AlertmanagerKind;
124
+ readonly alertmanagerPrefix: string;
125
+ }
126
+
127
+ /**
128
+ * The namespaces a ruler target declares, deduplicated, in the order they
129
+ * are named. This is the ownership boundary: import reads only these, and
130
+ * an apply (#3372) writes and deletes only inside these.
131
+ */
132
+ export function declaredNamespaces(target: Pick<RulerTarget, "namespace" | "groupNamespaces">): string[] {
133
+ const out: string[] = [];
134
+ for (const ns of [target.namespace, ...Object.values(target.groupNamespaces)]) {
135
+ if (ns !== undefined && ns !== "" && !out.includes(ns)) out.push(ns);
136
+ }
137
+ return out;
138
+ }
139
+
140
+ /** The namespace a declared group lives in: its `groupNamespaces` entry, else `namespace`. */
141
+ export function namespaceOfGroup(target: Pick<RulerTarget, "namespace" | "groupNamespaces">, group: string): string | undefined {
142
+ return target.groupNamespaces[group] ?? target.namespace;
143
+ }
144
+
145
+ type Env = Record<string, string | undefined>;
146
+
147
+ function profileAuth(
148
+ source: string,
149
+ profile: { token?: { env: string }; basicAuth?: { user: { env: string }; password: { env: string } } },
150
+ env: Env,
151
+ ): PromAuth | undefined | UnresolvedTarget {
152
+ if (profile.token) {
153
+ const token = env[profile.token.env];
154
+ if (!token) return { reason: "no-credentials", detail: `${source}.token names ${profile.token.env}, which is not set` };
155
+ return { token };
156
+ }
157
+ if (profile.basicAuth) {
158
+ const user = env[profile.basicAuth.user.env];
159
+ const password = env[profile.basicAuth.password.env];
160
+ if (!user || !password) {
161
+ const missing = [!user ? profile.basicAuth.user.env : undefined, !password ? profile.basicAuth.password.env : undefined].filter(Boolean).join(" and ");
162
+ return { reason: "no-credentials", detail: `${source}.basicAuth names ${missing}, which ${missing.includes(" and ") ? "are" : "is"} not set` };
163
+ }
164
+ return { user, password };
165
+ }
166
+ return undefined;
167
+ }
168
+
169
+ function envAuth(env: Env, prefix: string): PromAuth | undefined {
170
+ const token = env[`${prefix}_TOKEN`];
171
+ if (token) return { token };
172
+ const user = env[`${prefix}_USER`];
173
+ const password = env[`${prefix}_PASSWORD`];
174
+ return user && password ? { user, password } : undefined;
175
+ }
176
+
177
+ const trim = (url: string) => url.replace(/\/+$/, "");
178
+ const prefixOf = (p: string | undefined, fallback: string) => (p === undefined ? fallback : p === "" ? "" : `/${p.replace(/^\/+|\/+$/g, "")}`);
179
+
180
+ interface ResolveInput {
181
+ environment?: string;
182
+ config?: Pick<ChantConfig, "prometheus">;
183
+ env?: Env;
184
+ }
185
+
186
+ /**
187
+ * The ruler an environment reads from. Pure: `config` and `env` are passed in.
188
+ *
189
+ * 1. `prometheus.profiles.<environment>.ruler`. A profile that names a
190
+ * credential variable which is not set is `no-credentials`.
191
+ * 2. `PROMETHEUS_RULER_URL`, with `PROMETHEUS_RULER_KIND` (default `mimir`),
192
+ * `PROMETHEUS_RULER_TENANT`, `PROMETHEUS_RULER_NAMESPACE` and
193
+ * `PROMETHEUS_RULER_TOKEN` (or `_USER` and `_PASSWORD`).
194
+ * 3. `PROMETHEUS_URL`: a plain Prometheus, read over `/api/v1/rules`.
195
+ * 4. Otherwise `no-binding`.
196
+ */
197
+ export function resolveRulerTarget(input: ResolveInput): RulerTarget | UnresolvedTarget {
198
+ const env = input.env ?? process.env;
199
+ const profile = input.environment !== undefined ? input.config?.prometheus?.profiles?.[input.environment]?.ruler : undefined;
200
+ if (profile) {
201
+ const source = `prometheus.profiles.${input.environment}.ruler`;
202
+ const auth = profileAuth(source, profile, env);
203
+ if (auth && isUnresolvedTarget(auth)) return auth;
204
+ if (profile.kind !== "prometheus" && !profile.namespace) {
205
+ return { reason: "no-binding", detail: `${source}.namespace is not set, and a ${profile.kind} ruler's groups live in a namespace` };
206
+ }
207
+ return {
208
+ kind: profile.kind,
209
+ url: trim(profile.url),
210
+ ...(profile.tenant ? { tenant: profile.tenant } : {}),
211
+ ...(auth ? { auth } : {}),
212
+ ...(profile.namespace ? { namespace: profile.namespace } : {}),
213
+ groupNamespaces: profile.groupNamespaces ?? {},
214
+ prometheusPrefix: prefixOf(profile.prometheusPrefix, profile.kind === "prometheus" ? "" : "/prometheus"),
215
+ source,
216
+ };
217
+ }
218
+
219
+ const where = input.environment !== undefined ? `prometheus.profiles.${input.environment}.ruler is not declared and ` : "";
220
+ const rulerUrl = env.PROMETHEUS_RULER_URL;
221
+ if (rulerUrl) {
222
+ const kind = (env.PROMETHEUS_RULER_KIND ?? "mimir") as RulerKind;
223
+ if (!RULER_KINDS.includes(kind)) {
224
+ return { reason: "no-binding", detail: `PROMETHEUS_RULER_KIND is "${kind}", not one of ${RULER_KINDS.join(", ")}` };
225
+ }
226
+ const auth = envAuth(env, "PROMETHEUS_RULER");
227
+ return {
228
+ kind,
229
+ url: trim(rulerUrl),
230
+ ...(env.PROMETHEUS_RULER_TENANT ? { tenant: env.PROMETHEUS_RULER_TENANT } : {}),
231
+ ...(auth ? { auth } : {}),
232
+ ...(env.PROMETHEUS_RULER_NAMESPACE ? { namespace: env.PROMETHEUS_RULER_NAMESPACE } : {}),
233
+ groupNamespaces: {},
234
+ prometheusPrefix: "/prometheus",
235
+ source: "env PROMETHEUS_RULER_URL",
236
+ };
237
+ }
238
+ const promUrl = env.PROMETHEUS_URL;
239
+ if (promUrl) {
240
+ const auth = envAuth(env, "PROMETHEUS");
241
+ return { kind: "prometheus", url: trim(promUrl), ...(auth ? { auth } : {}), groupNamespaces: {}, prometheusPrefix: "", source: "env PROMETHEUS_URL" };
242
+ }
243
+ return { reason: "no-binding", detail: `${where}neither PROMETHEUS_RULER_URL nor PROMETHEUS_URL is set, so there is no ruler to read` };
244
+ }
245
+
246
+ /**
247
+ * The Alertmanager an environment reads from. Pure.
248
+ *
249
+ * 1. `prometheus.profiles.<environment>.alertmanager`.
250
+ * 2. `ALERTMANAGER_URL`, with `ALERTMANAGER_KIND` (default `alertmanager`),
251
+ * `ALERTMANAGER_TENANT` and `ALERTMANAGER_TOKEN` (or `_USER` and `_PASSWORD`).
252
+ * 3. Otherwise `no-binding`.
253
+ */
254
+ export function resolveAlertmanagerTarget(input: ResolveInput): AlertmanagerTarget | UnresolvedTarget {
255
+ const env = input.env ?? process.env;
256
+ const profile = input.environment !== undefined ? input.config?.prometheus?.profiles?.[input.environment]?.alertmanager : undefined;
257
+ if (profile) {
258
+ const source = `prometheus.profiles.${input.environment}.alertmanager`;
259
+ const auth = profileAuth(source, profile, env);
260
+ if (auth && isUnresolvedTarget(auth)) return auth;
261
+ return {
262
+ kind: profile.kind ?? "alertmanager",
263
+ url: trim(profile.url),
264
+ ...(profile.tenant ? { tenant: profile.tenant } : {}),
265
+ ...(auth ? { auth } : {}),
266
+ alertmanagerPrefix: prefixOf(profile.alertmanagerPrefix, "/alertmanager"),
267
+ source,
268
+ };
269
+ }
270
+ const url = env.ALERTMANAGER_URL;
271
+ if (!url) {
272
+ const where = input.environment !== undefined ? `prometheus.profiles.${input.environment}.alertmanager is not declared and ` : "";
273
+ return { reason: "no-binding", detail: `${where}ALERTMANAGER_URL is not set, so there is no Alertmanager to read` };
274
+ }
275
+ const kind = (env.ALERTMANAGER_KIND ?? "alertmanager") as AlertmanagerKind;
276
+ if (!ALERTMANAGER_KINDS.includes(kind)) {
277
+ return { reason: "no-binding", detail: `ALERTMANAGER_KIND is "${kind}", not one of ${ALERTMANAGER_KINDS.join(", ")}` };
278
+ }
279
+ const auth = envAuth(env, "ALERTMANAGER");
280
+ return {
281
+ kind,
282
+ url: trim(url),
283
+ ...(env.ALERTMANAGER_TENANT ? { tenant: env.ALERTMANAGER_TENANT } : {}),
284
+ ...(auth ? { auth } : {}),
285
+ alertmanagerPrefix: "/alertmanager",
286
+ source: "env ALERTMANAGER_URL",
287
+ };
288
+ }