@intentius/chant-lexicon-prometheus 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.
- 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
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live export for prometheus (#3371): `chant import --from <env>` writes the
|
|
3
|
+
* environment's rule groups and Alertmanager config as chant TypeScript.
|
|
4
|
+
*
|
|
5
|
+
* All I/O is here. What is read goes through the file importer's own parse
|
|
6
|
+
* (./import/parser.ts) into the same IR, so a group read from a ruler and
|
|
7
|
+
* the same group in a rule file generate the same source through the same
|
|
8
|
+
* `PrometheusGenerator`, `Slo` recognition (./import/slo.ts) included.
|
|
9
|
+
*
|
|
10
|
+
* - Rule groups, from the environment's ruler (./api/ruler.ts). On Mimir,
|
|
11
|
+
* Cortex and Loki only the namespaces the profile declares are read
|
|
12
|
+
* (`namespace` and `groupNamespaces`, see ./config.ts); that is the
|
|
13
|
+
* ownership boundary, as a stack bounds a CloudFormation export. An ad-hoc
|
|
14
|
+
* read with no namespace declared reads every namespace of the tenant and
|
|
15
|
+
* says so, and with `owned` reads none. On a plain Prometheus the groups
|
|
16
|
+
* come from `/api/v1/rules` and are mapped back to the rule-file shape
|
|
17
|
+
* (./api/evaluated.ts), which is lossy, and the export says how.
|
|
18
|
+
* - The Alertmanager config, from `/api/v2/status` or, on Mimir and
|
|
19
|
+
* Cortex, `/api/v1/alerts` (./api/alertmanager.ts). Alertmanager's status
|
|
20
|
+
* config is its loaded config marshalled again; unless `verbatim`, what
|
|
21
|
+
* that adds is taken out (./import/alertmanager-live.ts). Secrets read
|
|
22
|
+
* `<secret>` and are named in a warning.
|
|
23
|
+
* - `selector.type` is `Prometheus::Rules::RuleGroup` (with `selector.name`
|
|
24
|
+
* a group name) or any `Prometheus::Alertmanager::*` type, which selects
|
|
25
|
+
* the whole config.
|
|
26
|
+
*
|
|
27
|
+
* Neither endpoint being bound is an error; one of them being unbound is a
|
|
28
|
+
* warning, so a project with only rule groups imports them.
|
|
29
|
+
*/
|
|
30
|
+
import { bindEndpoints } from "./api/bind.js";
|
|
31
|
+
import { EVALUATED_IMPORT_WARNING, evaluatedToRuleGroup } from "./api/evaluated.js";
|
|
32
|
+
import { declaredNamespaces, isUnresolvedTarget, namespaceOfGroup } from "./config.js";
|
|
33
|
+
import { ALERTMANAGER_RESOURCE_TYPE, RULE_FILE_RESOURCE_TYPE, loadPrometheusYaml, parseAlertmanagerDocument, parseRuleFileDocument, } from "./import/parser.js";
|
|
34
|
+
import { maskedSecretPaths, stripAlertmanagerDefaults } from "./import/alertmanager-live.js";
|
|
35
|
+
import { RULE_GROUP_TYPE } from "./rules.js";
|
|
36
|
+
const ALERTMANAGER_TYPE_PREFIX = "Prometheus::Alertmanager::";
|
|
37
|
+
function isObject(v) {
|
|
38
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
39
|
+
}
|
|
40
|
+
/** The groups of the declared namespaces (or, ad hoc, of every namespace), each with where it came from. */
|
|
41
|
+
async function rulerGroups(ruler, owned, warnings) {
|
|
42
|
+
const declared = declaredNamespaces(ruler.target);
|
|
43
|
+
if (declared.length === 0) {
|
|
44
|
+
if (owned) {
|
|
45
|
+
warnings.push(`no rule groups are imported with --owned: ${ruler.target.source} declares no ruler namespace, and the namespace is the ownership boundary`);
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
const all = await ruler.listNamespaces();
|
|
49
|
+
const names = Object.keys(all);
|
|
50
|
+
if (names.length > 0) {
|
|
51
|
+
warnings.push(`${ruler.target.source} declares no ruler namespace, so every namespace of the tenant was read (${names.join(", ")}); ` +
|
|
52
|
+
"set prometheus.profiles.<env>.ruler.namespace to bound what is imported and what an apply may change");
|
|
53
|
+
}
|
|
54
|
+
return names.map((namespace) => ({ namespace, raw: all[namespace] }));
|
|
55
|
+
}
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const namespace of declared) {
|
|
58
|
+
const raw = await ruler.readNamespace(namespace);
|
|
59
|
+
if (raw === undefined)
|
|
60
|
+
warnings.push(`ruler namespace "${namespace}" has no rule groups on ${ruler.target.url}`);
|
|
61
|
+
else
|
|
62
|
+
out.push({ namespace, raw });
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
async function exportRuleGroups(ruler, options, warnings) {
|
|
67
|
+
const name = options.selector?.name;
|
|
68
|
+
const groups = [];
|
|
69
|
+
const seen = new Map();
|
|
70
|
+
const keep = (group, from) => {
|
|
71
|
+
if (name !== undefined && group.name !== name)
|
|
72
|
+
return;
|
|
73
|
+
const first = seen.get(group.name);
|
|
74
|
+
if (first !== undefined) {
|
|
75
|
+
warnings.push(`rule group "${group.name}" is in both ${first} and ${from}; the one in ${first} is imported, since a rule file holds one group of a name`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
seen.set(group.name, from);
|
|
79
|
+
groups.push(group);
|
|
80
|
+
};
|
|
81
|
+
if (ruler.target.kind === "prometheus") {
|
|
82
|
+
const files = declaredNamespaces(ruler.target);
|
|
83
|
+
if (files.length === 0 && options.owned) {
|
|
84
|
+
warnings.push(`no rule groups are imported with --owned: ${ruler.target.source} names no rule file, and a rule file carries no ownership marker`);
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
87
|
+
const evaluated = (await ruler.evaluated()).filter((g) => files.length === 0 || files.includes(g.file));
|
|
88
|
+
if (evaluated.length > 0)
|
|
89
|
+
warnings.push(EVALUATED_IMPORT_WARNING);
|
|
90
|
+
for (const g of evaluated)
|
|
91
|
+
keep(evaluatedToRuleGroup(g), `rule file "${g.file}"`);
|
|
92
|
+
return groups;
|
|
93
|
+
}
|
|
94
|
+
for (const { namespace, raw } of await rulerGroups(ruler, options.owned, warnings)) {
|
|
95
|
+
const parsed = parseRuleFileDocument({ groups: raw });
|
|
96
|
+
for (const w of parsed.warnings)
|
|
97
|
+
warnings.push(`ruler namespace "${namespace}": ${w}`);
|
|
98
|
+
for (const g of parsed.file.groups) {
|
|
99
|
+
keep(g, `namespace "${namespace}"`);
|
|
100
|
+
if (seen.get(g.name) === `namespace "${namespace}"` && namespaceOfGroup(ruler.target, g.name) !== namespace) {
|
|
101
|
+
warnings.push(`rule group "${g.name}" was read from ruler namespace "${namespace}", which a rule file cannot say; ` +
|
|
102
|
+
`name it under prometheus.profiles.<env>.ruler.groupNamespaces so it is observed and applied there`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return groups;
|
|
107
|
+
}
|
|
108
|
+
async function exportAlertmanager(am, verbatim, warnings) {
|
|
109
|
+
const live = await am.readConfig();
|
|
110
|
+
if (live === undefined) {
|
|
111
|
+
warnings.push(`${am.target.url} has no Alertmanager config for this tenant`);
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
const loaded = loadPrometheusYaml(live.text);
|
|
115
|
+
if (!isObject(loaded))
|
|
116
|
+
throw new Error(`the Alertmanager config read from ${live.address} is not a YAML mapping`);
|
|
117
|
+
const doc = live.remarshalled && !verbatim ? stripAlertmanagerDefaults(loaded) : loaded;
|
|
118
|
+
const masked = maskedSecretPaths(doc);
|
|
119
|
+
if (masked.length > 0) {
|
|
120
|
+
warnings.push(`Alertmanager masks secrets in ${live.address}; these read "<secret>" and need their real values (a *_file path, say): ${masked.join(", ")}`);
|
|
121
|
+
}
|
|
122
|
+
if (live.templateFiles) {
|
|
123
|
+
warnings.push(`the Alertmanager's notification templates (${Object.keys(live.templateFiles).join(", ")}) are not imported; templates: names files, not their contents`);
|
|
124
|
+
}
|
|
125
|
+
const parsed = parseAlertmanagerDocument(doc);
|
|
126
|
+
warnings.push(...parsed.warnings.map((w) => `alertmanager: ${w}`));
|
|
127
|
+
return {
|
|
128
|
+
logicalId: "alertmanager",
|
|
129
|
+
type: ALERTMANAGER_RESOURCE_TYPE,
|
|
130
|
+
properties: { config: parsed.config },
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export async function exportResources(options) {
|
|
134
|
+
const { ruler, alertmanager } = await bindEndpoints(options);
|
|
135
|
+
const type = options.selector?.type;
|
|
136
|
+
const wantRules = type === undefined || type === RULE_GROUP_TYPE || type === RULE_FILE_RESOURCE_TYPE;
|
|
137
|
+
const wantAlertmanager = (type === undefined || type.startsWith(ALERTMANAGER_TYPE_PREFIX)) && options.selector?.name === undefined;
|
|
138
|
+
const unbound = [
|
|
139
|
+
...(wantRules && isUnresolvedTarget(ruler) ? [`rule groups: ${ruler.detail}`] : []),
|
|
140
|
+
...(wantAlertmanager && isUnresolvedTarget(alertmanager) ? [`Alertmanager: ${alertmanager.detail}`] : []),
|
|
141
|
+
];
|
|
142
|
+
const wanted = (wantRules ? 1 : 0) + (wantAlertmanager ? 1 : 0);
|
|
143
|
+
if (wanted > 0 && unbound.length === wanted)
|
|
144
|
+
throw new Error(`nothing to read for environment "${options.environment}": ${unbound.join("; ")}`);
|
|
145
|
+
const warnings = unbound.map((u) => `not imported, ${u}`);
|
|
146
|
+
const resources = [];
|
|
147
|
+
if (wantRules && !isUnresolvedTarget(ruler)) {
|
|
148
|
+
const groups = await exportRuleGroups(ruler, options, warnings);
|
|
149
|
+
if (groups.length > 0) {
|
|
150
|
+
resources.push({
|
|
151
|
+
logicalId: "ruleFile",
|
|
152
|
+
type: RULE_FILE_RESOURCE_TYPE,
|
|
153
|
+
properties: { file: { groups } },
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
if (wantAlertmanager && !isUnresolvedTarget(alertmanager)) {
|
|
158
|
+
const resource = await exportAlertmanager(alertmanager, options.verbatim, warnings);
|
|
159
|
+
if (resource)
|
|
160
|
+
resources.push(resource);
|
|
161
|
+
}
|
|
162
|
+
return { resources, parameters: [], warnings };
|
|
163
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alertmanager's `/api/v2/status` config, back to the shape a person writes
|
|
3
|
+
* (#3371).
|
|
4
|
+
*
|
|
5
|
+
* `config.original` is the loaded config marshalled again, not the file
|
|
6
|
+
* (`Config.String()` in Alertmanager's `config/config.go`). Against
|
|
7
|
+
* Alertmanager v0.34.1 (`PROMETHEUS_PIN.alertmanager`) that adds three
|
|
8
|
+
* kinds of thing a file did not say:
|
|
9
|
+
*
|
|
10
|
+
* - `global:` with every default of `DefaultGlobalConfig()` written out:
|
|
11
|
+
* `resolve_timeout: 5m`, the default `http_config`, `smtp_hello`,
|
|
12
|
+
* `smtp_require_tls`, `smtp_tls_config` and the integration API URLs.
|
|
13
|
+
* - every receiver integration with the settings it inherited from `global:`
|
|
14
|
+
* (`http_config`, an email config's `smarthost`, `from` and SMTP auth, a
|
|
15
|
+
* Slack config's `api_url`, a PagerDuty config's `url`, and so on), as
|
|
16
|
+
* `Config.UnmarshalYAML` copies them in.
|
|
17
|
+
* - every integration's own defaults (`DefaultEmailConfig`,
|
|
18
|
+
* `DefaultSlackConfig`, `DefaultPagerdutyConfig`, ...) and the zero values
|
|
19
|
+
* of fields marshalled without `omitempty` (`url_file: ""`,
|
|
20
|
+
* `max_alerts: 0`, `continue: false`).
|
|
21
|
+
*
|
|
22
|
+
* {@link stripAlertmanagerDefaults} takes those out, so an import writes
|
|
23
|
+
* what a person would have declared. It drops a value only where it equals
|
|
24
|
+
* the default or the global it was copied from; a value someone set to the
|
|
25
|
+
* default is indistinguishable from one they left out, and goes too, which
|
|
26
|
+
* builds to the same config. `verbatim` skips it. Secrets read `<secret>`
|
|
27
|
+
* either way (see {@link maskedSecretPaths}).
|
|
28
|
+
*/
|
|
29
|
+
function isObject(v) {
|
|
30
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
31
|
+
}
|
|
32
|
+
function canonical(v) {
|
|
33
|
+
if (Array.isArray(v))
|
|
34
|
+
return v.map(canonical);
|
|
35
|
+
if (isObject(v))
|
|
36
|
+
return Object.fromEntries(Object.keys(v).sort().map((k) => [k, canonical(v[k])]));
|
|
37
|
+
return v;
|
|
38
|
+
}
|
|
39
|
+
function same(a, b) {
|
|
40
|
+
return JSON.stringify(canonical(a)) === JSON.stringify(canonical(b));
|
|
41
|
+
}
|
|
42
|
+
/** Empty, false, zero or null: what a field marshalled without `omitempty` holds when nobody set it. */
|
|
43
|
+
function isZero(v) {
|
|
44
|
+
if (v === null || v === undefined || v === "" || v === false || v === 0)
|
|
45
|
+
return true;
|
|
46
|
+
if (Array.isArray(v))
|
|
47
|
+
return v.length === 0;
|
|
48
|
+
if (isObject(v))
|
|
49
|
+
return Object.values(v).every(isZero);
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
/** `commoncfg.DefaultHTTPClientConfig`, marshalled. */
|
|
53
|
+
const DEFAULT_HTTP_CONFIG = { follow_redirects: true, enable_http2: true };
|
|
54
|
+
function isDefaultHttpConfig(v) {
|
|
55
|
+
if (!isObject(v))
|
|
56
|
+
return false;
|
|
57
|
+
const rest = Object.fromEntries(Object.entries(v).filter(([k]) => !(k in DEFAULT_HTTP_CONFIG)));
|
|
58
|
+
return Object.entries(DEFAULT_HTTP_CONFIG).every(([k, d]) => v[k] === undefined || v[k] === d) && isZero(rest);
|
|
59
|
+
}
|
|
60
|
+
/** `DefaultGlobalConfig()` at the pin. `http_config` and `smtp_tls_config` are compared by {@link isDefaultHttpConfig} and {@link isZero}. */
|
|
61
|
+
const GLOBAL_DEFAULTS = {
|
|
62
|
+
resolve_timeout: "5m",
|
|
63
|
+
smtp_hello: "localhost",
|
|
64
|
+
smtp_require_tls: true,
|
|
65
|
+
pagerduty_url: "https://events.pagerduty.com/v2/enqueue",
|
|
66
|
+
opsgenie_api_url: "https://api.opsgenie.com/",
|
|
67
|
+
wechat_api_url: "https://qyapi.weixin.qq.com/cgi-bin/",
|
|
68
|
+
victorops_api_url: "https://alert.victorops.com/integrations/generic/20131114/alert/",
|
|
69
|
+
telegram_api_url: "https://api.telegram.org",
|
|
70
|
+
webex_api_url: "https://webexapis.com/v1/messages",
|
|
71
|
+
rocketchat_api_url: "https://open.rocket.chat/",
|
|
72
|
+
slack_app_url: "https://slack.com/api/chat.postMessage",
|
|
73
|
+
};
|
|
74
|
+
/** Fields `Config.UnmarshalYAML` copies from `global:` into each integration, integration field to global key. */
|
|
75
|
+
const INHERITED = {
|
|
76
|
+
email_configs: {
|
|
77
|
+
tls_config: "smtp_tls_config",
|
|
78
|
+
smarthost: "smtp_smarthost",
|
|
79
|
+
from: "smtp_from",
|
|
80
|
+
hello: "smtp_hello",
|
|
81
|
+
auth_username: "smtp_auth_username",
|
|
82
|
+
auth_password: "smtp_auth_password",
|
|
83
|
+
auth_password_file: "smtp_auth_password_file",
|
|
84
|
+
auth_secret: "smtp_auth_secret",
|
|
85
|
+
auth_secret_file: "smtp_auth_secret_file",
|
|
86
|
+
auth_identity: "smtp_auth_identity",
|
|
87
|
+
require_tls: "smtp_require_tls",
|
|
88
|
+
force_implicit_tls: "smtp_force_implicit_tls",
|
|
89
|
+
},
|
|
90
|
+
slack_configs: {
|
|
91
|
+
app_url: "slack_app_url",
|
|
92
|
+
app_token: "slack_app_token",
|
|
93
|
+
app_token_file: "slack_app_token_file",
|
|
94
|
+
api_url: "slack_api_url",
|
|
95
|
+
api_url_file: "slack_api_url_file",
|
|
96
|
+
},
|
|
97
|
+
pagerduty_configs: { url: "pagerduty_url" },
|
|
98
|
+
opsgenie_configs: { api_url: "opsgenie_api_url", api_key: "opsgenie_api_key", api_key_file: "opsgenie_api_key_file" },
|
|
99
|
+
wechat_configs: { api_url: "wechat_api_url", api_secret: "wechat_api_secret", api_secret_file: "wechat_api_secret_file", corp_id: "wechat_api_corp_id" },
|
|
100
|
+
victorops_configs: { api_url: "victorops_api_url", api_key: "victorops_api_key", api_key_file: "victorops_api_key_file" },
|
|
101
|
+
telegram_configs: { api_url: "telegram_api_url", bot_token: "telegram_bot_token", bot_token_file: "telegram_bot_token_file" },
|
|
102
|
+
webex_configs: { api_url: "webex_api_url" },
|
|
103
|
+
rocketchat_configs: {
|
|
104
|
+
api_url: "rocketchat_api_url",
|
|
105
|
+
token: "rocketchat_token",
|
|
106
|
+
token_file: "rocketchat_token_file",
|
|
107
|
+
token_id: "rocketchat_token_id",
|
|
108
|
+
token_id_file: "rocketchat_token_id_file",
|
|
109
|
+
},
|
|
110
|
+
mattermost_configs: { webhook_url: "mattermost_webhook_url", webhook_url_file: "mattermost_webhook_url_file" },
|
|
111
|
+
jira_configs: { api_url: "jira_api_url" },
|
|
112
|
+
};
|
|
113
|
+
/** Each integration's `Default<Kind>Config` at the pin, the fields it sets. Kinds not listed keep what they read. */
|
|
114
|
+
const INTEGRATION_DEFAULTS = {
|
|
115
|
+
webhook_configs: { send_resolved: true },
|
|
116
|
+
email_configs: { send_resolved: false, html: '{{ template "email.default.html" . }}' },
|
|
117
|
+
slack_configs: {
|
|
118
|
+
send_resolved: false,
|
|
119
|
+
color: '{{ template "slack.default.color" . }}',
|
|
120
|
+
username: '{{ template "slack.default.username" . }}',
|
|
121
|
+
title: '{{ template "slack.default.title" . }}',
|
|
122
|
+
title_link: '{{ template "slack.default.titlelink" . }}',
|
|
123
|
+
icon_emoji: '{{ template "slack.default.iconemoji" . }}',
|
|
124
|
+
icon_url: '{{ template "slack.default.iconurl" . }}',
|
|
125
|
+
pretext: '{{ template "slack.default.pretext" . }}',
|
|
126
|
+
text: '{{ template "slack.default.text" . }}',
|
|
127
|
+
fallback: '{{ template "slack.default.fallback" . }}',
|
|
128
|
+
callback_id: '{{ template "slack.default.callbackid" . }}',
|
|
129
|
+
footer: '{{ template "slack.default.footer" . }}',
|
|
130
|
+
},
|
|
131
|
+
pagerduty_configs: {
|
|
132
|
+
send_resolved: true,
|
|
133
|
+
description: '{{ template "pagerduty.default.description" .}}',
|
|
134
|
+
client: '{{ template "pagerduty.default.client" . }}',
|
|
135
|
+
client_url: '{{ template "pagerduty.default.clientURL" . }}',
|
|
136
|
+
},
|
|
137
|
+
webex_configs: { send_resolved: true, message: '{{ template "webex.default.message" . }}' },
|
|
138
|
+
rocketchat_configs: {
|
|
139
|
+
send_resolved: false,
|
|
140
|
+
color: '{{ if eq .Status "firing" }}red{{ else }}green{{ end }}',
|
|
141
|
+
emoji: '{{ template "rocketchat.default.emoji" . }}',
|
|
142
|
+
icon_url: '{{ template "rocketchat.default.iconurl" . }}',
|
|
143
|
+
text: '{{ template "rocketchat.default.text" . }}',
|
|
144
|
+
title: '{{ template "rocketchat.default.title" . }}',
|
|
145
|
+
title_link: '{{ template "rocketchat.default.titlelink" . }}',
|
|
146
|
+
},
|
|
147
|
+
wechat_configs: {
|
|
148
|
+
send_resolved: false,
|
|
149
|
+
message: '{{ template "wechat.default.message" . }}',
|
|
150
|
+
to_user: '{{ template "wechat.default.to_user" . }}',
|
|
151
|
+
to_party: '{{ template "wechat.default.to_party" . }}',
|
|
152
|
+
to_tag: '{{ template "wechat.default.to_tag" . }}',
|
|
153
|
+
agent_id: '{{ template "wechat.default.agent_id" . }}',
|
|
154
|
+
},
|
|
155
|
+
victorops_configs: {
|
|
156
|
+
send_resolved: true,
|
|
157
|
+
message_type: "CRITICAL",
|
|
158
|
+
state_message: '{{ template "victorops.default.state_message" . }}',
|
|
159
|
+
entity_display_name: '{{ template "victorops.default.entity_display_name" . }}',
|
|
160
|
+
monitoring_tool: '{{ template "victorops.default.monitoring_tool" . }}',
|
|
161
|
+
},
|
|
162
|
+
pushover_configs: {
|
|
163
|
+
send_resolved: true,
|
|
164
|
+
title: '{{ template "pushover.default.title" . }}',
|
|
165
|
+
message: '{{ template "pushover.default.message" . }}',
|
|
166
|
+
url: '{{ template "pushover.default.url" . }}',
|
|
167
|
+
priority: '{{ if eq .Status "firing" }}2{{ else }}0{{ end }}',
|
|
168
|
+
retry: "1m",
|
|
169
|
+
expire: "1h",
|
|
170
|
+
},
|
|
171
|
+
sns_configs: { send_resolved: true, subject: '{{ template "sns.default.subject" . }}', message: '{{ template "sns.default.message" . }}' },
|
|
172
|
+
};
|
|
173
|
+
/** `DefaultEmailSubject`, which an email config's `headers` gain when they set no Subject. */
|
|
174
|
+
const DEFAULT_EMAIL_SUBJECT = '{{ template "email.default.subject" . }}';
|
|
175
|
+
/** `DefaultPagerdutyDetails`, merged into every PagerDuty config's `details`. */
|
|
176
|
+
const DEFAULT_PAGERDUTY_DETAILS = {
|
|
177
|
+
firing: "{{ .Alerts.Firing | toJson }}",
|
|
178
|
+
resolved: "{{ .Alerts.Resolved | toJson }}",
|
|
179
|
+
num_firing: "{{ .Alerts.Firing | len }}",
|
|
180
|
+
num_resolved: "{{ .Alerts.Resolved | len }}",
|
|
181
|
+
};
|
|
182
|
+
function stripIntegration(kind, raw, global) {
|
|
183
|
+
const out = { ...raw };
|
|
184
|
+
const inherited = INHERITED[kind] ?? {};
|
|
185
|
+
const defaults = INTEGRATION_DEFAULTS[kind] ?? {};
|
|
186
|
+
for (const [field, globalKey] of Object.entries(inherited)) {
|
|
187
|
+
if (field in out && global[globalKey] !== undefined && same(out[field], global[globalKey]))
|
|
188
|
+
delete out[field];
|
|
189
|
+
else if (field in out && global[globalKey] === undefined && isZero(out[field]) && field !== "require_tls")
|
|
190
|
+
delete out[field];
|
|
191
|
+
}
|
|
192
|
+
if ("http_config" in out && (global.http_config !== undefined ? same(out.http_config, global.http_config) : isDefaultHttpConfig(out.http_config))) {
|
|
193
|
+
delete out.http_config;
|
|
194
|
+
}
|
|
195
|
+
for (const [field, d] of Object.entries(defaults)) {
|
|
196
|
+
if (field in out && same(out[field], d))
|
|
197
|
+
delete out[field];
|
|
198
|
+
}
|
|
199
|
+
if (kind === "email_configs" && isObject(out.headers)) {
|
|
200
|
+
const headers = { ...out.headers };
|
|
201
|
+
if (headers.Subject === DEFAULT_EMAIL_SUBJECT)
|
|
202
|
+
delete headers.Subject;
|
|
203
|
+
if (Object.keys(headers).length === 0)
|
|
204
|
+
delete out.headers;
|
|
205
|
+
else
|
|
206
|
+
out.headers = headers;
|
|
207
|
+
}
|
|
208
|
+
if (kind === "pagerduty_configs" && isObject(out.details)) {
|
|
209
|
+
const details = Object.fromEntries(Object.entries(out.details).filter(([k, v]) => !same(v, DEFAULT_PAGERDUTY_DETAILS[k])));
|
|
210
|
+
if (Object.keys(details).length === 0)
|
|
211
|
+
delete out.details;
|
|
212
|
+
else
|
|
213
|
+
out.details = details;
|
|
214
|
+
}
|
|
215
|
+
// What is left at its zero value was marshalled without omitempty. A field with a known default or a global to
|
|
216
|
+
// inherit from survived the comparisons above because it differs (`send_resolved: false` on a webhook,
|
|
217
|
+
// `require_tls: false` under a global `true`), and `send_resolved` of a kind with no known default stays.
|
|
218
|
+
for (const [k, v] of Object.entries(out)) {
|
|
219
|
+
if (k === "send_resolved" || k in defaults)
|
|
220
|
+
continue;
|
|
221
|
+
if (k in inherited && global[inherited[k]] !== undefined)
|
|
222
|
+
continue;
|
|
223
|
+
if (isZero(v))
|
|
224
|
+
delete out[k];
|
|
225
|
+
}
|
|
226
|
+
return out;
|
|
227
|
+
}
|
|
228
|
+
function stripRoute(raw) {
|
|
229
|
+
if (!isObject(raw))
|
|
230
|
+
return raw;
|
|
231
|
+
const out = {};
|
|
232
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
233
|
+
if (k === "routes" && Array.isArray(v)) {
|
|
234
|
+
if (v.length > 0)
|
|
235
|
+
out.routes = v.map(stripRoute);
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (isZero(v))
|
|
239
|
+
continue;
|
|
240
|
+
out[k] = v;
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
function stripGlobal(raw) {
|
|
245
|
+
const out = {};
|
|
246
|
+
for (const [k, v] of Object.entries(raw)) {
|
|
247
|
+
if (k in GLOBAL_DEFAULTS && same(v, GLOBAL_DEFAULTS[k]))
|
|
248
|
+
continue;
|
|
249
|
+
if (k === "http_config" && isDefaultHttpConfig(v))
|
|
250
|
+
continue;
|
|
251
|
+
if (k === "smtp_require_tls" && v === false) {
|
|
252
|
+
out[k] = v;
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
if (isZero(v))
|
|
256
|
+
continue;
|
|
257
|
+
out[k] = v;
|
|
258
|
+
}
|
|
259
|
+
return out;
|
|
260
|
+
}
|
|
261
|
+
/** The re-marshalled config with what Alertmanager added taken out (see the module comment). Pure; `doc` is not changed. */
|
|
262
|
+
export function stripAlertmanagerDefaults(doc) {
|
|
263
|
+
const global = isObject(doc.global) ? doc.global : {};
|
|
264
|
+
const out = {};
|
|
265
|
+
for (const [section, value] of Object.entries(doc)) {
|
|
266
|
+
if (section === "global") {
|
|
267
|
+
const g = stripGlobal(global);
|
|
268
|
+
if (Object.keys(g).length > 0)
|
|
269
|
+
out.global = g;
|
|
270
|
+
}
|
|
271
|
+
else if (section === "receivers" && Array.isArray(value)) {
|
|
272
|
+
out.receivers = value.map((r) => {
|
|
273
|
+
if (!isObject(r))
|
|
274
|
+
return r;
|
|
275
|
+
const receiver = {};
|
|
276
|
+
for (const [k, v] of Object.entries(r)) {
|
|
277
|
+
if (k.endsWith("_configs") && Array.isArray(v)) {
|
|
278
|
+
if (v.length > 0)
|
|
279
|
+
receiver[k] = v.map((c) => (isObject(c) ? stripIntegration(k, c, global) : c));
|
|
280
|
+
}
|
|
281
|
+
else if (k === "name" || !isZero(v))
|
|
282
|
+
receiver[k] = v;
|
|
283
|
+
}
|
|
284
|
+
return receiver;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
else if (section === "route") {
|
|
288
|
+
out.route = stripRoute(value);
|
|
289
|
+
}
|
|
290
|
+
else if (section === "inhibit_rules" && Array.isArray(value)) {
|
|
291
|
+
if (value.length > 0)
|
|
292
|
+
out.inhibit_rules = value.map((r) => (isObject(r) ? Object.fromEntries(Object.entries(r).filter(([, v]) => !isZero(v))) : r));
|
|
293
|
+
}
|
|
294
|
+
else if (!isZero(value)) {
|
|
295
|
+
out[section] = value;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return out;
|
|
299
|
+
}
|
|
300
|
+
/** The paths that read `<secret>`, the mask Alertmanager writes over every secret it marshals. */
|
|
301
|
+
export function maskedSecretPaths(doc, at = "") {
|
|
302
|
+
if (doc === "<secret>")
|
|
303
|
+
return [at];
|
|
304
|
+
if (Array.isArray(doc))
|
|
305
|
+
return doc.flatMap((v, i) => maskedSecretPaths(v, `${at}[${i}]`));
|
|
306
|
+
if (isObject(doc))
|
|
307
|
+
return Object.entries(doc).flatMap(([k, v]) => maskedSecretPaths(v, at ? `${at}.${k}` : k));
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prometheus and Alertmanager content embedded in another lexicon's
|
|
3
|
+
* resource, for `chant import`: a k8s `PrometheusRule`'s `spec.groups`, or a
|
|
4
|
+
* rule file held as text in a ConfigMap (#2962), and an `alertmanager.yml`
|
|
5
|
+
* held as text in a ConfigMap (#3031).
|
|
6
|
+
*
|
|
7
|
+
* The groups are imported exactly as `chant import rules.yml` would import
|
|
8
|
+
* them, into a directory of its own. `spec.groups` becomes the list of the
|
|
9
|
+
* declared groups (an `Slo`'s by its `rules` member), which the k8s
|
|
10
|
+
* serializer renders as the same groups; a rule file becomes
|
|
11
|
+
* `ruleFileYaml([...])`, the text the prometheus serializer writes.
|
|
12
|
+
*
|
|
13
|
+
* An `alertmanager.yml` is imported exactly as `chant import alertmanager.yml`
|
|
14
|
+
* would import it, and becomes `alertmanagerYaml([...])` over every receiver,
|
|
15
|
+
* time interval, root route, inhibit rule and settings it declares. The
|
|
16
|
+
* ConfigMap then holds the config as the serializer writes it: the same
|
|
17
|
+
* config, with Alertmanager's deprecated spellings rewritten as the
|
|
18
|
+
* standalone import rewrites them.
|
|
19
|
+
*
|
|
20
|
+
* A `prometheus.yml` held as text in a ConfigMap is imported exactly as
|
|
21
|
+
* `chant import prometheus.yml` would import it, and becomes
|
|
22
|
+
* `prometheusConfigYaml([...])` over every scrape job and the sections
|
|
23
|
+
* besides them. The ConfigMap then holds the config as the serializer
|
|
24
|
+
* writes it: scrape jobs sorted by name, sections in Prometheus's order.
|
|
25
|
+
*/
|
|
26
|
+
import { looksLikeAlertmanagerConfig, looksLikeRuleFile } from "../model.js";
|
|
27
|
+
import { looksLikePrometheusConfig } from "../config-model.js";
|
|
28
|
+
import { parsePrometheusYaml } from "./parser.js";
|
|
29
|
+
import { generateAlertmanager, generatePrometheusConfig, generateRuleFile } from "./generator.js";
|
|
30
|
+
const PACKAGE = "@intentius/chant-lexicon-prometheus";
|
|
31
|
+
export const ruleGroupsImporter = {
|
|
32
|
+
what: "Prometheus rule groups",
|
|
33
|
+
matches(content) {
|
|
34
|
+
if (!looksLikeRuleFile(content.document))
|
|
35
|
+
return false;
|
|
36
|
+
return content.select === "groups" || (content.select === undefined && typeof content.text === "string");
|
|
37
|
+
},
|
|
38
|
+
import(content) {
|
|
39
|
+
// JSON is YAML: the document goes through the same parser a rule file does.
|
|
40
|
+
const source = content.select === undefined ? content.text : JSON.stringify(content.document);
|
|
41
|
+
const parsed = parsePrometheusYaml(source);
|
|
42
|
+
if (parsed.kind !== "rules")
|
|
43
|
+
throw new Error("this is not a Prometheus rule file");
|
|
44
|
+
const { files, groups } = generateRuleFile(parsed.file);
|
|
45
|
+
const bindings = groups.map((g) => ({ from: g.path, name: g.name, ...(g.member ? { member: g.member } : {}) }));
|
|
46
|
+
return {
|
|
47
|
+
files,
|
|
48
|
+
value: content.select === undefined
|
|
49
|
+
? { bindings, shape: "list", through: { from: PACKAGE, name: "ruleFileYaml" } }
|
|
50
|
+
: { bindings, shape: "list" },
|
|
51
|
+
warnings: parsed.warnings,
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
export const alertmanagerImporter = {
|
|
56
|
+
what: "an Alertmanager config",
|
|
57
|
+
matches(content) {
|
|
58
|
+
return content.select === undefined && typeof content.text === "string" && looksLikeAlertmanagerConfig(content.document);
|
|
59
|
+
},
|
|
60
|
+
import(content) {
|
|
61
|
+
const parsed = parsePrometheusYaml(content.text);
|
|
62
|
+
if (parsed.kind !== "alertmanager")
|
|
63
|
+
throw new Error("this is not an alertmanager.yml");
|
|
64
|
+
const { files, declarations } = generateAlertmanager(parsed.config);
|
|
65
|
+
if (declarations.length === 0)
|
|
66
|
+
throw new Error("the import declared nothing");
|
|
67
|
+
return {
|
|
68
|
+
files,
|
|
69
|
+
value: {
|
|
70
|
+
bindings: declarations.map((d) => ({ from: d.path, name: d.name })),
|
|
71
|
+
shape: "list",
|
|
72
|
+
through: { from: PACKAGE, name: "alertmanagerYaml" },
|
|
73
|
+
},
|
|
74
|
+
warnings: parsed.warnings,
|
|
75
|
+
};
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
export const prometheusConfigImporter = {
|
|
79
|
+
what: "a prometheus.yml",
|
|
80
|
+
matches(content) {
|
|
81
|
+
return content.select === undefined && typeof content.text === "string" && looksLikePrometheusConfig(content.document);
|
|
82
|
+
},
|
|
83
|
+
import(content) {
|
|
84
|
+
const parsed = parsePrometheusYaml(content.text);
|
|
85
|
+
if (parsed.kind !== "prometheus")
|
|
86
|
+
throw new Error("this is not a prometheus.yml");
|
|
87
|
+
const { files, declarations } = generatePrometheusConfig(parsed.config);
|
|
88
|
+
if (declarations.length === 0)
|
|
89
|
+
throw new Error("the import declared nothing");
|
|
90
|
+
return {
|
|
91
|
+
files,
|
|
92
|
+
value: {
|
|
93
|
+
bindings: declarations.map((d) => ({ from: d.path, name: d.name })),
|
|
94
|
+
shape: "list",
|
|
95
|
+
through: { from: PACKAGE, name: "prometheusConfigYaml" },
|
|
96
|
+
},
|
|
97
|
+
warnings: parsed.warnings,
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
};
|