diff --git a/demo/karma.yaml b/demo/karma.yaml index 9fd62d364..ce48a6c6d 100644 --- a/demo/karma.yaml +++ b/demo/karma.yaml @@ -17,6 +17,11 @@ custom: filters: default: - "@receiver=by-cluster-service" +grid: + sorting: + order: label + reverse: false + label: severity labels: color: static: diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 868700096..7685293bd 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -245,6 +245,61 @@ filters: default: [] ``` +### Grid + +`grid` section allows customizing how alert grid is rendered in the UI. +Sorting configuration can be overridden by each user via UI settings. +Syntax: + +```YAML +grid: + sorting: + order: string + reverse: bool + label: string +``` + +- `sorting:order` - default sort order for alert grid, valid values are: + - `disabled` - no sorting, alert groups are rendered in the order they are + returned by the API + - `startsAt` - sort by alert timestamps, most recent alert in each group will + be used when comparing each group + - `label` - sort by labels, if the label used for sorting is not shared by + all alerts in a group then the first alert in the group will be queried for + it +- `sorting:reverse` - default value for reversed sort order +- `sorting:label` - label name for sorting when `grid:sorting:order` is set + to `label`. Labels can be assigned custom values used only by sorting via + `labels:sorting:valueMapping`, see [Labels](#Labels) section for details. + +Defaults: + +```YAML +grid: + sorting: + order: startsAt + reverse: true + label: alertname +``` + +Example with sorting using `severity` label, with extra option to map severity +labels to numeric values for sorting: + +```YAML +grid: + sorting: + order: label + reverse: false + label: severity +labels: + sorting: + valueMapping: + severity: + critical: 1 + warning: 2 + info: 3 +``` + ### Labels `labels` section allows configuring how alert labels will be rendered in the @@ -293,6 +348,8 @@ labels: 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. + Note: this option is not available via environment variables, you can only set + it via the config file. 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 diff --git a/internal/config/config.go b/internal/config/config.go index 38afd7a5c..ec42b1b71 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -7,6 +7,7 @@ import ( "strings" "time" + "github.com/prymitive/karma/internal/slices" "github.com/prymitive/karma/internal/uri" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -60,6 +61,10 @@ func init() { "List of labels to keep, all other labels will be stripped") pflag.StringSlice("labels.strip", []string{}, "List of labels to ignore") + pflag.String("grid.sorting.order", "startsAt", "Default sort order for alert grid") + pflag.Bool("grid.sorting.reverse", true, "Reverse sort order") + pflag.String("grid.sorting.label", "alertname", "Label name to use when sorting alert grid by label") + pflag.Bool("log.config", true, "Log used configuration to log on startup") pflag.String("log.level", "info", "Log level, one of: debug, info, warning, error, fatal and panic") @@ -137,6 +142,9 @@ func (config *configSchema) Read() { config.Custom.JS = v.GetString("custom.js") config.Debug = v.GetBool("debug") config.Filters.Default = v.GetStringSlice("filters.default") + config.Grid.Sorting.Order = v.GetString("grid.sorting.order") + config.Grid.Sorting.Reverse = v.GetBool("grid.sorting.reverse") + config.Grid.Sorting.Label = v.GetString("grid.sorting.label") config.Labels.Color.Custom = map[string]map[string]string{} config.Labels.Color.Static = v.GetStringSlice("labels.color.static") config.Labels.Color.Unique = v.GetStringSlice("labels.color.unique") @@ -172,6 +180,10 @@ func (config *configSchema) Read() { log.Fatal(err) } + if !slices.StringInSlice([]string{"disabled", "startsAt", "label"}, config.Grid.Sorting.Order) { + log.Fatalf("Invalid grid.sorting.order value '%s', allowed options: disabled, startsAt, label", config.Grid.Sorting.Order) + } + // 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 034dfaf94..7900d7045 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -82,6 +82,11 @@ filters: default: - '@state=active' - foo=bar +grid: + sorting: + order: startsAt + reverse: true + label: alertname labels: keep: - foo diff --git a/internal/config/models.go b/internal/config/models.go index 2b3302555..f810796f6 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -43,6 +43,13 @@ type configSchema struct { Filters struct { Default []string } + Grid struct { + Sorting struct { + Order string + Reverse bool + Label string + } + } Labels struct { Keep []string Strip []string diff --git a/internal/models/api.go b/internal/models/api.go index 60ce14de8..fad016380 100644 --- a/internal/models/api.go +++ b/internal/models/api.go @@ -155,8 +155,16 @@ func (ag *APIAlertGroup) DedupSharedMaps() { } } +// GridSettings exposes all grid settings from the config file +type GridSettings struct { + Order string `json:"order"` + Reverse bool `json:"reverse"` + Label string `json:"label"` +} + // SortSettings nests all settings specific to sorting type SortSettings struct { + Grid GridSettings `json:"grid"` ValueMapping map[string]map[string]int `json:"valueMapping"` } diff --git a/ui/src/Components/Grid/AlertGrid/index.js b/ui/src/Components/Grid/AlertGrid/index.js index 85ec39c6f..7e11d0db0 100644 --- a/ui/src/Components/Grid/AlertGrid/index.js +++ b/ui/src/Components/Grid/AlertGrid/index.js @@ -72,37 +72,48 @@ const AlertGrid = observer( compare = (a, b) => { const { alertStore, settingsStore } = this.props; - // don't sort if sorting is disabled - if ( + const useDefaults = settingsStore.gridConfig.config.sortOrder === - settingsStore.gridConfig.options.disabled.value - ) + settingsStore.gridConfig.options.default.value; + + const sortOrder = useDefaults + ? alertStore.settings.values.sorting.grid.order + : settingsStore.gridConfig.config.sortOrder; + + // don't sort if sorting is disabled + if (sortOrder === settingsStore.gridConfig.options.disabled.value) return 0; + const sortReverse = + useDefaults || settingsStore.gridConfig.config.reverseSort === undefined + ? alertStore.settings.values.sorting.grid.reverse + : settingsStore.gridConfig.config.reverseSort; + + const sortLabel = + useDefaults || settingsStore.gridConfig.config.sortLabel === undefined + ? alertStore.settings.values.sorting.grid.label + : settingsStore.gridConfig.config.sortLabel; + const getLabelValue = g => { // if timestamp sort is enabled use latest alert for sorting - if ( - settingsStore.gridConfig.config.sortOrder === - settingsStore.gridConfig.options.startsAt.value - ) { + if (sortOrder === settingsStore.gridConfig.options.startsAt.value) { return moment.max(g.alerts.map(a => moment(a.startsAt))); } - const labelName = settingsStore.gridConfig.config.sortLabel; const labelValue = - g.labels[labelName] || - g.shared.labels[labelName] || - g.alerts[0].labels[labelName]; + g.labels[sortLabel] || + g.shared.labels[sortLabel] || + g.alerts[0].labels[sortLabel]; let mappedValue; // check if we have a mapping for label value if ( labelValue !== undefined && - alertStore.settings.values.sorting.valueMapping[labelName] !== + alertStore.settings.values.sorting.valueMapping[sortLabel] !== undefined ) { mappedValue = - alertStore.settings.values.sorting.valueMapping[labelName][ + alertStore.settings.values.sorting.valueMapping[sortLabel][ labelValue ]; } @@ -111,7 +122,7 @@ const AlertGrid = observer( return mappedValue !== undefined ? mappedValue : labelValue; }; - const val = settingsStore.gridConfig.config.reverseSort ? -1 : 1; + const val = sortReverse ? -1 : 1; const av = getLabelValue(a); const bv = getLabelValue(b); diff --git a/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.js b/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.js index 9938a744c..2dd593cc4 100644 --- a/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.js +++ b/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.js @@ -49,13 +49,19 @@ const AlertGroupSortConfiguration = observer( .includes(settingsStore.gridConfig.config.sortOrder) ) { settingsStore.gridConfig.config.sortOrder = - settingsStore.gridConfig.defaults.sortOrder; + settingsStore.gridConfig.options.default.value; } }); render() { const { settingsStore } = this.props; + const hideReverse = + settingsStore.gridConfig.config.sortOrder === + settingsStore.gridConfig.options.default.value || + settingsStore.gridConfig.config.sortOrder === + settingsStore.gridConfig.options.disabled.value; + return (