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.
Import the files
Section titled “Import the files”From a chant project whose chant.config.ts lists prometheus:
chant import rules.yml --output srcchant import alertmanager.yml --output srcchant import prometheus.yml --output srcA 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:
chant import alertmanager.yml --lexicon prometheus --output srcThe 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):
| File | Holds |
|---|---|
rules.ts | per group, its rules as a Rule[] const and a RuleGroup |
slos.ts | an Slo(...) call for each group an Slo built |
receivers.ts | one Receiver per entry under receivers: |
time-intervals.ts | one TimeInterval per entry under time_intervals: |
routes.ts | the root Route, with its children as RouteProps objects |
inhibit-rules.ts | one InhibitRule per entry under inhibit_rules: |
settings.ts | AlertmanagerSettings for global:, templates: and tracing: |
scrape-configs.ts | one ScrapeConfig per entry under scrape_configs: |
prometheus.ts | PrometheusConfig 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:
chant build src --lexicon prometheus -o dist/rules.yml # also writes dist/alertmanager.yml and dist/prometheus.ymlRule 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.
What the generated source looks like
Section titled “What the generated source looks like”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:
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:
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.
Credentials and templates
Section titled “Credentials and templates”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.
Deprecated spellings
Section titled “Deprecated spellings”Alertmanager still reads a few older forms. The importer writes the current one and prints a warning naming each change:
- a route’s
matchandmatch_remaps becomematchersentries (severity="page",service=~"api|web"), and an inhibit rule’ssource_match*andtarget_match*becomesource_matchersandtarget_matchers; - the top-level
mute_time_intervalslist is declared asTimeIntervals, which the build writes undertime_intervals.
Alertmanager treats both forms the same way, so the rebuilt file routes and mutes the same alerts.
Fields Alertmanager doesn’t define
Section titled “Fields Alertmanager doesn’t define”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:
// 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.
What the importer cannot carry
Section titled “What the importer cannot carry”A few things have no place in the lexicon, and chant import prints a warning for each:
- a top-level
alertmanager.ymlsection other thanglobal,templates,route,inhibit_rules,receivers,time_intervals,mute_time_intervalsandtracing(for exampleevent_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:
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;chant import --from prod --lexicon prometheus --output src| Endpoint | What 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_intervalwhen the group set none), and grouplabelsandquery_offsetare not reported. - Alertmanager’s
config.originalis its loaded config written out again, with every default filled in and each receiver’s inheritedglobal:settings copied into it. The import takes those back out;--verbatimkeeps 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.
Checking the result
Section titled “Checking the result”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.