Merge pull request #6 from cloudflare/viper

Generate flag for each environment key
This commit is contained in:
Łukasz Mierzwa
2017-03-26 17:15:49 -07:00
committed by GitHub
16 changed files with 309 additions and 175 deletions

138
README.md
View File

@@ -35,20 +35,10 @@ By default unsee will listen on port `8080` and Alertmanager mock data will be
used, to override Alertmanager URI set `ALERTMANAGER_URI` and/or `PORT` make
variables. Example:
make PORT=5000 ALERTMANAGER_URI=https://alertmanager.unicorn.corp run
make PORT=5000 ALERTMANAGER_URI=https://alertmanager.example.com run
### Environment variables
#### ALERTMANAGER_URI
URI of the Alertmanager instance, unsee will use it to pull alert groups and
silences. Endpoints in use:
* ${ALERTMANAGER_URI}/api/v1/alerts/groups
* ${ALERTMANAGER_URI}/api/v1/silences
This variable is required and there is no default value.
#### ALERTMANAGER_TIMEOUT
Timeout for requests send to Alertmanager, accepts values in
@@ -57,8 +47,45 @@ Timeout for requests send to Alertmanager, accepts values in
ALERTMANAGER_TIMEOUT=10s
ALERTMANAGER_TIMEOUT=2m
This option can also be set using `-alertmanager.timeout` flag. Example:
$ unsee -alertmanager.timeout 2m
Default is `40s`.
#### ALERTMANAGER_TTL
Interval for refreshing alerts and silences, tells unsee how often pull new
data from Alertmanager, accepts values in
[time.Duration](https://golang.org/pkg/time/#Duration) format. Examples:
ALERTMANAGER_TTL=30s
ALERTMANAGER_TTL=5m
This option can also be set using `-alertmanager.ttl` flag. Example:
$ unsee -alertmanager.ttl 5m
Default is `1m`.
#### ALERTMANAGER_URI
URI of the Alertmanager instance, unsee will use it to pull alert groups and
silences. Endpoints in use:
* ${ALERTMANAGER_URI}/api/v1/alerts/groups
* ${ALERTMANAGER_URI}/api/v1/silences
Example:
ALERTMANAGER_URI=https://alertmanager.example.com
This option can also be set using `-alertmanager.uri` flag. Example:
$ unsee -alertmanager.uri https://alertmanager.example.com
This variable is required and there is no default value.
#### DEBUG
Will enable [gin](https://github.com/gin-gonic/gin) debug mode. Examples:
@@ -66,29 +93,60 @@ Will enable [gin](https://github.com/gin-gonic/gin) debug mode. Examples:
DEBUG=true
DEBUG=false
This option can also be set using `-debug` flag. Example:
$ unsee -debug
Default is `false`.
#### COLOR_LABELS
#### COLOR_LABELS_STATIC
List of label names that will all have the same color applied (different than
the default label color). This allows to quickly spot a specific label that
can have high range of values, but it's important when reading the dashboard.
For example coloring the instance label allows to quickly learn which instance
is affected by given alert. Accepts space separated list of label names.
Examples:
COLOR_LABELS_STATIC=instance
COLOR_LABELS_STATIC="instance cluster"
This option can also be set using `-color.labels.static` flag. Example:
$ unsee -color.labels.static "instance cluster"
This variable is optional and default is not set (no label will have static
color).
#### COLOR_LABELS_UNIQUE
List of label names that should have unique colors generated in the UI. Colors
can help visually identify alerts with shared labels, for example coloring
hostname label will allow to quickly spot all alerts for the same host.
Accepts space separated list of label names. Examples:
COLOR_LABELS=hostname
COLOR_LABELS="cluster environment rack"
COLOR_LABELS_UNIQUE=hostname
COLOR_LABELS_UNIQUE="cluster environment rack"
This option can also be set using `-color.labels.unique` flag. Example:
$ unsee -color.labels.unique "cluster environment rack"
This variable is optional and default is not set (no label will have unique
color).
#### DEFAULT_FILTER
#### FILTER_DEFAULT
Default alert filter to apply when user loads unsee UI without any filter
specified. Accepts comma separated list of filter expressions (visit /help page
in unsee for details on filters). Examples:
DEFAULT_FILTER=level=critical
DEFAULT_FILTER="cluster=prod,instance=~prod"
FILTER_DEFAULT=level=critical
FILTER_DEFAULT="cluster=prod,instance=~prod"
This option can also be set using `-filter.default` flag. Example:
$ unsee -filter.default "cluster=prod,instance=~prod"
Default is not set (no filter will be applied).
@@ -103,24 +161,17 @@ Rule syntax:
Accepts space separated list of rules. Examples:
JIRA_REGEX="DEVOPS-[0-9]+@https://jira.unicorn.corp
JIRA_REGEX="DEVOPS-[0-9]+@https://jira.example.com"
The above will match DEVOPS-123 text in the silence comment string and convert
it to `https://jira.unicorn.corp/browse/DEVOPS-123` link.
it to `https://jira.example.com/browse/DEVOPS-123` link.
This option can also be set using `-jira.regex` flag. Example:
$ unsee -jira.regex "DEVOPS-[0-9]+@https://jira.example.com"
This variable is optional and default is not set (no rule will be applied).
#### UPDATE_INTERVAL
Interval for refreshing alerts and silences, tells unsee how often pull new
data from Alertmanager, accepts values in
[time.Duration](https://golang.org/pkg/time/#Duration) format. Examples:
UPDATE_INTERVAL=30s
UPDATE_INTERVAL=5m
Default is `1m`.
#### SENTRY_DSN
DSN for [Sentry](https://sentry.io) integration in Go. See
@@ -129,6 +180,10 @@ details. Example:
SENTRY_DSN=https://<key>:<secret>@sentry.io/<project>
This option can also be set using `-sentry.dsn` flag. Example:
$ unsee -sentry.dsn "https://<key>:<secret>@sentry.io/<project>"
This variable is optional and default is not set (Sentry support is disabled for
Go errors).
@@ -140,24 +195,13 @@ Example:
SENTRY_PUBLIC_DSN=https://<key>@sentry.io/<project>
This option can also be set using `-sentry.public.dsn` flag. Example:
$ unsee -sentry.public.dsn "https://<key>@sentry.io/<project>"
This variable is optional and default is not set (Sentry support is disabled for
javascript errors).
#### STATIC_COLOR_LABELS
List of label names that will all have the same color applied (different than
the default label color). This allows to quickly spot a specific label that
can have high range of values, but it's important when reading the dashboard.
For example coloring the instance label allows to quickly learn which instance
is affected by given alert. Accepts space separated list of label names.
Examples:
STATIC_COLOR_LABELS=instance
STATIC_COLOR_LABELS="instance cluster"
This variable is optional and default is not set (no label will have static
color).
#### STRIP_LABELS
List of label names that should not be shown on the UI. This allows to hide some
@@ -167,4 +211,8 @@ of label names. Examples:
STRIP_LABELS=exporter_type
STRIP_LABELS="prometheus_instance alert_type"
This option can also be set using `-strip.labels` flag. Example:
$ unsee -strip.labels "prometheus_instance alert_type"
This variable is optional and default is not set (all labels will be shown).

View File

@@ -3,6 +3,7 @@ package alertmanager
import (
"errors"
"time"
"github.com/cloudflare/unsee/config"
"github.com/cloudflare/unsee/models"
@@ -12,21 +13,21 @@ import (
// AlertGroupsAPIResponse is the schema of API response for /api/v1/alerts/groups
type AlertGroupsAPIResponse struct {
Status string `json:"status"`
Groups []models.AlertManagerAlertGroup `json:"data"`
Groups []models.AlertmanagerAlertGroup `json:"data"`
ErrorType string `json:"errorType"`
Error string `json:"error"`
}
// Get response from AlertManager /api/v1/alerts/groups
// Get response from Alertmanager /api/v1/alerts/groups
func (response *AlertGroupsAPIResponse) Get() error {
start := time.Now()
url, err := joinURL(config.Config.AlertManagerURL, "api/v1/alerts/groups")
url, err := joinURL(config.Config.AlertmanagerURI, "api/v1/alerts/groups")
if err != nil {
return err
}
err = getJSONFromURL(url, config.Config.AlertManagerTimeout, response)
err = getJSONFromURL(url, config.Config.AlertmanagerTimeout, response)
if err != nil {
return err
}

View File

@@ -45,7 +45,7 @@ func getJSONFromURL(url string, timeout time.Duration, target interface{}) error
}
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("Request to AlertManager failed with %s", resp.Status)
return fmt.Errorf("Request to Alertmanager failed with %s", resp.Status)
}
defer resp.Body.Close()

View File

@@ -5,6 +5,7 @@ import (
"fmt"
"math"
"time"
"github.com/cloudflare/unsee/config"
"github.com/cloudflare/unsee/models"
@@ -12,11 +13,11 @@ import (
)
type silencesData struct {
Silences []models.AlertManagerSilence `json:"silences"`
Silences []models.AlertmanagerSilence `json:"silences"`
TotalSilences int `json:"totalSilences"`
}
// SilenceAPIResponse is what AlertManager API returns
// SilenceAPIResponse is what Alertmanager API returns
type SilenceAPIResponse struct {
Status string `json:"status"`
Data silencesData `json:"data"`
@@ -24,17 +25,17 @@ type SilenceAPIResponse struct {
Error string `json:"error"`
}
// Get will return fresh data from AlertManager API
// Get will return fresh data from Alertmanager API
func (response *SilenceAPIResponse) Get() error {
start := time.Now()
url, err := joinURL(config.Config.AlertManagerURL, "api/v1/silences")
url, err := joinURL(config.Config.AlertmanagerURI, "api/v1/silences")
if err != nil {
return err
}
url = fmt.Sprintf("%s?limit=%d", url, math.MaxUint32)
err = getJSONFromURL(url, config.Config.AlertManagerTimeout, response)
err = getJSONFromURL(url, config.Config.AlertmanagerTimeout, response)
if err != nil {
return err
}

View File

@@ -1,9 +1,14 @@
package config
import (
"bytes"
"flag"
"fmt"
"os"
"reflect"
"strings"
"time"
"unicode"
log "github.com/Sirupsen/logrus"
"github.com/kelseyhightower/envconfig"
@@ -17,27 +22,102 @@ func (mvd *spaceSeparatedList) Decode(value string) error {
}
type configEnvs struct {
Debug bool `envconfig:"DEBUG" default:"false"`
AlertManagerURL string `envconfig:"ALERTMANAGER_URI" required:"true"`
AlertManagerTimeout time.Duration `envconfig:"ALERTMANAGER_TIMEOUT" default:"40s"`
UpdateInterval time.Duration `envconfig:"UPDATE_INTERVAL" default:"1m"`
SentryDSN string `envconfig:"SENTRY_DSN"`
SentryPublicDSN string `envconfig:"SENTRY_PUBLIC_DSN"`
DefaultFilter string `envconfig:"DEFAULT_FILTER"`
ColorLabels spaceSeparatedList `envconfig:"COLOR_LABELS"`
StaticColorLabels spaceSeparatedList `envconfig:"STATIC_COLOR_LABELS"`
StripLabels spaceSeparatedList `envconfig:"STRIP_LABELS"`
JIRARegexp spaceSeparatedList `envconfig:"JIRA_REGEX"`
AlertmanagerTimeout time.Duration `envconfig:"ALERTMANAGER_TIMEOUT" default:"40s" help:"Timeout for all request send to Alertmanager"`
AlertmanagerTTL time.Duration `envconfig:"ALERTMANAGER_TTL" default:"1m" help:"TTL for Alertmanager alerts and silences"`
AlertmanagerURI string `envconfig:"ALERTMANAGER_URI" required:"true" help:"Alertmanager URI"`
ColorLabelsStatic spaceSeparatedList `envconfig:"COLOR_LABELS_STATIC" help:"List of label names that should have the same (but distinct) color"`
ColorLabelsUnique spaceSeparatedList `envconfig:"COLOR_LABELS_UNIQUE" help:"List of label names that should have unique color"`
Debug bool `envconfig:"DEBUG" default:"false" help:"Enable debug mode"`
FilterDefault string `envconfig:"FILTER_DEFAULT" help:"Default filter string"`
JiraRegexp spaceSeparatedList `envconfig:"JIRA_REGEX" help:"List of JIRA regex rules"`
SentryDSN string `envconfig:"SENTRY_DSN" help:"Sentry DSN for Go exceptions"`
SentryPublicDSN string `envconfig:"SENTRY_PUBLIC_DSN" help:"Sentry DSN for javascript exceptions"`
StripLabels spaceSeparatedList `envconfig:"STRIP_LABELS" help:"List of labels to ignore"`
}
// Config exposes all options required to run
var Config configEnvs
//
// generate flag name from the option name, a dot will be injected between
// <lower case char><upper case char>
func makeFlagName(s string) string {
var buffer bytes.Buffer
prevUpper := true
for _, rune := range s {
if unicode.IsUpper(rune) && !prevUpper {
buffer.WriteRune('.')
}
prevUpper = unicode.IsUpper(rune)
buffer.WriteRune(unicode.ToLower(rune))
}
return buffer.String()
}
// Iterate all defined envconfig variables and generate a flag for each key.
// Next parse those flags and for each set flag inject env variable which will
// be read by envconfig later on.
type flagMapper struct {
isBool bool
stringVal *string
boolVal *bool
}
func mapEnvConfigToFlags() {
flags := make(map[string]flagMapper)
s := reflect.ValueOf(Config)
typeOfSpec := s.Type()
for i := 0; i < s.NumField(); i++ {
f := typeOfSpec.Field(i)
flagName := makeFlagName(f.Name)
// check if flag was already set, this usually happens only during testing
if flag.Lookup(flagName) != nil {
continue
}
envName := f.Tag.Get("envconfig")
defaultVal := f.Tag.Get("default")
helpMsg := fmt.Sprintf("%s. This flag can also be set via %s environment variable.", f.Tag.Get("help"), f.Tag.Get("envconfig"))
if f.Tag.Get("required") == "true" {
helpMsg = fmt.Sprintf("%s This option is required.", helpMsg)
}
mapper := flagMapper{}
if s.Field(i).Kind() == reflect.Bool {
mapper.isBool = true
mapper.boolVal = flag.Bool(flagName, false, helpMsg)
} else {
mapper.stringVal = flag.String(flagName, defaultVal, helpMsg)
}
flags[envName] = mapper
}
flag.Parse()
for envName, mapper := range flags {
if mapper.isBool {
if *mapper.boolVal == true {
err := os.Setenv(envName, "true")
if err != nil {
log.Fatal(err)
}
}
} else {
if *mapper.stringVal != "" {
err := os.Setenv(envName, *mapper.stringVal)
if err != nil {
log.Fatal(err)
}
}
}
}
}
func (config *configEnvs) Read() {
mapEnvConfigToFlags()
err := envconfig.Process("", config)
if err != nil {
log.Fatal(err.Error())
log.Fatal(err)
}
s := reflect.ValueOf(config).Elem()

View File

@@ -2,12 +2,12 @@ package filters_test
import (
"encoding/json"
"strconv"
"testing"
"time"
"github.com/cloudflare/unsee/filters"
"github.com/cloudflare/unsee/models"
"github.com/cloudflare/unsee/store"
"strconv"
"testing"
"time"
)
type filterTest struct {
@@ -34,13 +34,13 @@ var tests = []filterTest{
filterTest{
Expression: "@silenced=true",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
IsMatch: true,
},
filterTest{
Expression: "@silenced!=true",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
IsMatch: false,
},
filterTest{
@@ -51,225 +51,225 @@ var tests = []filterTest{
filterTest{
Expression: "@silence_jira=1",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1}, JiraID: "1"},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1}, JiraID: "1"},
IsMatch: true,
},
filterTest{
Expression: "@silence_jira=2",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1}},
IsMatch: false,
},
filterTest{
Expression: "@silence_jira!=3",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1}, JiraID: "x"},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1}, JiraID: "x"},
IsMatch: true,
},
filterTest{
Expression: "@silence_jira!=4",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1}, JiraID: "4"},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1}, JiraID: "4"},
IsMatch: false,
},
filterTest{
Expression: "@silence_jira!=5",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1}},
IsMatch: true,
},
filterTest{
Expression: "@silence_jira=~abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1}, JiraID: "xxabcxx"},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1}, JiraID: "xxabcxx"},
IsMatch: true,
},
filterTest{
Expression: "@silence_jira=~abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1}, JiraID: "xxx"},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1}, JiraID: "xxx"},
IsMatch: false,
},
filterTest{
Expression: "@silence_author=john",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1, CreatedBy: "john"}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1, CreatedBy: "john"}},
IsMatch: true,
},
filterTest{
Expression: "@silence_author=john",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1, CreatedBy: "bob"}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1, CreatedBy: "bob"}},
IsMatch: false,
},
filterTest{
Expression: "@silence_author!=john",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1, CreatedBy: "bob"}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1, CreatedBy: "bob"}},
IsMatch: true,
},
filterTest{
Expression: "@silence_author!=john",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1, CreatedBy: "john"}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1, CreatedBy: "john"}},
IsMatch: false,
},
filterTest{
Expression: "@silence_author!=john",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1}},
IsMatch: true,
},
filterTest{
Expression: "@age<1h",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{StartsAt: time.Now().Add(time.Minute * -55)}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{StartsAt: time.Now().Add(time.Minute * -55)}},
IsMatch: true,
},
filterTest{
Expression: "@age>1h",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{StartsAt: time.Now().Add(time.Hour * -2)}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{StartsAt: time.Now().Add(time.Hour * -2)}},
IsMatch: true,
},
filterTest{
Expression: "@age<-1h",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{StartsAt: time.Now().Add(time.Minute * -55)}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{StartsAt: time.Now().Add(time.Minute * -55)}},
IsMatch: true,
},
filterTest{
Expression: "@age>-1h",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{StartsAt: time.Now().Add(time.Hour * -2)}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{StartsAt: time.Now().Add(time.Hour * -2)}},
IsMatch: true,
},
filterTest{
Expression: "node=vps1",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Labels: map[string]string{"node": "vps1"}}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Labels: map[string]string{"node": "vps1"}}},
IsMatch: true,
},
filterTest{
Expression: "node=vps1",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{}},
IsMatch: false,
},
filterTest{
Expression: "node!=vps1",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Labels: map[string]string{"node": "vps1"}}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Labels: map[string]string{"node": "vps1"}}},
IsMatch: false,
},
filterTest{
Expression: "node!=vps1",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Labels: map[string]string{"node": "vps2"}}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Labels: map[string]string{"node": "vps2"}}},
IsMatch: true,
},
filterTest{
Expression: "node=~vps",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Labels: map[string]string{"node": "vps1"}}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Labels: map[string]string{"node": "vps1"}}},
IsMatch: true,
},
filterTest{
Expression: "node!~vps",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Labels: map[string]string{"node": "vps1"}}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Labels: map[string]string{"node": "vps1"}}},
IsMatch: false,
},
filterTest{
Expression: "node!~abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Labels: map[string]string{"node": "vps1"}}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Labels: map[string]string{"node": "vps1"}}},
IsMatch: true,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Labels: map[string]string{"key": "abc"}}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Labels: map[string]string{"key": "abc"}}},
IsMatch: true,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Labels: map[string]string{"key": "XXXabcx"}}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Labels: map[string]string{"key": "XXXabcx"}}},
IsMatch: true,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Labels: map[string]string{"abc": "xxxab"}}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Labels: map[string]string{"abc": "xxxab"}}},
IsMatch: false,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Annotations: map[string]string{"key": "abc"}}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Annotations: map[string]string{"key": "abc"}}},
IsMatch: true,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Annotations: map[string]string{"key": "ccc abc"}}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Annotations: map[string]string{"key": "ccc abc"}}},
IsMatch: true,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Annotations: map[string]string{"abc": "zzz"}}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Annotations: map[string]string{"abc": "zzz"}}},
IsMatch: false,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1, Comment: "abc"}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1, Comment: "abc"}},
IsMatch: true,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1, Comment: "abcxxx"}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1, Comment: "abcxxx"}},
IsMatch: true,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1, Comment: "ABCD"}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1, Comment: "ABCD"}},
IsMatch: true,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertManagerSilence: models.AlertManagerSilence{ID: 1, Comment: "xzc"}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{Silenced: 1}},
Silence: models.UnseeSilence{AlertmanagerSilence: models.AlertmanagerSilence{ID: 1, Comment: "xzc"}},
IsMatch: false,
},
filterTest{
Expression: "abc",
IsValid: true,
Alert: models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{}},
Alert: models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{}},
IsMatch: false,
},
filterTest{
@@ -365,7 +365,7 @@ func TestLimitFilter(t *testing.T) {
t.Errorf("[%s] GetIsValid() returned %#v while %#v was expected", ft.Expression, f.GetIsValid(), ft.IsValid)
}
if f.GetIsValid() {
alert := models.UnseeAlert{AlertManagerAlert: models.AlertManagerAlert{}}
alert := models.UnseeAlert{AlertmanagerAlert: models.AlertmanagerAlert{}}
var index int = 0
for _, isMatch := range ft.IsMatch {
m := f.Match(&alert, index)

33
main.go
View File

@@ -3,6 +3,7 @@ package main
import (
"sync"
"time"
"github.com/cloudflare/unsee/config"
"github.com/cloudflare/unsee/transform"
@@ -20,7 +21,7 @@ var (
version = "dev"
// ticker is a timer used by background loop that will keep pulling
// data from AlertManager
// data from Alertmanager
ticker *time.Ticker
// apiCache will be used to keep short lived copy of JSON reponses generated for the UI
@@ -28,31 +29,31 @@ var (
// rather than do all the filtering every time
apiCache *cache.Cache
// errorLock holds a mutex used to synchronize updates to AlertManagerError
// errorLock holds a mutex used to synchronize updates to AlertmanagerError
// to avoid any race between readers and writers
errorLock = sync.RWMutex{}
// alertManagerError holds the description of last error raised when pulling data
// from AlertManager, if there was any error
// from Alertmanager, if there was any error
// This error will be returned in UnseeAlertsResponse and presented by Ui
alertManagerError string
metricAlerts = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "unsee_collected_alerts",
Help: "Total number of alerts collected from AlertManager API",
Help: "Total number of alerts collected from Alertmanager API",
},
[]string{"silenced"},
)
metricAlertGroups = prometheus.NewGauge(
prometheus.GaugeOpts{
Name: "unsee_collected_groups",
Help: "Total number of alert groups collected from AlertManager API",
Help: "Total number of alert groups collected from Alertmanager API",
},
)
metricAlertManagerErrors = prometheus.NewGaugeVec(
metricAlertmanagerErrors = prometheus.NewGaugeVec(
prometheus.GaugeOpts{
Name: "unsee_alertmanager_errors_total",
Help: "Total number of errors encounter when requesting data from AlertManager API",
Help: "Total number of errors encounter when requesting data from Alertmanager API",
},
[]string{"endpoint"},
)
@@ -61,27 +62,27 @@ var (
func init() {
prometheus.MustRegister(metricAlerts)
prometheus.MustRegister(metricAlertGroups)
prometheus.MustRegister(metricAlertManagerErrors)
prometheus.MustRegister(metricAlertmanagerErrors)
metricAlertManagerErrors.With(prometheus.Labels{"endpoint": "alerts"}).Set(0)
metricAlertManagerErrors.With(prometheus.Labels{"endpoint": "silences"}).Set(0)
metricAlertmanagerErrors.With(prometheus.Labels{"endpoint": "alerts"}).Set(0)
metricAlertmanagerErrors.With(prometheus.Labels{"endpoint": "silences"}).Set(0)
}
func main() {
log.Infof("Version: %s", version)
config.Config.Read()
transform.ParseRules(config.Config.JIRARegexp)
transform.ParseRules(config.Config.JiraRegexp)
apiCache = cache.New(cache.NoExpiration, 10*time.Second)
// before we start try to fetch data from AlertManager
log.Infof("Initial AlertManager query, this can delay startup up to %s", 2*config.Config.AlertManagerTimeout)
PullFromAlertManager()
// before we start try to fetch data from Alertmanager
log.Infof("Initial Alertmanager query, this can delay startup up to %s", 2*config.Config.AlertmanagerTimeout)
PullFromAlertmanager()
log.Info("Done, starting HTTP server")
// background loop that will fetch updates from AlertManager
ticker = time.NewTicker(config.Config.UpdateInterval)
// background loop that will fetch updates from Alertmanager
ticker = time.NewTicker(config.Config.AlertmanagerTTL)
go Tick()
switch config.Config.Debug {

View File

@@ -2,8 +2,8 @@ package models
import "time"
// AlertManagerAlert is vanilla alert object from AlertManager
type AlertManagerAlert struct {
// AlertmanagerAlert is vanilla alert object from Alertmanager
type AlertmanagerAlert struct {
Annotations map[string]string `json:"annotations"`
Labels map[string]string `json:"labels"`
StartsAt time.Time `json:"startsAt"`
@@ -13,16 +13,16 @@ type AlertManagerAlert struct {
Silenced int `json:"silenced"`
}
// AlertManagerAlertGroup is vanilla group object from AlertManager, exposed under api/v1/alerts/groups
type AlertManagerAlertGroup struct {
// AlertmanagerAlertGroup is vanilla group object from Alertmanager, exposed under api/v1/alerts/groups
type AlertmanagerAlertGroup struct {
Labels map[string]string `json:"labels"`
Blocks []struct {
Alerts []AlertManagerAlert `json:"alerts"`
Alerts []AlertmanagerAlert `json:"alerts"`
} `json:"blocks"`
}
// AlertManagerSilence is vanilla silence object from AlertManager, exposed under api/v1/silences
type AlertManagerSilence struct {
// AlertmanagerSilence is vanilla silence object from Alertmanager, exposed under api/v1/silences
type AlertmanagerSilence struct {
ID int `json:"id"`
Matchers []struct {
Name string `json:"name"`
@@ -41,7 +41,7 @@ type AlertManagerSilence struct {
// extracted ID is used to generate link to JIRA issue
// this means Unsee needs to store additional fields for each silence
type UnseeSilence struct {
AlertManagerSilence
AlertmanagerSilence
JiraID string `json:"jiraID"`
JiraURL string `json:"jiraURL"`
}
@@ -52,7 +52,7 @@ type UnseeSilence struct {
// and returned under links field, Unsee UI used this to show links differently
// than other annotations
type UnseeAlert struct {
AlertManagerAlert
AlertmanagerAlert
Links map[string]string `json:"links"`
}
@@ -72,7 +72,7 @@ func (a UnseeAlertList) Less(i, j int) bool {
return a[i].StartsAt.Round(2 * time.Second).After(a[j].StartsAt.Round(2 * time.Second))
}
// UnseeAlertGroup is vanilla AlertManager group, but alerts are flattened
// UnseeAlertGroup is vanilla Alertmanager group, but alerts are flattened
// There is a hash computed from all alerts, it's used by UI to quickly tell
// if there was any change in a group and it needs to refresh it
type UnseeAlertGroup struct {

View File

@@ -3,6 +3,7 @@ package store
import (
"sync"
"time"
"github.com/cloudflare/unsee/models"
)
@@ -31,10 +32,10 @@ var (
// (alerts, silences, colors, ac)
StoreLock = sync.RWMutex{}
// AlertStore holds all alerts retrieved from AlertManager
// AlertStore holds all alerts retrieved from Alertmanager
AlertStore = alertStoreType{}
// SilenceStore holds all silences retrieved from AlertManager
// SilenceStore holds all silences retrieved from Alertmanager
SilenceStore = silenceStoreType{}
// ColorStore holds all color maps generated from alerts

View File

@@ -47,7 +47,7 @@
autocomplete="off"
value="{{ .QFilter }}"
data-default-used="{{ .DefaultUsed }}"
data-default-filter="{{ .Config.DefaultFilter }}"
data-default-filter="{{ .Config.FilterDefault }}"
autofocus>
</div>
</div>

View File

@@ -8,6 +8,7 @@ import (
"sort"
"strconv"
"time"
"github.com/cloudflare/unsee/alertmanager"
"github.com/cloudflare/unsee/config"
"github.com/cloudflare/unsee/models"
@@ -19,10 +20,10 @@ import (
"github.com/prometheus/client_golang/prometheus"
)
// PullFromAlertManager will try to fetch latest alerts and silences
// from AlertManager API, it's called by Ticker timer
func PullFromAlertManager() {
log.Info("Pulling latest alerts and silences from AlertManager")
// PullFromAlertmanager will try to fetch latest alerts and silences
// from Alertmanager API, it's called by Ticker timer
func PullFromAlertmanager() {
log.Info("Pulling latest alerts and silences from Alertmanager")
silenceResponse := alertmanager.SilenceAPIResponse{}
err := silenceResponse.Get()
@@ -31,7 +32,7 @@ func PullFromAlertManager() {
errorLock.Lock()
alertManagerError = err.Error()
errorLock.Unlock()
metricAlertManagerErrors.With(prometheus.Labels{"endpoint": "silences"}).Inc()
metricAlertmanagerErrors.With(prometheus.Labels{"endpoint": "silences"}).Inc()
return
}
@@ -42,7 +43,7 @@ func PullFromAlertManager() {
errorLock.Lock()
alertManagerError = err.Error()
errorLock.Unlock()
metricAlertManagerErrors.With(prometheus.Labels{"endpoint": "alerts"}).Inc()
metricAlertmanagerErrors.With(prometheus.Labels{"endpoint": "alerts"}).Inc()
return
}
@@ -50,7 +51,7 @@ func PullFromAlertManager() {
for _, silence := range silenceResponse.Data.Silences {
jiraID, jiraLink := transform.DetectJIRAs(&silence)
silenceStore[strconv.Itoa(silence.ID)] = models.UnseeSilence{
AlertManagerSilence: silence,
AlertmanagerSilence: silence,
JiraID: jiraID,
JiraURL: jiraLink,
}
@@ -93,7 +94,7 @@ func PullFromAlertManager() {
for _, alertBlock := range alertGroup.Blocks {
for _, alert := range alertBlock.Alerts {
apiAlert := models.UnseeAlert{AlertManagerAlert: alert}
apiAlert := models.UnseeAlert{AlertmanagerAlert: alert}
apiAlert.Annotations, apiAlert.Links = transform.DetectLinks(apiAlert.Annotations)
@@ -163,12 +164,12 @@ func PullFromAlertManager() {
runtime.GC()
}
// Tick is the background timer used to call PullFromAlertManager
// Tick is the background timer used to call PullFromAlertmanager
func Tick() {
for {
select {
case <-ticker.C:
PullFromAlertManager()
PullFromAlertmanager()
}
}
}

View File

@@ -2,10 +2,10 @@ package transform
import (
"crypto/sha1"
"io"
"math/rand"
"github.com/cloudflare/unsee/config"
"github.com/cloudflare/unsee/models"
"io"
"math/rand"
"github.com/hansrodtang/randomcolor"
)
@@ -25,7 +25,7 @@ func labelToSeed(key string, val string) int64 {
// from label key and value passed here
// It's used to generate unique colors for configured labels
func ColorLabel(colorStore models.UnseeColorMap, key string, val string) {
if stringInSlice(config.Config.ColorLabels, key) == true {
if stringInSlice(config.Config.ColorLabelsUnique, key) == true {
if _, found := colorStore[key]; !found {
colorStore[key] = make(map[string]models.UnseeLabelColor)
}

View File

@@ -5,6 +5,7 @@ import (
"log"
"regexp"
"strings"
"github.com/cloudflare/unsee/models"
)
@@ -33,10 +34,10 @@ func ParseRules(rules []string) {
}
}
// DetectJIRAs will try to find JIRA links in AlertManager silence objects
// DetectJIRAs will try to find JIRA links in Alertmanager silence objects
// using regexp rules from configuration that were parsed and populated
// by ParseRules call
func DetectJIRAs(silence *models.AlertManagerSilence) (jiraID, jiraLink string) {
func DetectJIRAs(silence *models.AlertmanagerSilence) (jiraID, jiraLink string) {
for _, jdr := range jiraDetectRules {
jiraID := jdr.Regexp.FindString(silence.Comment)
if jiraID != "" {

View File

@@ -12,7 +12,7 @@ func StripLables(ignoredLabels []string, sourceLabels map[string]string) map[str
for label, value := range sourceLabels {
if !stringInSlice(ignoredLabels, label) {
// strip leading and trailung space in label value
// this is to normalize values in case space is added by AlertManager rules
// this is to normalize values in case space is added by Alertmanager rules
labels[label] = strings.TrimSpace(value)
}
}

View File

@@ -4,15 +4,15 @@ import (
"crypto/sha1"
"encoding/json"
"fmt"
"github.com/cloudflare/unsee/config"
"github.com/cloudflare/unsee/models"
"github.com/cloudflare/unsee/store"
"io"
"net/http"
"sort"
"strconv"
"strings"
"time"
"github.com/cloudflare/unsee/config"
"github.com/cloudflare/unsee/models"
"github.com/cloudflare/unsee/store"
log "github.com/Sirupsen/logrus"
"github.com/gin-gonic/gin"
@@ -54,7 +54,7 @@ func Index(c *gin.Context) {
"Config": config.Config,
"QFilter": q,
"DefaultUsed": defaultUsed,
"StaticColorLabels": strings.Join(config.Config.StaticColorLabels, " "),
"StaticColorLabels": strings.Join(config.Config.ColorLabelsStatic, " "),
})
log.Infof("[%s] %s %s took %s", c.ClientIP(), c.Request.Method, c.Request.RequestURI, time.Since(start))

View File

@@ -22,7 +22,7 @@ import (
func mockConfig() {
log.SetLevel(log.FatalLevel)
os.Setenv("ALERTMANAGER_URI", "http://localhost")
os.Setenv("COLOR_LABELS", "alertname")
os.Setenv("COLOR_LABELS_UNIQUE", "alertname")
config.Config.Read()
}
@@ -132,7 +132,7 @@ func mockAlerts() {
}`
httpmock.RegisterResponder("GET", "http://localhost/api/v1/alerts/groups", httpmock.NewStringResponder(200, alerts))
PullFromAlertManager()
PullFromAlertmanager()
}
func TestAlerts(t *testing.T) {