mirror of
https://github.com/prymitive/karma
synced 2026-05-07 03:26:52 +00:00
Merge pull request #454 from prymitive/sort-value-mapping
feat(config): allow configuring label value mapping for sorting
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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://<key>:<secret>@sentry.io/<project>
|
||||
public: https://<key>:<secret>@sentry.io/<project>
|
||||
@@ -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
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -98,6 +98,8 @@ labels:
|
||||
unique:
|
||||
- f
|
||||
- gg
|
||||
sorting:
|
||||
valueMapping: {}
|
||||
listen:
|
||||
address: 0.0.0.0
|
||||
port: 80
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -230,4 +230,60 @@ describe("<AlertGrid />", () => {
|
||||
"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"
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -169,7 +169,10 @@ class AlertStore {
|
||||
staticColorLabels: [],
|
||||
annotationsDefaultHidden: false,
|
||||
annotationsHidden: [],
|
||||
annotationsVisible: []
|
||||
annotationsVisible: [],
|
||||
sorting: {
|
||||
valueMapping: {}
|
||||
}
|
||||
}
|
||||
},
|
||||
{},
|
||||
|
||||
@@ -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: [],
|
||||
|
||||
7
views.go
7
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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user