From 6674dc39bcce1bcdf9320d5c528634aae05bbca7 Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Tue, 28 Jul 2026 10:03:55 +0200 Subject: [PATCH 1/5] Add distribution-agnostic alert-routing-discovery library This has been factored out from https://github.com/appuio/component-openshift4-monitoring/. --- alert-routing-discovery.libsonnet | 135 ++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 alert-routing-discovery.libsonnet diff --git a/alert-routing-discovery.libsonnet b/alert-routing-discovery.libsonnet new file mode 100644 index 0000000..ce3ad29 --- /dev/null +++ b/alert-routing-discovery.libsonnet @@ -0,0 +1,135 @@ +local com = import 'lib/commodore.libjsonnet'; +local syn_teams = import 'syn-teams.libsonnet'; + +local inv = com.inventory(); +local params = inv.parameters; + +// discoverNS returns the namespace for the given application. +// It looks into the follwing places: +// - params..namespace +// - params..namespace.name +// It does respect aliased applications and looks in the instance first and then in the base application. +local discoverNS = function(app, monitoring_instance) + local f = function(k) + if std.objectHas(params, k) then + local p = params[k]; + if !std.isObject(p) then + std.trace('[WARN] parameters for component instance "%s" not an object!' % k, null) + else if std.isObject(p) && std.objectHas(p, 'namespace') then + if std.isString(p.namespace) then + p.namespace + else if std.isObject(p.namespace) && std.objectHas(p.namespace, 'name') && std.isString(p.namespace.name) then + p.namespace.name; + + local ks = syn_teams.appKeys(app); + local aliased = f(ks[0]); + local ns = + if aliased != null then + aliased + else if std.length(ks) == 2 then + f(ks[1]); + + // We tend to use `namespace: ${_instance}` for components where we deploy + // each instance in a separate namespace (e.g. component-vault, + // component-openshift4-operators). However, because we read the instance + // namespaces from openshift4-monitoring's parameters, `${_instance}` is + // resolved to `openshift4-monitoring` and not to the component instance + // name. + // + // We override the discovered namespace here if we discover a namespace that + // contains `openshift4-monitoring` for any app other than + // `openshift4-monitoring` itself. + if + ns != null && + app != monitoring_instance && + std.length(std.findSubstr(monitoring_instance, ns)) > 0 + then + std.trace( + 'overriding namespace autodiscovery for `%s` (discovered namespace: %s)' % [ app, ns ], + std.strReplace(ns, monitoring_instance, std.strReplace(ks[0], '_', '-')) + ) + else + ns; + +local ownerOrFallbackTeam(fallback_team) = + if std.objectHas(params, 'syn') && std.objectHas(params.syn, 'owner') then + params.syn.owner + else + fallback_team; + +// teamToNS is a map from a team to namespaces. +// The inner `std.prune()` is to drop `null` entries from a list that contains +// a mix of null and non-null entries. The outer `std.prune()` drops teams +// for which we haven't discovered any namespaces from the resulting object. +local teamToNS(monitoring_instance) = std.prune(std.mapWithKey( + function(_, a) std.uniq(std.sort(std.prune(a))), + std.foldl( + function(prev, app) + local instance = syn_teams.appKeys(app, true)[0]; + local team = syn_teams.teamForApplication(instance); + prev { [team]+: [ discoverNS(app, monitoring_instance) ] }, + inv.applications, + {} + ) +)); + +// teamBasedRouting contains discovered routes for teams. +// The routes are set up with `continue: true` so we can route to multiple teams. +// The last route catches all alerts already routed to a team. +local teamBasedRouting(adParams, nullReceiver, monitoring_instance) = + local teamNSMap = teamToNS(monitoring_instance); + std.map( + function(k) { + receiver: adParams.team_receiver_format % k, + matchers: adParams.additional_alert_matchers + [ + 'namespace =~ "%s"' % std.join('|', teamNSMap[k]), + ], + continue: true, + }, + std.objectFields(teamNSMap) + ) + [ { + // catch all alerts already routed to a team + receiver: nullReceiver, + matchers: adParams.additional_alert_matchers + [ + 'namespace =~ "%s"' % std.join('|', std.foldl(function(prev, nss) prev + nss, std.objectValues(teamNSMap), [])), + ], + continue: false, + } ]; + +local alertmanagerConfig(adParams, amConfig, nullReceiver, fallback_team, monitoring_instance) = + local routes = std.get(amConfig.route, 'routes', []); + local finalRoute = + if ownerOrFallbackTeam(fallback_team) != null then + [ { + receiver: adParams.team_receiver_format % ownerOrFallbackTeam(fallback_team), + } ] + else + [ { receiver: nullReceiver } ]; + std.prune(amConfig) { + receivers+: [ { name: nullReceiver } ], + route+: { + routes: + adParams.prepend_routes + + teamBasedRouting(adParams, nullReceiver, monitoring_instance) + + adParams.append_routes + + routes + + finalRoute, + }, + }; + +local debugConfigMapData = function(adParams, amConfig, nullReceiver, fallback_team, monitoring_instance) + { + local discoveredNamespaces = std.foldl(function(prev, app) prev { [app]: discoverNS(app, monitoring_instance) }, inv.applications, {}), + local discoveredTeams = std.foldl(function(prev, app) prev { [app]: syn_teams.teamForApplication(syn_teams.appKeys(app, true)[0]) }, inv.applications, {}), + applications: std.manifestJsonMinified(inv.applications), + discovered_namespaces: std.manifestYamlDoc(discoveredNamespaces), + apps_without_namespaces: std.manifestYamlDoc(std.foldl(function(prev, app) if discoveredNamespaces[app] == null then prev + [ app ] else prev, std.objectFields(discoveredNamespaces), [])), + discovered_teams: std.manifestYamlDoc(discoveredTeams), + proposed_routes: std.manifestYamlDoc(teamBasedRouting(adParams, nullReceiver, monitoring_instance)), + alertmanager: std.manifestYamlDoc(alertmanagerConfig(adParams, amConfig, nullReceiver, fallback_team, monitoring_instance)), + }; + +{ + debugConfigMapData: debugConfigMapData, + alertmanagerConfig: alertmanagerConfig, +} From 22fc045102add4d30f8f065a28d40222d79dea8b Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Tue, 28 Jul 2026 14:42:58 +0200 Subject: [PATCH 2/5] Add test case for alert-routing-discovery.libsonnet The inventory for the test case is copied (and slightly adapted) from component-openshift4-monitoring's `team-routing` test case. --- tests/alert-routing-discovery.jsonnet | 50 ++++++ tests/alert-routing-discovery.yaml | 201 +++++++++++++++++++++++ tests/golden/alert-routing-discovery.yml | 72 ++++++++ 3 files changed, 323 insertions(+) create mode 100644 tests/alert-routing-discovery.jsonnet create mode 100644 tests/alert-routing-discovery.yaml create mode 100644 tests/golden/alert-routing-discovery.yml diff --git a/tests/alert-routing-discovery.jsonnet b/tests/alert-routing-discovery.jsonnet new file mode 100644 index 0000000..e06a257 --- /dev/null +++ b/tests/alert-routing-discovery.jsonnet @@ -0,0 +1,50 @@ +local ard = import 'alert-routing-discovery.libsonnet'; +local com = import 'lib/commodore.libjsonnet'; + +local inv = com.inventory(); + +local expected = inv.expected; +local monitoring_instance = inv.parameters._instance; + +local nullR = '__null'; +local fallback_team = inv.parameters.openshift4_monitoring.fallback_team; + +local adParams = inv.parameters.openshift4_monitoring.alertManagerAutoDiscovery; +local amConfig = inv.parameters.openshift4_monitoring.alertManagerConfig; + +local debugData = + local rendered = ard.debugConfigMapData( + adParams, amConfig, nullR, fallback_team, monitoring_instance + ); + local desired = { + applications: std.manifestJsonMinified(inv.applications), + discovered_namespaces: std.manifestYamlDoc(expected.discovered_namespaces), + apps_without_namespaces: std.manifestYamlDoc(expected.apps_without_namespaces), + discovered_teams: std.manifestYamlDoc(expected.discovered_teams), + proposed_routes: std.manifestYamlDoc(expected.proposed_routes), + alertmanager: std.manifestYamlDoc(expected.alertmanagerConfig), + }; + if rendered != desired then + error + 'Error rendering debug config map data for alert routing discovery\n' + + 'Expected: %s\n' % [ desired ] + + 'Got: %s\n' % [ rendered ] + else + rendered; + +local alertmanagerConfig = + local rendered = ard.alertmanagerConfig( + adParams, amConfig, nullR, fallback_team, monitoring_instance + ); + if rendered != expected.alertmanagerConfig then + error + 'Error rendering alertmanager config for alert routing discovery\n' + + 'Expected: %s\n' % [ expected.alertmanagerConfig ] + + 'Got: %s\n' % [ rendered ] + else + rendered; + +{ + debugData: debugData, + alertmanagerConfig: alertmanagerConfig, +} diff --git a/tests/alert-routing-discovery.yaml b/tests/alert-routing-discovery.yaml new file mode 100644 index 0000000..366f51b --- /dev/null +++ b/tests/alert-routing-discovery.yaml @@ -0,0 +1,201 @@ +# NOTE(sg): This test inventory is copied from openshift4-monitoring. But the +# library under test (alert-routing-discovery.libsonnet) should be +# distribution agnostic. +applications: + - espejote + - non-existing + - no-ns + - ns-string + - ns-object + - base as ns-in-base + - base as ns-overridden + - non-existing as still-non-existing + - same-ns-1 + - same-ns-2 + - openshift4-monitoring + - no-ns as no-ns-team + - no-ns as no-ns-team2 + - instance-ns + +parameters: + _instance: &instance openshift4-monitoring + + syn: + owner: clumsy-donkeys + teams: + chubby-cockroaches: + instances: + - ns-in-base + - ns-overridden + lovable-lizards: + instances: + - ns-object + - same-ns-1 + - same-ns-2 + - ns-in-base + - ~ns-in-base + - no-ns-team2 + sleepy-badgers: + instances: + - no-ns-team + + espejote: + namespace: syn-espejote + + openshift4_monitoring: + namespace: openshift-monitoring + fallback_team: null + alertManagerConfig: + receivers: + - name: foo + opsgenie_configs: + - source: null + route: + routes: + - receiver: other + matchers: + - other = "true" + alertManagerAutoDiscovery: + append_routes: [] + debug_config_map: true + team_receiver_format: team_default_%s + additional_alert_matchers: + - 'syn_team = ""' + prepend_routes: + - matchers: + - alertname = Watchdog + repeat_interval: 60s + receiver: heartbeat + + rules: + my-rules: + "alert:MyAlert": + expr: 'vector(1)' + labels: + syn_team: yet_another_team + "alert:NoLabels": + expr: 'vector(1)' + + no_ns: {} + + ns_string: + namespace: "ns-string" + + ns_object: + namespace: + name: "ns-object" + + base: + namespace: base + + ns_in_base: {} + + ns_overridden: + namespace: overridden + + same_ns_1: + namespace: same-ns + + same_ns_2: + namespace: same-ns + + instance_ns: + # NOTE: we essentially mock `namespace: ${_instance}` here. + # For a real inventory, this will resolve to `instance-ns` or an instance + # when rendering the actual namespace. + namespace: *instance + +expected: + discovered_namespaces: + "base as ns-in-base": base + "base as ns-overridden": "overridden" + "espejote": syn-espejote + "instance-ns": "instance-ns" + "no-ns": null + "no-ns as no-ns-team": null + "no-ns as no-ns-team2": null + "non-existing": null + "non-existing as still-non-existing": null + "ns-object": "ns-object" + "ns-string": "ns-string" + "openshift4-monitoring": openshift-monitoring + "same-ns-1": "same-ns" + "same-ns-2": "same-ns" + apps_without_namespaces: + - no-ns + - no-ns as no-ns-team + - no-ns as no-ns-team2 + - non-existing + - non-existing as still-non-existing + discovered_teams: + "base as ns-in-base": "chubby-cockroaches" + "base as ns-overridden": "chubby-cockroaches" + "espejote": "clumsy-donkeys" + "instance-ns": "clumsy-donkeys" + "no-ns": "clumsy-donkeys" + "no-ns as no-ns-team": "sleepy-badgers" + "no-ns as no-ns-team2": "lovable-lizards" + "non-existing": "clumsy-donkeys" + "non-existing as still-non-existing": "clumsy-donkeys" + "ns-object": "lovable-lizards" + "ns-string": "clumsy-donkeys" + "openshift4-monitoring": "clumsy-donkeys" + "same-ns-1": "lovable-lizards" + "same-ns-2": "lovable-lizards" + + proposed_routes: + - continue: true + matchers: + - "syn_team = \"\"" + - "namespace =~ \"base|overridden\"" + receiver: "team_default_chubby-cockroaches" + - continue: true + matchers: + - "syn_team = \"\"" + - "namespace =~ \"instance-ns|ns-string|openshift-monitoring|syn-espejote\"" + receiver: "team_default_clumsy-donkeys" + - continue: true + matchers: + - "syn_team = \"\"" + - "namespace =~ \"ns-object|same-ns\"" + receiver: "team_default_lovable-lizards" + - continue: false + matchers: + - "syn_team = \"\"" + - "namespace =~ \"base|overridden|instance-ns|ns-string|openshift-monitoring|syn-espejote|ns-object|same-ns\"" + receiver: "__null" + + alertmanagerConfig: + receivers: + - name: "foo" + - name: "__null" + route: + routes: + - matchers: + - "alertname = Watchdog" + receiver: "heartbeat" + repeat_interval: "60s" + - continue: true + matchers: + - "syn_team = \"\"" + - "namespace =~ \"base|overridden\"" + receiver: "team_default_chubby-cockroaches" + - continue: true + matchers: + - "syn_team = \"\"" + - "namespace =~ \"instance-ns|ns-string|openshift-monitoring|syn-espejote\"" + receiver: "team_default_clumsy-donkeys" + - continue: true + matchers: + - "syn_team = \"\"" + - "namespace =~ \"ns-object|same-ns\"" + receiver: "team_default_lovable-lizards" + - continue: false + matchers: + - "syn_team = \"\"" + - "namespace =~ \"base|overridden|instance-ns|ns-string|openshift-monitoring|syn-espejote|ns-object|same-ns\"" + receiver: "__null" + - matchers: + - "other = \"true\"" + receiver: "other" + - receiver: "team_default_clumsy-donkeys" diff --git a/tests/golden/alert-routing-discovery.yml b/tests/golden/alert-routing-discovery.yml new file mode 100644 index 0000000..1a56037 --- /dev/null +++ b/tests/golden/alert-routing-discovery.yml @@ -0,0 +1,72 @@ +{ + "alertmanagerConfig": { + "receivers": [ + { + "name": "foo" + }, + { + "name": "__null" + } + ], + "route": { + "routes": [ + { + "matchers": [ + "alertname = Watchdog" + ], + "receiver": "heartbeat", + "repeat_interval": "60s" + }, + { + "continue": true, + "matchers": [ + "syn_team = \"\"", + "namespace =~ \"base|overridden\"" + ], + "receiver": "team_default_chubby-cockroaches" + }, + { + "continue": true, + "matchers": [ + "syn_team = \"\"", + "namespace =~ \"instance-ns|ns-string|openshift-monitoring|syn-espejote\"" + ], + "receiver": "team_default_clumsy-donkeys" + }, + { + "continue": true, + "matchers": [ + "syn_team = \"\"", + "namespace =~ \"ns-object|same-ns\"" + ], + "receiver": "team_default_lovable-lizards" + }, + { + "continue": false, + "matchers": [ + "syn_team = \"\"", + "namespace =~ \"base|overridden|instance-ns|ns-string|openshift-monitoring|syn-espejote|ns-object|same-ns\"" + ], + "receiver": "__null" + }, + { + "matchers": [ + "other = \"true\"" + ], + "receiver": "other" + }, + { + "receiver": "team_default_clumsy-donkeys" + } + ] + } + }, + "debugData": { + "alertmanager": "\"receivers\":\n- \"name\": \"foo\"\n- \"name\": \"__null\"\n\"route\":\n \"routes\":\n - \"matchers\":\n - \"alertname = Watchdog\"\n \"receiver\": \"heartbeat\"\n \"repeat_interval\": \"60s\"\n - \"continue\": true\n \"matchers\":\n - \"syn_team = \\\"\\\"\"\n - \"namespace =~ \\\"base|overridden\\\"\"\n \"receiver\": \"team_default_chubby-cockroaches\"\n - \"continue\": true\n \"matchers\":\n - \"syn_team = \\\"\\\"\"\n - \"namespace =~ \\\"instance-ns|ns-string|openshift-monitoring|syn-espejote\\\"\"\n \"receiver\": \"team_default_clumsy-donkeys\"\n - \"continue\": true\n \"matchers\":\n - \"syn_team = \\\"\\\"\"\n - \"namespace =~ \\\"ns-object|same-ns\\\"\"\n \"receiver\": \"team_default_lovable-lizards\"\n - \"continue\": false\n \"matchers\":\n - \"syn_team = \\\"\\\"\"\n - \"namespace =~ \\\"base|overridden|instance-ns|ns-string|openshift-monitoring|syn-espejote|ns-object|same-ns\\\"\"\n \"receiver\": \"__null\"\n - \"matchers\":\n - \"other = \\\"true\\\"\"\n \"receiver\": \"other\"\n - \"receiver\": \"team_default_clumsy-donkeys\"", + "applications": "[\"espejote\",\"non-existing\",\"no-ns\",\"ns-string\",\"ns-object\",\"base as ns-in-base\",\"base as ns-overridden\",\"non-existing as still-non-existing\",\"same-ns-1\",\"same-ns-2\",\"openshift4-monitoring\",\"no-ns as no-ns-team\",\"no-ns as no-ns-team2\",\"instance-ns\"]", + "apps_without_namespaces": "- \"no-ns\"\n- \"no-ns as no-ns-team\"\n- \"no-ns as no-ns-team2\"\n- \"non-existing\"\n- \"non-existing as still-non-existing\"", + "discovered_namespaces": "\"base as ns-in-base\": \"base\"\n\"base as ns-overridden\": \"overridden\"\n\"espejote\": \"syn-espejote\"\n\"instance-ns\": \"instance-ns\"\n\"no-ns\": null\n\"no-ns as no-ns-team\": null\n\"no-ns as no-ns-team2\": null\n\"non-existing\": null\n\"non-existing as still-non-existing\": null\n\"ns-object\": \"ns-object\"\n\"ns-string\": \"ns-string\"\n\"openshift4-monitoring\": \"openshift-monitoring\"\n\"same-ns-1\": \"same-ns\"\n\"same-ns-2\": \"same-ns\"", + "discovered_teams": "\"base as ns-in-base\": \"chubby-cockroaches\"\n\"base as ns-overridden\": \"chubby-cockroaches\"\n\"espejote\": \"clumsy-donkeys\"\n\"instance-ns\": \"clumsy-donkeys\"\n\"no-ns\": \"clumsy-donkeys\"\n\"no-ns as no-ns-team\": \"sleepy-badgers\"\n\"no-ns as no-ns-team2\": \"lovable-lizards\"\n\"non-existing\": \"clumsy-donkeys\"\n\"non-existing as still-non-existing\": \"clumsy-donkeys\"\n\"ns-object\": \"lovable-lizards\"\n\"ns-string\": \"clumsy-donkeys\"\n\"openshift4-monitoring\": \"clumsy-donkeys\"\n\"same-ns-1\": \"lovable-lizards\"\n\"same-ns-2\": \"lovable-lizards\"", + "proposed_routes": "- \"continue\": true\n \"matchers\":\n - \"syn_team = \\\"\\\"\"\n - \"namespace =~ \\\"base|overridden\\\"\"\n \"receiver\": \"team_default_chubby-cockroaches\"\n- \"continue\": true\n \"matchers\":\n - \"syn_team = \\\"\\\"\"\n - \"namespace =~ \\\"instance-ns|ns-string|openshift-monitoring|syn-espejote\\\"\"\n \"receiver\": \"team_default_clumsy-donkeys\"\n- \"continue\": true\n \"matchers\":\n - \"syn_team = \\\"\\\"\"\n - \"namespace =~ \\\"ns-object|same-ns\\\"\"\n \"receiver\": \"team_default_lovable-lizards\"\n- \"continue\": false\n \"matchers\":\n - \"syn_team = \\\"\\\"\"\n - \"namespace =~ \\\"base|overridden|instance-ns|ns-string|openshift-monitoring|syn-espejote|ns-object|same-ns\\\"\"\n \"receiver\": \"__null\"" + } +} From 466e4435935e5c953e5e774682d4424e6c76faff Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Tue, 28 Jul 2026 14:50:05 +0200 Subject: [PATCH 3/5] Remove `monitoring_instance` from alert-routing-discovery API We can always use `parameters._instance` to do the autodiscovery patching for instantiated components which use `${_instance}` to define their namespace. --- alert-routing-discovery.libsonnet | 47 ++++++++++++++------------- tests/alert-routing-discovery.jsonnet | 5 ++- 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/alert-routing-discovery.libsonnet b/alert-routing-discovery.libsonnet index ce3ad29..41cdcc6 100644 --- a/alert-routing-discovery.libsonnet +++ b/alert-routing-discovery.libsonnet @@ -9,7 +9,7 @@ local params = inv.parameters; // - params..namespace // - params..namespace.name // It does respect aliased applications and looks in the instance first and then in the base application. -local discoverNS = function(app, monitoring_instance) +local discoverNS = function(app) local f = function(k) if std.objectHas(params, k) then local p = params[k]; @@ -32,21 +32,25 @@ local discoverNS = function(app, monitoring_instance) // We tend to use `namespace: ${_instance}` for components where we deploy // each instance in a separate namespace (e.g. component-vault, // component-openshift4-operators). However, because we read the instance - // namespaces from openshift4-monitoring's parameters, `${_instance}` is - // resolved to `openshift4-monitoring` and not to the component instance - // name. + // namespaces from the monitoring component's parameters, `${_instance}` is + // resolved to that component's instance name and not to the actual + // component instance name. // // We override the discovered namespace here if we discover a namespace that - // contains `openshift4-monitoring` for any app other than - // `openshift4-monitoring` itself. + // contains `parameters._instance` for any app other than the app using this + // library. + // + // It's safe to use `parameters._instance` here, since that's the value that + // will be substituted for instantiated components that use `${_instance}` + // in their namespace definition. if ns != null && - app != monitoring_instance && - std.length(std.findSubstr(monitoring_instance, ns)) > 0 + app != params._instance && + std.length(std.findSubstr(params._instance, ns)) > 0 then std.trace( 'overriding namespace autodiscovery for `%s` (discovered namespace: %s)' % [ app, ns ], - std.strReplace(ns, monitoring_instance, std.strReplace(ks[0], '_', '-')) + std.strReplace(ns, params._instance, std.strReplace(ks[0], '_', '-')) ) else ns; @@ -61,13 +65,13 @@ local ownerOrFallbackTeam(fallback_team) = // The inner `std.prune()` is to drop `null` entries from a list that contains // a mix of null and non-null entries. The outer `std.prune()` drops teams // for which we haven't discovered any namespaces from the resulting object. -local teamToNS(monitoring_instance) = std.prune(std.mapWithKey( +local teamToNS = std.prune(std.mapWithKey( function(_, a) std.uniq(std.sort(std.prune(a))), std.foldl( function(prev, app) local instance = syn_teams.appKeys(app, true)[0]; local team = syn_teams.teamForApplication(instance); - prev { [team]+: [ discoverNS(app, monitoring_instance) ] }, + prev { [team]+: [ discoverNS(app) ] }, inv.applications, {} ) @@ -76,27 +80,26 @@ local teamToNS(monitoring_instance) = std.prune(std.mapWithKey( // teamBasedRouting contains discovered routes for teams. // The routes are set up with `continue: true` so we can route to multiple teams. // The last route catches all alerts already routed to a team. -local teamBasedRouting(adParams, nullReceiver, monitoring_instance) = - local teamNSMap = teamToNS(monitoring_instance); +local teamBasedRouting(adParams, nullReceiver) = std.map( function(k) { receiver: adParams.team_receiver_format % k, matchers: adParams.additional_alert_matchers + [ - 'namespace =~ "%s"' % std.join('|', teamNSMap[k]), + 'namespace =~ "%s"' % std.join('|', teamToNS[k]), ], continue: true, }, - std.objectFields(teamNSMap) + std.objectFields(teamToNS) ) + [ { // catch all alerts already routed to a team receiver: nullReceiver, matchers: adParams.additional_alert_matchers + [ - 'namespace =~ "%s"' % std.join('|', std.foldl(function(prev, nss) prev + nss, std.objectValues(teamNSMap), [])), + 'namespace =~ "%s"' % std.join('|', std.foldl(function(prev, nss) prev + nss, std.objectValues(teamToNS), [])), ], continue: false, } ]; -local alertmanagerConfig(adParams, amConfig, nullReceiver, fallback_team, monitoring_instance) = +local alertmanagerConfig(adParams, amConfig, nullReceiver, fallback_team) = local routes = std.get(amConfig.route, 'routes', []); local finalRoute = if ownerOrFallbackTeam(fallback_team) != null then @@ -110,23 +113,23 @@ local alertmanagerConfig(adParams, amConfig, nullReceiver, fallback_team, monito route+: { routes: adParams.prepend_routes - + teamBasedRouting(adParams, nullReceiver, monitoring_instance) + + teamBasedRouting(adParams, nullReceiver) + adParams.append_routes + routes + finalRoute, }, }; -local debugConfigMapData = function(adParams, amConfig, nullReceiver, fallback_team, monitoring_instance) +local debugConfigMapData = function(adParams, amConfig, nullReceiver, fallback_team) { - local discoveredNamespaces = std.foldl(function(prev, app) prev { [app]: discoverNS(app, monitoring_instance) }, inv.applications, {}), + local discoveredNamespaces = std.foldl(function(prev, app) prev { [app]: discoverNS(app) }, inv.applications, {}), local discoveredTeams = std.foldl(function(prev, app) prev { [app]: syn_teams.teamForApplication(syn_teams.appKeys(app, true)[0]) }, inv.applications, {}), applications: std.manifestJsonMinified(inv.applications), discovered_namespaces: std.manifestYamlDoc(discoveredNamespaces), apps_without_namespaces: std.manifestYamlDoc(std.foldl(function(prev, app) if discoveredNamespaces[app] == null then prev + [ app ] else prev, std.objectFields(discoveredNamespaces), [])), discovered_teams: std.manifestYamlDoc(discoveredTeams), - proposed_routes: std.manifestYamlDoc(teamBasedRouting(adParams, nullReceiver, monitoring_instance)), - alertmanager: std.manifestYamlDoc(alertmanagerConfig(adParams, amConfig, nullReceiver, fallback_team, monitoring_instance)), + proposed_routes: std.manifestYamlDoc(teamBasedRouting(adParams, nullReceiver)), + alertmanager: std.manifestYamlDoc(alertmanagerConfig(adParams, amConfig, nullReceiver, fallback_team)), }; { diff --git a/tests/alert-routing-discovery.jsonnet b/tests/alert-routing-discovery.jsonnet index e06a257..d31242f 100644 --- a/tests/alert-routing-discovery.jsonnet +++ b/tests/alert-routing-discovery.jsonnet @@ -4,7 +4,6 @@ local com = import 'lib/commodore.libjsonnet'; local inv = com.inventory(); local expected = inv.expected; -local monitoring_instance = inv.parameters._instance; local nullR = '__null'; local fallback_team = inv.parameters.openshift4_monitoring.fallback_team; @@ -14,7 +13,7 @@ local amConfig = inv.parameters.openshift4_monitoring.alertManagerConfig; local debugData = local rendered = ard.debugConfigMapData( - adParams, amConfig, nullR, fallback_team, monitoring_instance + adParams, amConfig, nullR, fallback_team, ); local desired = { applications: std.manifestJsonMinified(inv.applications), @@ -34,7 +33,7 @@ local debugData = local alertmanagerConfig = local rendered = ard.alertmanagerConfig( - adParams, amConfig, nullR, fallback_team, monitoring_instance + adParams, amConfig, nullR, fallback_team ); if rendered != expected.alertmanagerConfig then error From 99219d24eb3e1f4b449d76ed51ed6034b555a109 Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Tue, 28 Jul 2026 15:23:59 +0200 Subject: [PATCH 4/5] alert-routing-discovery: Make `nullReceiver` and `fallbackTeam` optional parameters --- alert-routing-discovery.libsonnet | 16 +++++++++------- tests/alert-routing-discovery.jsonnet | 2 +- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/alert-routing-discovery.libsonnet b/alert-routing-discovery.libsonnet index 41cdcc6..32c6b1f 100644 --- a/alert-routing-discovery.libsonnet +++ b/alert-routing-discovery.libsonnet @@ -55,11 +55,11 @@ local discoverNS = function(app) else ns; -local ownerOrFallbackTeam(fallback_team) = +local ownerOrFallbackTeam(fallbackTeam) = if std.objectHas(params, 'syn') && std.objectHas(params.syn, 'owner') then params.syn.owner else - fallback_team; + fallbackTeam; // teamToNS is a map from a team to namespaces. // The inner `std.prune()` is to drop `null` entries from a list that contains @@ -99,12 +99,14 @@ local teamBasedRouting(adParams, nullReceiver) = continue: false, } ]; -local alertmanagerConfig(adParams, amConfig, nullReceiver, fallback_team) = +local _nullR = '__alert_routing_discovery_null'; + +local alertmanagerConfig(adParams, amConfig, nullReceiver=_nullR, fallbackTeam=null) = local routes = std.get(amConfig.route, 'routes', []); local finalRoute = - if ownerOrFallbackTeam(fallback_team) != null then + if ownerOrFallbackTeam(fallbackTeam) != null then [ { - receiver: adParams.team_receiver_format % ownerOrFallbackTeam(fallback_team), + receiver: adParams.team_receiver_format % ownerOrFallbackTeam(fallbackTeam), } ] else [ { receiver: nullReceiver } ]; @@ -120,7 +122,7 @@ local alertmanagerConfig(adParams, amConfig, nullReceiver, fallback_team) = }, }; -local debugConfigMapData = function(adParams, amConfig, nullReceiver, fallback_team) +local debugConfigMapData = function(adParams, amConfig, nullReceiver=_nullR, fallbackTeam=null) { local discoveredNamespaces = std.foldl(function(prev, app) prev { [app]: discoverNS(app) }, inv.applications, {}), local discoveredTeams = std.foldl(function(prev, app) prev { [app]: syn_teams.teamForApplication(syn_teams.appKeys(app, true)[0]) }, inv.applications, {}), @@ -129,7 +131,7 @@ local debugConfigMapData = function(adParams, amConfig, nullReceiver, fallback_t apps_without_namespaces: std.manifestYamlDoc(std.foldl(function(prev, app) if discoveredNamespaces[app] == null then prev + [ app ] else prev, std.objectFields(discoveredNamespaces), [])), discovered_teams: std.manifestYamlDoc(discoveredTeams), proposed_routes: std.manifestYamlDoc(teamBasedRouting(adParams, nullReceiver)), - alertmanager: std.manifestYamlDoc(alertmanagerConfig(adParams, amConfig, nullReceiver, fallback_team)), + alertmanager: std.manifestYamlDoc(alertmanagerConfig(adParams, amConfig, nullReceiver, fallbackTeam)), }; { diff --git a/tests/alert-routing-discovery.jsonnet b/tests/alert-routing-discovery.jsonnet index d31242f..0d900ed 100644 --- a/tests/alert-routing-discovery.jsonnet +++ b/tests/alert-routing-discovery.jsonnet @@ -33,7 +33,7 @@ local debugData = local alertmanagerConfig = local rendered = ard.alertmanagerConfig( - adParams, amConfig, nullR, fallback_team + adParams, amConfig, nullR, fallback_team, ); if rendered != expected.alertmanagerConfig then error From 5f6a24c6e7acb72d8dfc345b40f478cc28ca9b13 Mon Sep 17 00:00:00 2001 From: Simon Gerber Date: Tue, 28 Jul 2026 15:24:24 +0200 Subject: [PATCH 5/5] Add doxygen-formatted documentation in alert-routing-discovery.libsonnet --- alert-routing-discovery.libsonnet | 67 +++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) diff --git a/alert-routing-discovery.libsonnet b/alert-routing-discovery.libsonnet index 32c6b1f..10d834e 100644 --- a/alert-routing-discovery.libsonnet +++ b/alert-routing-discovery.libsonnet @@ -1,3 +1,17 @@ +/** + * \file alert-routing-discovery.libsonnet + * \brief Functionality to render an Alertmanager config which routes alerts based on component instance team assignments. + * + * NOTE: This Jsonnet library is intended for use with Commodore. The library + * can be used in component code as well as postprocessing filters. + * + * This library is inteded to be used by Commodore components which configure + * Alertmanager (e.g. openshift4-monitoring or prometheus). The library + * exposes two functions `alertmanagerConfig` and `debugConfigMapData`. Both + * functions expect the same set of parameters: `adParams`, `amConfig`, + * `nullReceiver` and `fallbackTeam`. See the documentation of function + * `alertmanagerConfig()` for a detailed description of these parameters. + */ local com = import 'lib/commodore.libjsonnet'; local syn_teams = import 'syn-teams.libsonnet'; @@ -101,6 +115,48 @@ local teamBasedRouting(adParams, nullReceiver) = local _nullR = '__alert_routing_discovery_null'; +/** + * \brief Returns an Alertmanager config that's suitable to be used to configure Alertmanager via prometheus-operator. + * + * \arg adParams + * Configuration for the alert routing discovery algorithm. + * + * This parameter is an object which is expected to have fields + * `prepend_routes`, `append_routes`, `team_receiver_format`, and + * `additional_alert_matchers`. + * + * Fields `prepend_routes` and `append_routes` are added to the final + * Alertmanager config before and after the dynamic routes that are + * generated by the discovery algorithm. + * + * Field `team_receiver_format` is used to render Alertmanager receiver + * names for each discovered team. The library expects that the value of + * this field contains exactly one '%s' which is substituted with the target + * team's name. Users of the library must ensure that matching receivers are + * defined in `amConfig.receivers` (or injected into the result of this + * function). + * + * Field `additional_alert_matchers` is used in addition ot the matchers + * generated by the discovery logic and allows users to further restrict the + * set of alerts that are matched by the generated routes. + * + * \arg amConfig + * Partial Alertmanager config. The config that's rendered by this function + * is merged into this object. + * + * \arg nullReceiver (optional) + * Name to use for the fallback blackhole receiver for alerts that can't be + * assigned to any team. Defaults to `__alert_routing_discovery_null`. + * + * \arg fallbackTeam (optional) + * Fallback team to which to route alerts which can't be mapped to a team. + * Has no effect if `syn.owner` is set for the target cluster. + * Defaults to `null`. If neither `syn.owner` nor this argument is set to + * non-null, alerts that can't be mapped to a team are blackholed. + * + * \return + * An object that can be used to configure Alertmanager via prometheus-operator. + */ local alertmanagerConfig(adParams, amConfig, nullReceiver=_nullR, fallbackTeam=null) = local routes = std.get(amConfig.route, 'routes', []); local finalRoute = @@ -122,6 +178,17 @@ local alertmanagerConfig(adParams, amConfig, nullReceiver=_nullR, fallbackTeam=n }, }; +/** + * \brief Returns an object with data that's helpful when debugging the alert routing discovery for a certain cluster. + * + * See function `alertmanagerConfig` for a description of this function's + * parameters. + * + * \return + * An object containing debug information for the alert routing discovery. + * The debug information is formatted as string-string pairs so the debug + * information can be deployed in the cluster as a `ConfigMap`. + */ local debugConfigMapData = function(adParams, amConfig, nullReceiver=_nullR, fallbackTeam=null) { local discoveredNamespaces = std.foldl(function(prev, app) prev { [app]: discoverNS(app) }, inv.applications, {}),