diff --git a/demo/karma.yaml b/demo/karma.yaml index 32bac3ed6..9fd62d364 100644 --- a/demo/karma.yaml +++ b/demo/karma.yaml @@ -30,6 +30,16 @@ labels: info: "#87c4e0" warning: "#ffae42" critical: "#ff220c" + sorting: + valueMapping: + cluster: + prod: 1 + staging: 2 + dev: 3 + severity: + critical: 1 + warning: 2 + info: 3 log: config: false level: warning diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 598151dd7..868700096 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -29,7 +29,7 @@ pair of Alertmanager instances running in [HA mode](https://prometheus.io/docs/alerting/alertmanager/#high-availability). Syntax: -```yaml +```YAML alertmanager: interval: duration servers: @@ -97,7 +97,7 @@ alertmanager: Example with two production Alertmanager instances running in HA mode and a staging instance that is also proxied and requires a custom auth header: -```yaml +```YAML alertmanager: interval: 1m servers: @@ -131,7 +131,7 @@ alertmanager: Defaults: -```yaml +```YAML alertmanager: interval: 1m servers: [] @@ -148,7 +148,7 @@ needs to be configured without a config file see the UI. Syntax: -```yaml +```YAML annotations: default: hidden: bool @@ -174,7 +174,7 @@ Example where all annotations except `summary` are hidden by default. If there are additional annotation keys user will need to click on the `+` icon to see them. -```yaml +```YAML annotations: default: hidden: true @@ -192,7 +192,7 @@ Example where all annotations except `details` are visible by default. If icon to see it. Additionally `secret` annotation is stripped and never shown in the UI. -```yaml +```YAML annotations: default: hidden: false @@ -206,7 +206,7 @@ annotations: Defaults: -```yaml +```YAML annotations: default: hidden: false @@ -220,7 +220,7 @@ annotations: Syntax: -```yaml +```YAML filters: default: list of strings ``` @@ -231,7 +231,7 @@ filters: Example: -```yaml +```YAML filters: default: - "@state=active" @@ -240,7 +240,7 @@ filters: Defaults: -```yaml +```YAML filters: default: [] ``` @@ -258,7 +258,7 @@ with shared labels, for example coloring hostname label will allow to quickly spot all alerts for the same host. Syntax: -```yaml +```YAML labels: color: static: [] @@ -266,6 +266,8 @@ labels: custom: {} keep: list of strings strip: list of strings + sorting: + valueMapping: {} ``` - `color:static` - list of label names that will all have the same color applied @@ -284,12 +286,19 @@ labels: it via the config file. - `keep` - list of allowed labels, if empty all labels are allowed. - `strip` - list of ignored labels. +- `sorting:valueMapping` - when sorting using alert labels values are compared + as strings, which work for labels like `cluster=A`, `cluster=B` & `cluster=C`, + but not for `cluster=prod`, `cluster=staging` & `cluster=dev`. Alphabetic + sort would order the second case as follows: `dev`, `prod`, `staging`. + To allow for more natural sorting `sorting:valueMapping` can be used to + map label values to integer values which will be used for sorting instead + of original string values. Example with static color for the `job` label (every `job` label will have the same color regardless of the value) and unique color for the `@receiver` label (every `@receiver` label will have color unique for each value). -```yaml +```YAML labels: color: static: @@ -300,7 +309,7 @@ labels: Example where `task_id` label is ignored by karma: -```yaml +```YAML labels: keep: [] strip: @@ -309,7 +318,7 @@ labels: Example where all but `instance` and `alertname` labels are allowed: -```yaml +```YAML labels: keep: - alertname @@ -320,7 +329,7 @@ labels: Example where `severity` label will have a red color for `critical`, yellow for `warning` and blue for `info`: -```yaml +```YAML labels: color: custom: @@ -332,9 +341,27 @@ labels: critical: "#ff220c" ``` +Example with custom value mapping for sorting, this allows to put `cluster=prod` +or `severity=critical` (depending on which label is used for sorting) on top of +the dashboard grid (or bottom if `Reverse` is enabled by user). + +```YAML +labels: + sorting: + valueMapping: + cluster: + prod: 1 + staging: 2 + dev: 3 + severity: + critical: 1 + warning: 2 + info: 3 +``` + Defaults: -```yaml +```YAML labels: color: static: [] @@ -342,6 +369,8 @@ labels: custom: {} keep: [] strip: [] + sorting: + valueMapping: {} ``` ### Listen @@ -349,7 +378,7 @@ labels: `listen` section allows configuring karma web server behavior. Syntax: -```yaml +```YAML listen: address: string port: integer @@ -364,7 +393,7 @@ listen: Example where karma would listen for HTTP requests on `http://1.2.3.4:80/karma/` -```yaml +```YAML listen: address: 1.2.3.4 port: 80 @@ -373,7 +402,7 @@ listen: Defaults: -```yaml +```YAML listen: address: "0.0.0.0" port: 8080 @@ -385,7 +414,7 @@ listen: `log` section allows configuring logging subsystem. Syntax: -```yaml +```YAML log: config: bool level: string @@ -397,7 +426,7 @@ log: Defaults: -```yaml +```YAML log: config: true level: info @@ -410,7 +439,7 @@ issues in silence comments. If a string inside a comment matches one of the rules it will be rendered as a link. Syntax: -```yaml +```YAML jira: - regex: string - uri: string @@ -423,7 +452,7 @@ jira: Example where a string `DEVOPS-123` inside a comment would be rendered as a link to `https://jira.example.com/browse/DEVOPS-123`. -```yaml +```YAML jira: - regex: DEVOPS-[0-9]+ uri: https://jira.example.com @@ -431,7 +460,7 @@ jira: Defaults: -```yaml +```YAML jira: [] ``` @@ -442,7 +471,7 @@ handled by karma. If alerts are routed to multiple receivers they can be duplicated in the UI, each instance will have different value for `@receiver`. Syntax: -```yaml +```YAML receivers: keep: list of strings strip: list of strings @@ -455,7 +484,7 @@ receivers: Example where alerts that are routed to the `alertmanage2es` receiver are ignored by karma. -```yaml +```YAML receivers: strip: - alertmanage2es @@ -463,7 +492,7 @@ receivers: Defaults: -```yaml +```YAML receivers: strip: [] ``` @@ -475,7 +504,7 @@ receivers: details. Syntax: -```yaml +```YAML sentry: private: string public: string @@ -488,7 +517,7 @@ sentry: Example: -```yaml +```YAML sentry: private: https://:@sentry.io/ public: https://:@sentry.io/ @@ -502,7 +531,7 @@ JavaScript code, which can be used to either override built in CSS styles or integrate with extra services, for example using error handlers other than Sentry. -```yaml +```YAML custom: css: string js: string @@ -513,7 +542,7 @@ custom: Example: -```yaml +```YAML custom: css: /theme/custom.css js: /assets/custom.js diff --git a/internal/config/config.go b/internal/config/config.go index 0c1a221c0..38afd7a5c 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -167,6 +167,11 @@ func (config *configSchema) Read() { log.Fatal(err) } + err = v.UnmarshalKey("labels.sorting.valuemapping", &config.Labels.Sorting.ValueMapping) + if err != nil { + log.Fatal(err) + } + // accept single Alertmanager server from flag/env if nothing is set yet if len(config.Alertmanager.Servers) == 0 && v.GetString("alertmanager.uri") != "" { log.Info("Using simple config with a single Alertmanager server") diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 2c33995f8..034dfaf94 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -98,6 +98,8 @@ labels: unique: - f - gg + sorting: + valueMapping: {} listen: address: 0.0.0.0 port: 80 diff --git a/internal/config/models.go b/internal/config/models.go index eefd6b66a..2b3302555 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -51,6 +51,9 @@ type configSchema struct { Static []string Unique []string } + Sorting struct { + ValueMapping map[string]map[string]int `yaml:"valueMapping"` + } } Listen struct { Address string diff --git a/internal/models/api.go b/internal/models/api.go index a20c85fca..60ce14de8 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -155,12 +155,18 @@ func (ag *APIAlertGroup) DedupSharedMaps() { } } +// SortSettings nests all settings specific to sorting +type SortSettings struct { + ValueMapping map[string]map[string]int `json:"valueMapping"` +} + // Settings is used to export karma configuration that is used by UI type Settings struct { - StaticColorLabels []string `json:"staticColorLabels"` - AnnotationsDefaultHidden bool `json:"annotationsDefaultHidden"` - AnnotationsHidden []string `json:"annotationsHidden"` - AnnotationsVisible []string `json:"annotationsVisible"` + StaticColorLabels []string `json:"staticColorLabels"` + AnnotationsDefaultHidden bool `json:"annotationsDefaultHidden"` + AnnotationsHidden []string `json:"annotationsHidden"` + AnnotationsVisible []string `json:"annotationsVisible"` + Sorting SortSettings `json:"sorting"` } // AlertsResponse is the structure of JSON response UI will use to get alert data diff --git a/ui/src/Components/Grid/AlertGrid/index.js b/ui/src/Components/Grid/AlertGrid/index.js index c29d11536..85ec39c6f 100644 --- a/ui/src/Components/Grid/AlertGrid/index.js +++ b/ui/src/Components/Grid/AlertGrid/index.js @@ -70,7 +70,7 @@ const AlertGrid = observer( }); compare = (a, b) => { - const { settingsStore } = this.props; + const { alertStore, settingsStore } = this.props; // don't sort if sorting is disabled if ( @@ -88,17 +88,42 @@ const AlertGrid = observer( return moment.max(g.alerts.map(a => moment(a.startsAt))); } - const label = settingsStore.gridConfig.config.sortLabel; - return g.labels[label] || g.alerts[0].labels[label] || ""; + const labelName = settingsStore.gridConfig.config.sortLabel; + const labelValue = + g.labels[labelName] || + g.shared.labels[labelName] || + g.alerts[0].labels[labelName]; + let mappedValue; + + // check if we have a mapping for label value + if ( + labelValue !== undefined && + alertStore.settings.values.sorting.valueMapping[labelName] !== + undefined + ) { + mappedValue = + alertStore.settings.values.sorting.valueMapping[labelName][ + labelValue + ]; + } + + // if we have a mapped value then return it, if not return original value + return mappedValue !== undefined ? mappedValue : labelValue; }; + const val = settingsStore.gridConfig.config.reverseSort ? -1 : 1; + const av = getLabelValue(a); const bv = getLabelValue(b); - const val = settingsStore.gridConfig.config.reverseSort ? -1 : 1; - if (av > bv) { + if (av === undefined && av === undefined) { + // if both alerts lack the label they are equal + return 0; + } else if (av === undefined || av > bv) { + // if first one lacks it it's should be rendered after alerts with that label return val; - } else if (av < bv) { + } else if (bv === undefined || av < bv) { + // if the first one has label but the second doesn't then the second should be rendered after the first return val * -1; } else { return 0; diff --git a/ui/src/Components/Grid/AlertGrid/index.test.js b/ui/src/Components/Grid/AlertGrid/index.test.js index 9daa346c3..00f4025ab 100644 --- a/ui/src/Components/Grid/AlertGrid/index.test.js +++ b/ui/src/Components/Grid/AlertGrid/index.test.js @@ -230,4 +230,60 @@ describe("", () => { "id3" ]); }); + + it("label value mappings from settings are used to order alerts", () => { + alertStore.settings.values.sorting.valueMapping = { + cluster: { + prod: 1, + staging: 2, + dev: 3 + } + }; + + settingsStore.gridConfig.config.sortOrder = + settingsStore.gridConfig.options.label.value; + settingsStore.gridConfig.config.sortLabel = "cluster"; + settingsStore.gridConfig.config.reverseSort = false; + + MockGroupList(3, 1); + alertStore.data.groups.id1.alerts[0].labels.cluster = "dev"; + alertStore.data.groups.id2.alerts[0].labels.cluster = "staging"; + alertStore.data.groups.id3.alerts[0].labels.cluster = "prod"; + + const tree = ShallowAlertGrid(); + const alertGroups = tree.find("AlertGroup"); + expect(alertGroups.map(g => g.props().group.id)).toEqual([ + "id3", + "id2", + "id1" + ]); + }); + + it("label value mappings from settings are used to order alerts and reverse flag is respected", () => { + alertStore.settings.values.sorting.valueMapping = { + cluster: { + prod: 1, + staging: 2, + dev: 3 + } + }; + + settingsStore.gridConfig.config.sortOrder = + settingsStore.gridConfig.options.label.value; + settingsStore.gridConfig.config.sortLabel = "cluster"; + settingsStore.gridConfig.config.reverseSort = true; + + MockGroupList(3, 1); + alertStore.data.groups.id1.alerts[0].labels.cluster = "dev"; + alertStore.data.groups.id2.alerts[0].labels.cluster = "prod"; + alertStore.data.groups.id3.alerts[0].labels.cluster = "staging"; + + const tree = ShallowAlertGrid(); + const alertGroups = tree.find("AlertGroup"); + expect(alertGroups.map(g => g.props().group.id)).toEqual([ + "id1", + "id3", + "id2" + ]); + }); }); diff --git a/ui/src/Stores/AlertStore.js b/ui/src/Stores/AlertStore.js index d0e7b7fec..c9bcabfa5 100644 --- a/ui/src/Stores/AlertStore.js +++ b/ui/src/Stores/AlertStore.js @@ -169,7 +169,10 @@ class AlertStore { staticColorLabels: [], annotationsDefaultHidden: false, annotationsHidden: [], - annotationsVisible: [] + annotationsVisible: [], + sorting: { + valueMapping: {} + } } }, {}, diff --git a/ui/src/__mocks__/Fetch.js b/ui/src/__mocks__/Fetch.js index 7165ac539..1555242ad 100644 --- a/ui/src/__mocks__/Fetch.js +++ b/ui/src/__mocks__/Fetch.js @@ -23,6 +23,20 @@ const EmptyAPIResponse = () => ({ } ], settings: { + sorting: { + valueMapping: { + cluster: { + dev: 3, + prod: 1, + staging: 2 + }, + severity: { + critical: 1, + info: 3, + warning: 2 + } + } + }, staticColorLabels: ["job"], annotationsDefaultHidden: false, annotationsHidden: [], diff --git a/views.go b/views.go index a1be6d90d..c54b5fdbd 100644 --- a/views.go +++ b/views.go @@ -83,12 +83,19 @@ func alerts(c *gin.Context) { resp.Version = version resp.Upstreams = getUpstreams() resp.Settings = models.Settings{ + Sorting: models.SortSettings{ + ValueMapping: map[string]map[string]int{}, + }, StaticColorLabels: config.Config.Labels.Color.Static, AnnotationsDefaultHidden: config.Config.Annotations.Default.Hidden, AnnotationsHidden: config.Config.Annotations.Hidden, AnnotationsVisible: config.Config.Annotations.Visible, } + if config.Config.Labels.Sorting.ValueMapping != nil { + resp.Settings.Sorting.ValueMapping = config.Config.Labels.Sorting.ValueMapping + } + // use full URI (including query args) as cache key cacheKey := c.Request.RequestURI