Skip to content

Importing Rule Files, alertmanager.yml and prometheus.yml

chant import reads a Prometheus rule file, an alertmanager.yml or a prometheus.yml and writes TypeScript that declares the same thing with this lexicon’s classes. chant build on the result gives back the file you started from, with key order and quoting normalised and groups, receivers and time intervals sorted by name.

From a chant project whose chant.config.ts lists prometheus:

Terminal window
chant import rules.yml --output src
chant import alertmanager.yml --output src
chant import prometheus.yml --output src

A file with groups: of named rule lists is a rule file; one with route: or receivers: at the top level is an Alertmanager config; one with scrape_configs:, rule_files:, alerting:, remote_write: or remote_read: at the top level, or Prometheus’s own keys under global:, is a prometheus.yml. Outside such a project, name the lexicon and detection is skipped:

Terminal window
chant import alertmanager.yml --lexicon prometheus --output src

The imports write different modules, so the second adds its files beside the first (chant notes that the directory isn’t empty, and skips any file that already exists unless you pass --force):

FileHolds
rules.tsper group, its rules as a Rule[] const and a RuleGroup
slos.tsan Slo(...) call for each group an Slo built
receivers.tsone Receiver per entry under receivers:
time-intervals.tsone TimeInterval per entry under time_intervals:
routes.tsthe root Route, with its children as RouteProps objects
inhibit-rules.tsone InhibitRule per entry under inhibit_rules:
settings.tsAlertmanagerSettings for global:, templates: and tracing:
scrape-configs.tsone ScrapeConfig per entry under scrape_configs:
prometheus.tsPrometheusConfig for every other section of a prometheus.yml

A file with more than eight groups or receivers is split into rules-1.ts, rules-2.ts and so on, the limit COR009 sets. Then build both files back:

Terminal window
chant build src --lexicon prometheus -o dist/rules.yml # also writes dist/alertmanager.yml and dist/prometheus.yml

Rule groups inside Kubernetes manifests are imported the same way when you import the manifest with the k8s lexicon. A PrometheusRule’s spec.groups becomes the list of imported RuleGroups, a rule file held in a ConfigMap becomes ruleFileYaml([...]), and an alertmanager.yml held in a ConfigMap becomes alertmanagerYaml([...]) over the receivers, time intervals, route, inhibit rules and settings it declares, and a prometheus.yml held in a ConfigMap becomes prometheusConfigYaml([...]) over the scrape jobs and the sections besides them. See the k8s lexicon’s Importing Existing YAML.

Rules stay plain objects, the same shape ruleGroupConfig emits, so they can be moved into a function later the way the rules-from-data example does. A multi-line expr: | becomes a template literal:

rules.ts
import { type LabelSet, type Rule, RuleGroup } from "@intentius/chant-lexicon-prometheus";
const nodeLabels: LabelSet = { team: "platform" };
const nodeRules: Rule[] = [
{
record: "instance:node_cpu_utilisation:rate5m",
expr: `1 - avg without (cpu, mode) (
rate(node_cpu_seconds_total{mode="idle"}[5m])
)
`,
},
{
alert: "NodeHighCpu",
expr: "instance:node_cpu_utilisation:rate5m > 0.9",
for: "15m",
labels: { severity: "ticket" },
annotations: { summary: "{{ $labels.instance }} CPU above 90%" },
},
];
const node = new RuleGroup({ name: "node", interval: "1m", labels: nodeLabels, rules: nodeRules });
export { node };

Routes name their receivers and time intervals by the declared variable, imported from the module that declares it, so TypeScript checks the reference. A name nothing in the file declares stays a string, and PROM201 or PROM204 reports it:

routes.ts
import { Route, type RouteProps } from "@intentius/chant-lexicon-prometheus";
import { defaultReceiver, oncall, tickets } from "./receivers";
import { outsideOfficeHours } from "./time-intervals";
const rootChildren: RouteProps[] = [
{ receiver: oncall, matchers: ['severity="page"'], repeat_interval: "1h" },
{ receiver: tickets, matchers: ['severity="ticket"'], mute_time_intervals: [outsideOfficeHours] },
];
const root = new Route({ receiver: defaultReceiver, group_by: ["alertname"], routes: rootChildren });
export { root };

A receiver or group whose name isn’t an identifier, or is a reserved word, gets a suffix: default becomes defaultReceiver.

Values are carried exactly as written. *_file fields (routing_key_file, api_url_file, smtp_auth_password_file) stay paths, and the Go templates in annotations, route labels and notifier fields stay strings, for Prometheus and Alertmanager to expand. A credential written inline is imported as found, and chant lint reports it under PROM001 with the *_file field to use instead.

A rule group that Slo() built comes back as the Slo call, not as its recording and alerting rules. The importer reads the objective, window, SLI and burn-rate pairs back out of the rules, and keeps the Slo only when calling it builds the same group, rule for rule. A group that was edited after it was built fails that check and is imported as a RuleGroup.

Alertmanager still reads a few older forms. The importer writes the current one and prints a warning naming each change:

  • a route’s match and match_re maps become matchers entries (severity="page", service=~"api|web"), and an inhibit rule’s source_match* and target_match* become source_matchers and target_matchers;
  • the top-level mute_time_intervals list is declared as TimeIntervals, which the build writes under time_intervals.

Alertmanager treats both forms the same way, so the rebuilt file routes and mutes the same alerts.

Every receiver integration and every global: field of Alertmanager v0.34.1 is typed, so an imported receiver’s lists come back as typed consts (OpsGenieConfig[], MSTeamsV2Config[], …). A key Alertmanager doesn’t define, in a receiver or in global:, is carried as data in a const without a type, spread into the declaration, with a comment naming it, and chant import prints a warning:

receivers.ts
// Receiver "oncall": pigeon_configs is not a field Alertmanager v0.34.1 defines, so it is carried as
// data, untyped.
const oncallUntyped = { pigeon_configs: [{ loft: "roof" }] };
const oncall = new Receiver({ name: "oncall", pagerduty_configs: oncallPagerduty, ...oncallUntyped });

That is usually an integration from a newer Alertmanager, or a typo, which amtool check-config would reject. A field inside an integration that its type doesn’t list is kept in place instead. It still builds back to the same YAML, but tsc reports it at the const that holds it. Fix the spelling, or mark the line with // @ts-expect-error and a reason.

A few things have no place in the lexicon, and chant import prints a warning for each:

  • a top-level alertmanager.yml section other than global, templates, route, inhibit_rules, receivers, time_intervals, mute_time_intervals and tracing (for example event_recorder);
  • a key in a rule group, rule, route or inhibit rule that Prometheus or Alertmanager doesn’t define, which neither would load anyway;
  • comments, YAML anchors and merge keys (the merged values are imported, each copy written out).

A Prometheus Operator PrometheusRule is a Kubernetes manifest, not a rule file, and imports through the k8s lexicon.

Import from a running ruler or Alertmanager

Section titled “Import from a running ruler or Alertmanager”

chant import --from <env> reads the rule groups and the Alertmanager config an environment runs, and writes them through the same importer, so a group read from a ruler and the same group in a rule file give the same source, Slo declarations included. Name the endpoints per environment in chant.config.ts:

chant.config.ts
import type { ChantConfig } from "@intentius/chant/config";
import "@intentius/chant-lexicon-prometheus";
export default {
lexicons: ["prometheus"],
prometheus: {
profiles: {
prod: {
ruler: {
kind: "mimir", // or "cortex", "loki", "prometheus"
url: "https://mimir.example.com",
tenant: "shop", // sent as X-Scope-OrgID
namespace: "shop", // where this project's groups live
groupNamespaces: { infra: "platform" },
token: { env: "MIMIR_TOKEN" },
},
alertmanager: { kind: "mimir", url: "https://mimir.example.com", tenant: "shop", token: { env: "MIMIR_TOKEN" } },
},
},
},
} satisfies ChantConfig;
Terminal window
chant import --from prod --lexicon prometheus --output src
EndpointWhat is read
Mimir ruler/prometheus/config/v1/rules/<namespace> (another prefix with prometheusPrefix)
Cortex ruler/api/v1/rules/<namespace>
Loki ruler/loki/api/v1/rules/<namespace>
Plain Prometheus/api/v1/rules
Alertmanager/api/v2/status, the config.original it reports
Mimir or Cortex Alertmanager/api/v1/alerts, the config the tenant uploaded

A rule group carries no ownership marker, so the namespaces a profile names are the boundary: namespace and the values of groupNamespaces are read, and no other namespace is. A rule file cannot say which namespace a group belongs in, so a group that lives outside namespace needs a groupNamespaces entry, and the import warns about any that has none. With no profile, PROMETHEUS_RULER_URL (with PROMETHEUS_RULER_KIND, PROMETHEUS_RULER_TENANT, PROMETHEUS_RULER_NAMESPACE and PROMETHEUS_RULER_TOKEN), PROMETHEUS_URL and ALERTMANAGER_URL bind instead; a ruler read with no namespace named reads every namespace of the tenant and lists them in a warning, and --owned then reads none.

Some of what comes back differs from what was written:

  • A plain Prometheus serves rules as it evaluates them. Expressions come back as PromQL prints them, a group’s interval is the one it runs on (the global evaluation_interval when the group set none), and group labels and query_offset are not reported.
  • Alertmanager’s config.original is its loaded config written out again, with every default filled in and each receiver’s inherited global: settings copied into it. The import takes those back out; --verbatim keeps them. Secrets come back as <secret>, and a warning lists each place one needs its real value.
  • Mimir’s uploaded notification templates are not imported. templates: names files, and the import warns with their names.

chant lifecycle diff --live and the other live reads use the same profile. Each declared RuleGroup is read in its namespace and reports health (ok, err or unknown), the first rule error, and how many of its alerts are firing or pending, from /api/v1/rules. Receivers and time intervals are found by name in the loaded Alertmanager config, routes by receiver and matchers, and inhibit rules by matchers.

The lexicon’s round-trip tests (src/import/roundtrip.test.ts) hold every example’s built files, Slo() output, and samples from the Prometheus documentation and Alertmanager’s example configs to the same standard: imported, built, compared with the original, linted, and passed through promtool check rules and amtool check-config when those are installed. To check your own import the same way, see Checking with promtool and amtool.