From 9e4abbd42b7f2ebf6564a9fe0acfe816a855289e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 4 Feb 2019 13:18:47 +0000 Subject: [PATCH 1/3] feat(backend): allow setting custom colors for labels This allows to have a user defined color (rgb or hex) for label values configured in the config file --- go.mod | 1 + go.sum | 2 ++ internal/config/config.go | 6 +++++ internal/config/config_test.go | 1 + internal/config/models.go | 1 + internal/transform/color_test.go | 46 +++++++++++++++++++++++--------- internal/transform/colors.go | 37 ++++++++++++++++++++++++- 7 files changed, 81 insertions(+), 13 deletions(-) diff --git a/go.mod b/go.mod index 2e8e30168..1614ea7ac 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/spf13/pflag v1.0.3 github.com/spf13/viper v1.3.1 github.com/terinjokes/bakelite v0.2.0 // indirect + gopkg.in/go-playground/colors.v1 v1.2.0 gopkg.in/jarcoal/httpmock.v1 v1.0.0-20181117152235-275e9df93516 gopkg.in/yaml.v2 v2.2.2 ) diff --git a/go.sum b/go.sum index aafe40a7f..7c10fccf8 100644 --- a/go.sum +++ b/go.sum @@ -275,6 +275,8 @@ gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMy gopkg.in/gemnasium/logrus-airbrake-hook.v2 v2.1.2/go.mod h1:Xk6kEKp8OKb+X14hQBKWaSkCsqBpgog8nAV2xsGOxlo= gopkg.in/go-playground/assert.v1 v1.2.1 h1:xoYuJVE7KT85PYWrN730RguIQO0ePzVRfFMXadIrXTM= gopkg.in/go-playground/assert.v1 v1.2.1/go.mod h1:9RXL0bg/zibRAgZUYszZSwO/z8Y/a8bDuhia5mkpMnE= +gopkg.in/go-playground/colors.v1 v1.2.0 h1:SPweMUve+ywPrfwao+UvfD5Ah78aOLUkT5RlJiZn52c= +gopkg.in/go-playground/colors.v1 v1.2.0/go.mod h1:AvbqcMpNXVl5gBrM20jBm3VjjKBbH/kI5UnqjU7lxFI= gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ= gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y= gopkg.in/jarcoal/httpmock.v1 v1.0.0-20181117152235-275e9df93516 h1:H6trpavCIuipdInWrab8l34Mf+GGVfphniHostMdMaQ= diff --git a/internal/config/config.go b/internal/config/config.go index d381fafcc..0c1a221c0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -137,6 +137,7 @@ func (config *configSchema) Read() { config.Custom.JS = v.GetString("custom.js") config.Debug = v.GetBool("debug") config.Filters.Default = v.GetStringSlice("filters.default") + 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") config.Labels.Keep = v.GetStringSlice("labels.keep") @@ -161,6 +162,11 @@ func (config *configSchema) Read() { log.Fatal(err) } + err = v.UnmarshalKey("labels.color.custom", &config.Labels.Color.Custom) + 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 0f5c095ec..2c33995f8 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -90,6 +90,7 @@ labels: - abc - def color: + custom: {} static: - a - bb diff --git a/internal/config/models.go b/internal/config/models.go index b38e7299b..eefd6b66a 100644 --- a/internal/config/models.go +++ b/internal/config/models.go @@ -47,6 +47,7 @@ type configSchema struct { Keep []string Strip []string Color struct { + Custom map[string]map[string]string Static []string Unique []string } diff --git a/internal/transform/color_test.go b/internal/transform/color_test.go index 1116224bb..d0a993891 100644 --- a/internal/transform/color_test.go +++ b/internal/transform/color_test.go @@ -9,22 +9,23 @@ import ( ) type colorTest struct { - config []string - labels map[string]string - colors map[string]string + uniqueLabels []string + customLabels map[string]map[string]string + labels map[string]string + colors map[string]string } var colorTests = []colorTest{ - colorTest{ + { labels: map[string]string{}, }, - colorTest{ + { labels: map[string]string{ "node": "localhost", }, }, - colorTest{ - config: []string{"node"}, + { + uniqueLabels: []string{"node"}, labels: map[string]string{ "node": "localhost", }, @@ -32,8 +33,8 @@ var colorTests = []colorTest{ "node": "localhost", }, }, - colorTest{ - config: []string{"node", "instance"}, + { + uniqueLabels: []string{"node", "instance"}, labels: map[string]string{ "node": "instance", "env": "instance", @@ -45,8 +46,8 @@ var colorTests = []colorTest{ "instance": "server1", }, }, - colorTest{ - config: []string{"job", "node", "instance"}, + { + uniqueLabels: []string{"job", "node", "instance"}, labels: map[string]string{ "job": "node_ping", }, @@ -54,11 +55,32 @@ var colorTests = []colorTest{ "job": "node_ping", }, }, + { + customLabels: map[string]map[string]string{ + "node": map[string]string{"localhost": "#fff"}, + }, + labels: map[string]string{ + "node": "localhost", + }, + colors: map[string]string{ + "node": "localhost", + }, + }, + { + customLabels: map[string]map[string]string{ + "node": map[string]string{"localhost": "not a color"}, + }, + labels: map[string]string{ + "node": "localhost", + }, + colors: map[string]string{}, + }, } func TestColorLabel(t *testing.T) { for _, testCase := range colorTests { - config.Config.Labels.Color.Unique = testCase.config + config.Config.Labels.Color.Unique = testCase.uniqueLabels + config.Config.Labels.Color.Custom = testCase.customLabels colorStore := models.LabelsColorMap{} for key, value := range testCase.labels { transform.ColorLabel(colorStore, key, value) diff --git a/internal/transform/colors.go b/internal/transform/colors.go index 3087f0d4c..9800a1f22 100644 --- a/internal/transform/colors.go +++ b/internal/transform/colors.go @@ -10,6 +10,7 @@ import ( "github.com/prymitive/karma/internal/slices" "github.com/hansrodtang/randomcolor" + plcolors "gopkg.in/go-playground/colors.v1" log "github.com/sirupsen/logrus" ) @@ -34,10 +35,44 @@ func labelToSeed(key string, val string) int64 { return seed } +func rgbToBrightness(r, g, b uint8) int32 { + return ((int32(r) * 299) + (int32(g) * 587) + (int32(b) * 114)) / 1000 +} + // ColorLabel update karmaColorMap object with a color object generated // from label key and value passed here // It's used to generate unique colors for configured labels func ColorLabel(colorStore models.LabelsColorMap, key string, val string) { + // first handle custom colors + _, ok := config.Config.Labels.Color.Custom[key] + if ok { + c, ol := config.Config.Labels.Color.Custom[key][val] + if ol { + color, err := plcolors.Parse(c) + if err != nil { + log.Warningf("Failed to parse custom color for %s=%s: %s", key, val, err) + return + } + rgb := color.ToRGB() + bc := models.Color{ + Red: rgb.R, + Green: rgb.G, + Blue: rgb.B, + Alpha: 255, + } + brightness := rgbToBrightness(bc.Red, bc.Green, bc.Blue) + if _, found := colorStore[key]; !found { + colorStore[key] = make(map[string]models.LabelColors) + } + colorStore[key][val] = models.LabelColors{ + Brightness: brightness, + Background: bc, + } + } + return + } + + // if no custom color is found then generate unique colors if needed if slices.StringInSlice(config.Config.Labels.Color.Unique, key) { if _, found := colorStore[key]; !found { colorStore[key] = make(map[string]models.LabelColors) @@ -54,7 +89,7 @@ func ColorLabel(colorStore models.LabelsColorMap, key string, val string) { } // check if color is bright or dark and pick the right background // uses https://www.w3.org/WAI/ER/WD-AERT/#color-contrast method - brightness := ((int32(bc.Red) * 299) + (int32(bc.Green) * 587) + (int32(bc.Blue) * 114)) / 1000 + brightness := rgbToBrightness(bc.Red, bc.Green, bc.Blue) colorStore[key][val] = models.LabelColors{ Brightness: brightness, Background: bc, From 8f068f2cade5ac7681872940551142d3101896c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 4 Feb 2019 13:19:11 +0000 Subject: [PATCH 2/3] feat(demo): add severity labels with custom colors --- demo/generator.py | 25 ++++++++++++++++--------- demo/karma.yaml | 5 +++++ 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/demo/generator.py b/demo/generator.py index 35f11c359..abef696e4 100755 --- a/demo/generator.py +++ b/demo/generator.py @@ -136,7 +136,8 @@ class AlwaysOnAlert(AlertGenerator): def alerts(self): def _gen(size, cluster): return [newAlert( - self._labels(instance="server{}".format(i), cluster=cluster), + self._labels(instance="server{}".format(i), cluster=cluster, + severity="info"), self._annotations( summary="Silence this alert, it's always firing") ) for i in xrange(1, size)] @@ -151,7 +152,8 @@ class RandomInstances(AlertGenerator): instances = random.randint(0, 30) return [ newAlert( - self._labels(instance="server{}".format(i), cluster="staging"), + self._labels(instance="server{}".format(i), cluster="staging", + severity="warning"), self._annotations( dashboard="https://www.google.com/search?q=" "server{}".format(i)) @@ -171,7 +173,7 @@ class RandomName(AlertGenerator): newAlert( self._labels(alertname="Alert Nr {}".format(throw), instance="server{}".format(i), - cluster="dev"), + cluster="dev", severity="info"), self._annotations( summary="This is a random alert", dashboard="https://www.google.com/search?q=" @@ -191,7 +193,8 @@ class LowChance(AlertGenerator): return [] return [ newAlert( - self._labels(instance="server{}".format(i), cluster="dev"), + self._labels(instance="server{}".format(i), cluster="dev", + severity="critical"), self._annotations() ) for i in xrange(0, 3) ] @@ -204,7 +207,8 @@ class TimeAnnotation(AlertGenerator): def alerts(self): return [ - newAlert(self._labels(instance="server1", cluster="prod"), + newAlert(self._labels(instance="server1", cluster="prod", + severity="warning"), self._annotations(time=str(int(time.time()))) ) ] @@ -222,7 +226,7 @@ class DiskFreeLowAlert(AlertGenerator): newAlert(self._labels(instance="server{}".format(i), cluster="prod", device="/dev/sda{}".format(i), - mount_point="/disk"), + mount_point="/disk", severity="critical"), self._annotations( summary="Only {}% free space left on /disk".format( spaceFree), @@ -238,7 +242,8 @@ class SilencedAlert(AlertGenerator): def alerts(self): return [ - newAlert(self._labels(instance="server1", cluster="prod"), + newAlert(self._labels(instance="server1", cluster="prod", + severity="info"), self._annotations( alertReference="https://www." "youtube.com/watch?v=dQw4w9WgXcQ") @@ -265,7 +270,8 @@ class MixedAlerts(AlertGenerator): def alerts(self): return [ - newAlert(self._labels(instance="server{}".format(i), cluster="prod"), + newAlert(self._labels(instance="server{}".format(i), cluster="prod", + severity="warning"), self._annotations( alertReference="https://www." "youtube.com/watch?v=dQw4w9WgXcQ") @@ -295,7 +301,8 @@ class LongNameAlerts(AlertGenerator): def alerts(self): def _gen(size, cluster): return [newAlert( - self._labels(instance="server{}".format(i), cluster=cluster), + self._labels(instance="server{}".format(i), cluster=cluster, + severity="info"), self._annotations( verylong="Lorem ipsum dolor sit amet, consectetur " "adipiscing elit, sed do eiusmod tempor incididunt" diff --git a/demo/karma.yaml b/demo/karma.yaml index 8f9f1e2ee..32bac3ed6 100644 --- a/demo/karma.yaml +++ b/demo/karma.yaml @@ -25,6 +25,11 @@ labels: - cluster - instance - "@receiver" + custom: + severity: + info: "#87c4e0" + warning: "#ffae42" + critical: "#ff220c" log: config: false level: warning From 0fdb6274cdbcbc87c7d05fb7c0b9b9ea01c93524 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Mon, 4 Feb 2019 13:27:18 +0000 Subject: [PATCH 3/3] feat(docs): document custom label colors --- docs/CONFIGURATION.md | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index e925db6f3..3f62fc58f 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -263,6 +263,7 @@ labels: color: static: [] unique: [] + custom: {} keep: list of strings strip: list of strings ``` @@ -274,6 +275,13 @@ labels: quickly learn which instance is affected by given alert. - `color:unique` - list of label names that should have unique colors generated in the UI. +- `color:custom` - nested map of label names and value with colors - this allows + to configure a set of labels with custom predefined colors applied to them + rather than generated. Colors can be defined as RGB or HEX values. + Value is a mapping with `label name` -> `label value` -> `color`, see examples + below. + Note: this option is not available via environment variables, you can only set + it via the config file. - `keep` - list of allowed labels, if empty all labels are allowed. - `strip` - list of ignored labels. @@ -282,8 +290,8 @@ same color regardless of the value) and unique color for the `@receiver` label (every `@receiver` label will have color unique for each value). ```yaml -colors: - labels: +labels: + color: static: - job unique: @@ -309,6 +317,19 @@ labels: strip: [] ``` +Example where `severity` label will have a red color for `critical`, yellow +for `warning` and blue for `info`: + +```yaml +labels: + color: + custom: + severity: + info: "#87c4e0" + warning: "#ffae42" + critical: "#ff220c" +``` + Defaults: ```yaml @@ -316,6 +337,7 @@ labels: color: static: [] unique: [] + custom: {} keep: [] strip: [] ```