diff --git a/README.md b/README.md index 77ad8b3ef..c12ca7843 100644 --- a/README.md +++ b/README.md @@ -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://:@sentry.io/ +This option can also be set using `-sentry.dsn` flag. Example: + + $ unsee -sentry.dsn "https://:@sentry.io/" + 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://@sentry.io/ +This option can also be set using `-sentry.public.dsn` flag. Example: + + $ unsee -sentry.public.dsn "https://@sentry.io/" + 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). diff --git a/alertmanager/alerts.go b/alertmanager/alerts.go index a0495e169..2c46a3469 100644 --- a/alertmanager/alerts.go +++ b/alertmanager/alerts.go @@ -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 } diff --git a/alertmanager/remote.go b/alertmanager/remote.go index 9de36d7cd..da44c9f41 100644 --- a/alertmanager/remote.go +++ b/alertmanager/remote.go @@ -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() diff --git a/alertmanager/silences.go b/alertmanager/silences.go index 17c6b3384..b63b03eb4 100644 --- a/alertmanager/silences.go +++ b/alertmanager/silences.go @@ -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 } diff --git a/config/config.go b/config/config.go index 28b33ab27..bf08dee4a 100644 --- a/config/config.go +++ b/config/config.go @@ -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 +// +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() diff --git a/filters/filter_test.go b/filters/filter_test.go index 4d9829da3..ae1f36d78 100644 --- a/filters/filter_test.go +++ b/filters/filter_test.go @@ -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) diff --git a/main.go b/main.go index 6d7e85efe..60a788d75 100644 --- a/main.go +++ b/main.go @@ -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 { diff --git a/models/models.go b/models/models.go index 096e07a14..3035b8faa 100644 --- a/models/models.go +++ b/models/models.go @@ -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 { diff --git a/store/store.go b/store/store.go index 99ec53e30..647010a1e 100644 --- a/store/store.go +++ b/store/store.go @@ -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 diff --git a/templates/index.html b/templates/index.html index 31a7876f2..38c5b7fd7 100644 --- a/templates/index.html +++ b/templates/index.html @@ -47,7 +47,7 @@ autocomplete="off" value="{{ .QFilter }}" data-default-used="{{ .DefaultUsed }}" - data-default-filter="{{ .Config.DefaultFilter }}" + data-default-filter="{{ .Config.FilterDefault }}" autofocus> diff --git a/timer.go b/timer.go index 58eda3663..5d275c414 100644 --- a/timer.go +++ b/timer.go @@ -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() } } } diff --git a/transform/colors.go b/transform/colors.go index e0733c74d..3e6a41e0a 100644 --- a/transform/colors.go +++ b/transform/colors.go @@ -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) } diff --git a/transform/jira.go b/transform/jira.go index a4f7eee5f..2e608a156 100644 --- a/transform/jira.go +++ b/transform/jira.go @@ -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 != "" { diff --git a/transform/strip.go b/transform/strip.go index eb2ad5ccb..464e10086 100644 --- a/transform/strip.go +++ b/transform/strip.go @@ -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) } } diff --git a/views.go b/views.go index 174d7cced..33e891fd1 100644 --- a/views.go +++ b/views.go @@ -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)) diff --git a/views_test.go b/views_test.go index 29fe645b9..1971013cd 100644 --- a/views_test.go +++ b/views_test.go @@ -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) {