Rewrite flag & env handling to use viper

This adds support for reading configuration from file, env support is still there and legacy env variables will still work, but flags are now following config schema, so they don't match old flags.
Having a config file allows to express more complex configuration options, which is needed for some feature requests.
This commit is contained in:
Łukasz Mierzwa
2017-11-26 19:08:54 -08:00
parent b52ab532e6
commit 6273a5a585
17 changed files with 465 additions and 208 deletions

68
Gopkg.lock generated
View File

@@ -37,6 +37,12 @@
packages = ["."]
revision = "30f82fa23fd844bd5bb1e5f216db87fd77b5eb43"
[[projects]]
name = "github.com/fsnotify/fsnotify"
packages = ["."]
revision = "629574ca2a5df945712d3079857300b5e4da0236"
version = "v1.4.2"
[[projects]]
branch = "master"
name = "github.com/getsentry/raven-go"
@@ -85,12 +91,24 @@
packages = ["."]
revision = "d27108b3d7a5d9bc11b8c51c558f60eda5da1b84"
[[projects]]
branch = "master"
name = "github.com/hashicorp/hcl"
packages = [".","hcl/ast","hcl/parser","hcl/scanner","hcl/strconv","hcl/token","json/parser","json/scanner","json/token"]
revision = "23c074d0eceb2b8a5bfdbb271ab780cde70f05a8"
[[projects]]
name = "github.com/kelseyhightower/envconfig"
packages = ["."]
revision = "f611eb38b3875cc3bd991ca91c51d06446afa14c"
version = "v1.3.0"
[[projects]]
name = "github.com/magiconair/properties"
packages = ["."]
revision = "be5ece7dd465ab0765a9682137865547526d1dfb"
version = "v1.7.3"
[[projects]]
name = "github.com/mattn/go-isatty"
packages = ["."]
@@ -109,12 +127,24 @@
packages = ["."]
revision = "56be4856691683575a5906cfa770e658aae2ae0a"
[[projects]]
branch = "master"
name = "github.com/mitchellh/mapstructure"
packages = ["."]
revision = "06020f85339e21b2478f756a78e295255ffa4d6a"
[[projects]]
name = "github.com/patrickmn/go-cache"
packages = ["."]
revision = "1881a9bccb818787f68c52bfba648c6cf34c34fa"
version = "v2.0.0"
[[projects]]
name = "github.com/pelletier/go-toml"
packages = ["."]
revision = "16398bac157da96aa88f98a2df640c7f32af1da2"
version = "v1.0.1"
[[projects]]
name = "github.com/prometheus/client_golang"
packages = ["prometheus","prometheus/promhttp"]
@@ -145,6 +175,36 @@
revision = "f006c2ac4710855cf0f916dd6b77acf6b048dc6e"
version = "v1.0.3"
[[projects]]
name = "github.com/spf13/afero"
packages = [".","mem"]
revision = "8d919cbe7e2627e417f3e45c3c0e489a5b7e2536"
version = "v1.0.0"
[[projects]]
name = "github.com/spf13/cast"
packages = ["."]
revision = "acbeb36b902d72a7a4c18e8f3241075e7ab763e4"
version = "v1.1.0"
[[projects]]
branch = "master"
name = "github.com/spf13/jwalterweatherman"
packages = ["."]
revision = "12bd96e66386c1960ab0f74ced1362f66f552f7b"
[[projects]]
name = "github.com/spf13/pflag"
packages = ["."]
revision = "e57e3eeb33f795204c1ca35f56c44f83227c6e66"
version = "v1.0.0"
[[projects]]
name = "github.com/spf13/viper"
packages = ["."]
revision = "25b30aa063fc18e48662b86996252eabdcf2f0c7"
version = "v1.0.0"
[[projects]]
branch = "master"
name = "github.com/ugorji/go"
@@ -163,6 +223,12 @@
packages = ["unix","windows"]
revision = "7ddbeae9ae08c6a06a59597f0c9edbc5ff2444ce"
[[projects]]
branch = "master"
name = "golang.org/x/text"
packages = ["internal/gen","internal/triegen","internal/ucd","transform","unicode/cldr","unicode/norm"]
revision = "88f656faf3f37f690df1a32515b479415e1a6769"
[[projects]]
name = "gopkg.in/go-playground/validator.v8"
packages = ["."]
@@ -184,6 +250,6 @@
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "32bf99922b6d83811ed209762176a18be21b3a747affed35d9b6a266d53434b6"
inputs-digest = "0fe21ce1c3a725f7f6020630d021f1c02361478ce5df6713499e7a3141313902"
solver-name = "gps-cdcl"
solver-version = 1

View File

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

View File

@@ -65,11 +65,11 @@ func DedupAlerts() []models.AlertGroup {
for _, alert := range alerts {
// remove all alerts for receiver(s) that the user doesn't
// want to see in the UI
if transform.StripReceivers(config.Config.StripReceivers, alert.Receiver) {
if transform.StripReceivers(config.Config.Receivers.Strip, alert.Receiver) {
continue
}
// strip labels user doesn't want to see in the UI
alert.Labels = transform.StripLables(config.Config.KeepLabels, config.Config.StripLabels, alert.Labels)
alert.Labels = transform.StripLables(config.Config.Labels.Keep, config.Config.Labels.Strip, alert.Labels)
// calculate final alert state based on the most important value found
// in the list of states from all instances
alertLFP := alert.LabelsFingerprint()

View File

@@ -50,12 +50,12 @@ func TestDedupAlerts(t *testing.T) {
}
func TestDedupAlertsWithoutLabels(t *testing.T) {
config.Config.KeepLabels = []string{"xyz"}
config.Config.Labels.Keep = []string{"xyz"}
if err := pullAlerts(); err != nil {
t.Error(err)
}
alertGroups := alertmanager.DedupAlerts()
config.Config.KeepLabels = []string{}
config.Config.Labels.Keep = []string{}
if len(alertGroups) != 10 {
t.Errorf("Expected %d alert groups, got %d", 10, len(alertGroups))

View File

@@ -1,155 +1,165 @@
package config
import (
"bufio"
"bytes"
"flag"
"fmt"
"net/url"
"os"
"reflect"
"path"
"strings"
"time"
"unicode"
"github.com/kelseyhightower/envconfig"
"github.com/spf13/pflag"
"github.com/spf13/viper"
log "github.com/sirupsen/logrus"
yaml "gopkg.in/yaml.v2"
)
type spaceSeparatedList []string
var (
// Config will hold final configuration read from the file and flags
Config configSchema
func (mvd *spaceSeparatedList) Decode(value string) error {
*mvd = spaceSeparatedList(strings.Split(value, " "))
return nil
configDir string
configFile string
)
func init() {
pflag.Duration("alertmanager.interval", time.Second*60,
"Interval for fetching data from Alertmanager servers")
pflag.Bool(
"annotations.default.hidden", false,
"Hide all annotations by default unless explicitly listed in the 'visible' list")
pflag.StringSlice("annotations.hidden", []string{},
"List of annotations that are hidden by default")
pflag.StringSlice("annotations.visible", []string{},
"List of annotations that are visible by default")
pflag.StringSlice("colors.labels.static", []string{},
"List of label names that should have the same (but distinct) color")
pflag.StringSlice("colors.labels.unique", []string{},
"List of label names that should have unique color")
pflag.StringVar(&configDir, "config.dir", ".",
"Directory with configuration file to read")
pflag.StringVar(&configFile, "config.file", "unsee",
"Name of the configuration file to read")
pflag.Bool("debug", false, "Enable debug mode")
pflag.StringSlice("filters.default", []string{}, "List of default filters")
pflag.StringSlice("labels.keep", []string{},
"List of labels to keep, all other labels will be stripped")
pflag.StringSlice("labels.strip", []string{}, "List of labels to ignore")
pflag.String("log.level", "info",
"Log level, one of: debug, info, warning, error, fatal and panic")
pflag.StringSlice("receivers.strip", []string{},
"List of receivers to not display alerts for")
pflag.Int("listen.port", 8080, "HTTP port to listen on")
pflag.String("listen.prefix", "/", "URL prefix")
pflag.String("sentry.public", "", "Sentry DSN for Go exceptions")
pflag.String("sentry.private", "", "Sentry DSN for JavaScript exceptions")
}
type configEnvs struct {
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"`
AlertmanagerURIs spaceSeparatedList `envconfig:"ALERTMANAGER_URIS" required:"true" help:"List of Alertmanager URIs (name:uri)"`
AnnotationsHidden spaceSeparatedList `envconfig:"ANNOTATIONS_HIDDEN" help:"List of annotations that are hidden by default"`
AnnotationsDefaultHidden bool `envconfig:"ANNOTATIONS_DEFAULT_HIDDEN" default:"false" help:"Hide all annotations by default unless listed in ANNOTATIONS_VISIBLE"`
AnnotationsVisible spaceSeparatedList `envconfig:"ANNOTATIONS_VISIBLE" help:"List of annotations that are visible by default"`
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"`
Port int `envconfig:"PORT" default:"8080" help:"HTTP port to listen on"`
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"`
StripReceivers spaceSeparatedList `envconfig:"STRIP_RECEIVERS" help:"List of receivers to not display alerts for"`
KeepLabels spaceSeparatedList `envconfig:"KEEP_LABELS" help:"List of labels to keep, all other labels will be stripped"`
WebPrefix string `envconfig:"WEB_PREFIX" default:"/" help:"URL prefix"`
}
// ReadConfig will read all sources of configuration, merge all keys and
// populate global Config variable, it should be only called on startup
func (config *configSchema) Read() {
v := viper.New()
// Config exposes all options required to run
var Config configEnvs
pflag.Parse()
// 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))
pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
pflag.Parse()
v.BindPFlags(pflag.CommandLine)
v.AutomaticEnv()
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// special envs
// HOST and PORT is used by gin
v.BindEnv("listen.address", "HOST")
v.BindEnv("listen.port", "PORT")
// raven-go expects this
v.BindEnv("sentry.private", "SENTRY_DSN")
// bind legacy env variables
config.legacyEnvs(v)
v.SetConfigType("yaml")
v.SetConfigName(configFile)
v.AddConfigPath(configDir)
log.Infof("Reading configuration file %s.yaml", path.Join(configDir, configFile))
err := v.ReadInConfig()
if v.ConfigFileUsed() != "" && err != nil {
log.Fatal(err)
}
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")
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, "", helpMsg)
}
flags[envName] = mapper
if v.ConfigFileUsed() != "" {
log.Infof("Config file used: %s", v.ConfigFileUsed())
}
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()
config.Alertmanager.Interval = v.GetDuration("alertmanager.interval")
config.Annotations.Default.Hidden = v.GetBool("annotations.default.hidden")
config.Annotations.Hidden = v.GetStringSlice("annotations.hidden")
config.Annotations.Visible = v.GetStringSlice("annotations.visible")
config.Colors.Labels.Static = v.GetStringSlice("colors.labels.static")
config.Colors.Labels.Unique = v.GetStringSlice("colors.labels.unique")
config.Debug = v.GetBool("debug")
config.Filters.Default = v.GetStringSlice("filters.default")
config.Labels.Keep = v.GetStringSlice("labels.keep")
config.Labels.Strip = v.GetStringSlice("labels.strip")
config.Listen.Address = v.GetString("listen.address")
config.Listen.Port = v.GetInt("listen.port")
config.Listen.Prefix = v.GetString("listen.prefix")
config.Log.Level = v.GetString("log.level")
config.Receivers.Strip = v.GetStringSlice("receivers.strip")
config.Sentry.Private = v.GetString("sentry.private")
config.Sentry.Public = v.GetString("sentry.public")
err := envconfig.Process("", config)
err = v.UnmarshalKey("alertmanager.servers", &config.Alertmanager.Servers)
if err != nil {
log.Fatal(err)
}
}
func hideURLPassword(s string) string {
u, err := url.Parse(s)
err = v.UnmarshalKey("jira", &config.JIRA)
if err != nil {
return s
log.Fatal(err)
}
if u.User != nil {
if _, pwdSet := u.User.Password(); pwdSet {
u.User = url.UserPassword(u.User.Username(), "xxx")
// populate legacy settings if needed
config.legacySettingsFallback()
}
// LogValues will dump runtime config to logs
func (config *configSchema) LogValues() {
// make a copy of our config so we can edit it
cfg := configSchema(*config)
// replace passwords in Alertmanager URIs with 'xxx'
servers := []alertmanagerConfig{}
for _, s := range cfg.Alertmanager.Servers {
server := alertmanagerConfig{
Name: s.Name,
URI: hideURLPassword(s.URI),
Timeout: s.Timeout,
}
return u.String()
servers = append(servers, server)
}
return s
}
cfg.Alertmanager.Servers = servers
func (config *configEnvs) LogValues() {
s := reflect.ValueOf(config).Elem()
typeOfT := s.Type()
for i := 0; i < s.NumField(); i++ {
env := typeOfT.Field(i).Tag.Get("envconfig")
val := fmt.Sprintf("%v", s.Field(i).Interface())
log.Infof("%20s => %v", env, hideURLPassword(val))
out, err := yaml.Marshal(cfg)
if err != nil {
log.Error(err)
}
log.Info("Parsed configuration:")
scanner := bufio.NewScanner(bytes.NewReader(out))
for scanner.Scan() {
log.Info(scanner.Text())
}
}

View File

@@ -6,53 +6,53 @@ import (
"time"
"github.com/cloudflare/unsee/internal/slices"
log "github.com/sirupsen/logrus"
)
type flagNameTest struct {
env string
flag string
}
var flagNameTests = []flagNameTest{
flagNameTest{env: "MyEnv", flag: "my.env"},
flagNameTest{env: "MyENV", flag: "my.env"},
flagNameTest{env: "MYEnv", flag: "myenv"},
}
func TestMakeFlagName(t *testing.T) {
for _, testCase := range flagNameTests {
generatedFlag := makeFlagName(testCase.env)
if generatedFlag != testCase.flag {
t.Errorf("Invalid flag name generated from env '%s', expected '%s', got '%s'", testCase.env, testCase.flag, generatedFlag)
}
func testReadConfig(t *testing.T) {
if Config.Alertmanager.Interval != time.Second {
t.Errorf("Config.Alertmanager.Interval is invalid, expected 1s, got %v", Config.Alertmanager.Interval)
}
if Config.Debug != true {
t.Errorf("Config.Debug is %v with env DEBUG=true set", Config.Debug)
}
if !slices.StringInSlice(Config.Colors.Labels.Static, "a") {
t.Errorf("Config.Colors.Labels.Static is missing value 'a': %v", Config.Colors.Labels.Static)
}
if !slices.StringInSlice(Config.Colors.Labels.Static, "bb") {
t.Errorf("Config.Colors.Labels.Static is missing value 'bb': %v", Config.Colors.Labels.Static)
}
if !slices.StringInSlice(Config.Colors.Labels.Static, "ccc") {
t.Errorf("Config.Colors.Labels.Static is missing value 'ccc': %v", Config.Colors.Labels.Static)
}
if Config.Listen.Port != 8080 {
t.Errorf("Config.Listen.Port is invalid, expected 8080, got %v", Config.Listen.Port)
}
if len(Config.Labels.Keep) != 0 {
t.Errorf("Config.Labels.Keep is not empty, got %v", Config.Labels.Keep)
}
}
func TestReadConfig(t *testing.T) {
func TestReadConfigLegacy(t *testing.T) {
log.SetLevel(log.ErrorLevel)
os.Setenv("ALERTMANAGER_TTL", "1s")
os.Setenv("ALERTMANAGER_URIS", "default:http://localhost")
os.Setenv("DEBUG", "true")
os.Setenv("COLOR_LABELS_STATIC", "a bb ccc")
Config.Read()
if Config.AlertmanagerTTL != time.Second {
t.Errorf("Config.AlertmanagerTTL is invalid, expected 1s, got %v", Config.AlertmanagerTTL)
}
if Config.Debug != true {
t.Errorf("Config.Debug is %v with env DEBUG=true set", Config.Debug)
}
if !slices.StringInSlice(Config.ColorLabelsStatic, "a") {
t.Errorf("Config.ColorLabelsStatic is missing value 'a': %v", Config.ColorLabelsStatic)
}
if !slices.StringInSlice(Config.ColorLabelsStatic, "bb") {
t.Errorf("Config.ColorLabelsStatic is missing value 'bb': %v", Config.ColorLabelsStatic)
}
if !slices.StringInSlice(Config.ColorLabelsStatic, "ccc") {
t.Errorf("Config.ColorLabelsStatic is missing value 'ccc': %v", Config.ColorLabelsStatic)
}
if Config.Port != 8080 {
t.Errorf("Config.Port is invalid, expected 8080, got %v", Config.Port)
}
testReadConfig(t)
}
func TestReadConfig(t *testing.T) {
log.SetLevel(log.ErrorLevel)
os.Setenv("ALERTMANAGER_INTERVAL", "1s")
os.Setenv("ALERTMANAGER_URIS", "default:http://localhost")
os.Setenv("DEBUG", "true")
os.Setenv("COLORS_LABELS_STATIC", "a bb ccc")
Config.Read()
testReadConfig(t)
}
type urlSecretTest struct {

78
internal/config/legacy.go Normal file
View File

@@ -0,0 +1,78 @@
package config
import (
"os"
"strings"
"time"
log "github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
func (config *configSchema) legacyEnvs(v *viper.Viper) {
// legacy env variables
v.BindEnv("alertmanager.interval", "ALERTMANAGER_TTL")
v.BindEnv("annotations.default.hidden", "ANNOTATIONS_DEFAULT_HIDDEN")
v.BindEnv("annotations.hidden", "ANNOTATIONS_HIDE")
v.BindEnv("annotations.visible", "ANNOTATIONS_SHOW")
v.BindEnv("colors.labels.static", "COLOR_LABELS_STATIC")
v.BindEnv("colors.labels.unique", "COLOR_LABELS_UNIQUE")
v.BindEnv("filters.default", "FILTER_DEFAULT")
v.BindEnv("labels.keep", "KEEP_LABELS")
v.BindEnv("labels.strip", "STRIP_LABELS")
v.BindEnv("listen.prefix", "WEB_PREFIX")
v.BindEnv("receivers.strip", "STRIP_RECEIVERS")
v.BindEnv("sentry.public", "SENTRY_PUBLIC_DSN")
}
func (config *configSchema) legacySettingsFallback() {
// no Alertmanager servers configured and legacy ALERTMANAGER_URIS is present
if len(config.Alertmanager.Servers) == 0 && os.Getenv("ALERTMANAGER_URIS") != "" {
log.Warn("ALERTMANAGER_URIS env variable is deprecated")
for _, s := range strings.Split(os.Getenv("ALERTMANAGER_URIS"), " ") {
z := strings.SplitN(s, ":", 2)
if len(z) != 2 {
log.Fatalf("Invalid Alertmanager URI '%s', expected format 'name:uri'", s)
continue
}
name := z[0]
uri := z[1]
ac := alertmanagerConfig{
Name: name,
URI: uri,
Timeout: time.Second * 40,
}
if os.Getenv("ALERTMANAGER_TIMEOUT") != "" {
log.Warn("ALERTMANAGER_TIMEOUT env variable is deprecated")
timeout, err := time.ParseDuration(os.Getenv("ALERTMANAGER_TIMEOUT"))
if err != nil {
log.Fatalf("Invalid ALERTMANAGER_TIMEOUT: %s", err)
}
ac.Timeout = timeout
}
config.Alertmanager.Servers = append(config.Alertmanager.Servers, ac)
}
}
// no default filters and legacy FILTER_DEFAULT is present
if len(config.Filters.Default) == 0 && os.Getenv("FILTER_DEFAULT") != "" {
log.Warn("FILTER_DEFAULT env variable is deprecated")
config.Filters.Default = strings.Split(os.Getenv("FILTER_DEFAULT"), ",")
}
// no jira rules configured and legacy JIRA_REGEX is present
if len(config.JIRA) == 0 && os.Getenv("JIRA_REGEX") != "" {
log.Warn("JIRA_REGEX env variable is deprecated")
rules := []jiraRule{}
for _, s := range strings.Split(os.Getenv("JIRA_REGEX"), " ") {
ss := strings.SplitN(s, "@", 2)
re := ss[0]
url := ss[1]
if re == "" || url == "" {
log.Fatalf("Invalid JIRA rule '%s', regexp part is '%s', url is '%s'", s, re, url)
}
rules = append(rules, jiraRule{Regex: re, URI: url})
}
config.JIRA = rules
}
}

58
internal/config/models.go Normal file
View File

@@ -0,0 +1,58 @@
package config
import "time"
type alertmanagerConfig struct {
Name string
URI string
Timeout time.Duration
}
type jiraRule struct {
Regex string
URI string
}
type configSchema struct {
Alertmanager struct {
Interval time.Duration
Servers []alertmanagerConfig
}
Annotations struct {
Default struct {
Hidden bool
}
Hidden []string
Visible []string
}
Colors struct {
Labels struct {
Static []string
Unique []string
}
}
Debug bool
Filters struct {
Default []string
}
Labels struct {
Strip []string
Keep []string
}
Listen struct {
Address string
Port int
Prefix string
}
Log struct {
Level string
}
JIRA []jiraRule
Receivers struct {
Strip []string
}
Sentry struct {
Private string
Public string
}
}

View File

@@ -0,0 +1,17 @@
package config
import "net/url"
func hideURLPassword(s string) string {
u, err := url.Parse(s)
if err != nil {
return s
}
if u.User != nil {
if _, pwdSet := u.User.Password(); pwdSet {
u.User = url.UserPassword(u.User.Username(), "xxx")
}
return u.String()
}
return s
}

View File

@@ -67,15 +67,15 @@ func isLink(s string) bool {
}
func isVisible(name string) bool {
if slices.StringInSlice(config.Config.AnnotationsVisible, name) {
if slices.StringInSlice(config.Config.Annotations.Visible, name) {
// annotation was explicitly marked as visible
return true
}
if slices.StringInSlice(config.Config.AnnotationsHidden, name) {
if slices.StringInSlice(config.Config.Annotations.Hidden, name) {
// annotation was explicitly marked as hidden
return false
}
if config.Config.AnnotationsDefaultHidden {
if config.Config.Annotations.Default.Hidden {
// user specified that default is to hide anything without explicit rules
return false
}

8
internal/models/jira.go Normal file
View File

@@ -0,0 +1,8 @@
package models
// JiraRule is used to detect JIRA issue IDs in strings and turn those into
// links
type JiraRule struct {
Regex string
URI string
}

View File

@@ -58,7 +58,7 @@ var colorTests = []colorTest{
func TestColorLabel(t *testing.T) {
for _, testCase := range colorTests {
config.Config.ColorLabelsUnique = testCase.config
config.Config.Colors.Labels.Unique = testCase.config
colorStore := models.LabelsColorMap{}
for key, value := range testCase.labels {
transform.ColorLabel(colorStore, key, value)

View File

@@ -27,7 +27,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.LabelsColorMap, key string, val string) {
if slices.StringInSlice(config.Config.ColorLabelsUnique, key) == true {
if slices.StringInSlice(config.Config.Colors.Labels.Unique, key) == true {
if _, found := colorStore[key]; !found {
colorStore[key] = make(map[string]models.LabelColors)
}

View File

@@ -4,7 +4,6 @@ import (
"fmt"
"log"
"regexp"
"strings"
"github.com/cloudflare/unsee/internal/models"
)
@@ -18,17 +17,14 @@ var jiraDetectRules = []jiraDetectRule{}
// ParseRules will parse and validate list of JIRA detection rules provided
// from config, valid rules will be stored for future use in DetectJIRAs() calls
func ParseRules(rules []string) {
for _, s := range rules {
ss := strings.SplitN(s, "@", 2)
re := ss[0]
url := ss[1]
if re == "" || url == "" {
log.Fatalf("Invalid JIRA rule '%s', regexp part is '%s', url is '%s'", s, re, url)
func ParseRules(rules []models.JiraRule) {
for _, rule := range rules {
if rule.Regex == "" || rule.URI == "" {
log.Fatalf("Invalid JIRA rule with regexp '%s' and url '%s'", rule.Regex, rule.URI)
}
jdr := jiraDetectRule{
Regexp: regexp.MustCompile(re),
URL: url,
Regexp: regexp.MustCompile(rule.Regex),
URL: rule.URI,
}
jiraDetectRules = append(jiraDetectRules, jdr)
}

View File

@@ -13,9 +13,15 @@ type jiraTest struct {
jiraLink string
}
var jiraRules = []string{
"DEVOPS-[0-9]+@https://jira.example.com",
"PROJECT-[0-9]+@https://example.com",
var jiraRules = []models.JiraRule{
models.JiraRule{
Regex: "DEVOPS-[0-9]+",
URI: "https://jira.example.com",
},
models.JiraRule{
Regex: "PROJECT-[0-9]+",
URI: "https://example.com",
},
}
var jiraTests = []jiraTest{

56
main.go
View File

@@ -8,6 +8,7 @@ import (
"github.com/cloudflare/unsee/internal/alertmanager"
"github.com/cloudflare/unsee/internal/config"
"github.com/cloudflare/unsee/internal/models"
"github.com/cloudflare/unsee/internal/transform"
"github.com/DeanThompson/ginpprof"
@@ -36,7 +37,7 @@ var (
)
func getViewURL(sub string) string {
u := path.Join(config.Config.WebPrefix, sub)
u := path.Join(config.Config.Listen.Prefix, sub)
if strings.HasSuffix(sub, "/") {
// if sub path had trailing slash then add it here, since path.Join will
// skip it
@@ -57,33 +58,50 @@ func setupRouter(router *gin.Engine) {
}
func setupUpstreams() {
for _, s := range config.Config.AlertmanagerURIs {
z := strings.SplitN(s, ":", 2)
if len(z) != 2 {
log.Fatalf("Invalid Alertmanager URI '%s', expected format 'name:uri'", s)
continue
}
name := z[0]
uri := z[1]
err := alertmanager.NewAlertmanager(name, uri, config.Config.AlertmanagerTimeout)
for _, s := range config.Config.Alertmanager.Servers {
err := alertmanager.NewAlertmanager(s.Name, s.URI, s.Timeout)
if err != nil {
log.Fatalf("Failed to configure Alertmanager '%s' with URI '%s': %s", name, uri, err)
log.Fatalf("Failed to configure Alertmanager '%s' with URI '%s': %s", s.Name, s.URI, err)
}
}
}
func main() {
log.Infof("Version: %s", version)
func setupLogger() {
switch config.Config.Log.Level {
case "debug":
log.SetLevel(log.DebugLevel)
case "info":
log.SetLevel(log.InfoLevel)
case "warning":
log.SetLevel(log.WarnLevel)
case "error":
log.SetLevel(log.ErrorLevel)
case "fatal":
log.SetLevel(log.FatalLevel)
case "panic":
log.SetLevel(log.PanicLevel)
default:
log.Fatalf("Unknown log level '%s'", config.Config.Log.Level)
}
}
func main() {
config.Config.Read()
setupLogger()
// timer duration cannot be zero second or a negative one
if config.Config.AlertmanagerTTL <= time.Second*0 {
log.Fatalf("Invalid AlertmanagerTTL value '%v'", config.Config.AlertmanagerTTL)
if config.Config.Alertmanager.Interval <= time.Second*0 {
log.Fatalf("Invalid AlertmanagerTTL value '%v'", config.Config.Alertmanager.Interval)
}
log.Infof("Version: %s", version)
config.Config.LogValues()
transform.ParseRules(config.Config.JiraRegexp)
jiraRules := []models.JiraRule{}
for _, rule := range config.Config.JIRA {
jiraRules = append(jiraRules, models.JiraRule{Regex: rule.Regex, URI: rule.URI})
}
transform.ParseRules(jiraRules)
apiCache = cache.New(cache.NoExpiration, 10*time.Second)
@@ -94,12 +112,12 @@ func main() {
}
// before we start try to fetch data from Alertmanager
log.Infof("Initial Alertmanager query, this can delay startup up to %s", 3*config.Config.AlertmanagerTimeout)
log.Info("Initial Alertmanager query")
pullFromAlertmanager()
log.Info("Done, starting HTTP server")
// background loop that will fetch updates from Alertmanager
ticker = time.NewTicker(config.Config.AlertmanagerTTL)
ticker = time.NewTicker(config.Config.Alertmanager.Interval)
go Tick()
switch config.Config.Debug {
@@ -124,7 +142,7 @@ func main() {
ginpprof.Wrapper(router)
}
if config.Config.SentryDSN != "" {
if config.Config.Sentry.Public != "" {
raven.SetRelease(version)
router.Use(sentry.Recovery(raven.DefaultClient, false))
}

View File

@@ -40,12 +40,12 @@ func index(c *gin.Context) {
c.HTML(http.StatusOK, "templates/index.html", gin.H{
"Version": version,
"SentryDSN": config.Config.SentryPublicDSN,
"Config": config.Config,
"SentryDSN": config.Config.Sentry.Public,
"QFilter": q,
"DefaultUsed": defaultUsed,
"StaticColorLabels": strings.Join(config.Config.ColorLabelsStatic, " "),
"WebPrefix": config.Config.WebPrefix,
"DefaultFilter": strings.Join(config.Config.Filters.Default, ","),
"StaticColorLabels": strings.Join(config.Config.Colors.Labels.Static, " "),
"WebPrefix": config.Config.Listen.Prefix,
})
log.Infof("[%s] %s %s took %s", c.ClientIP(), c.Request.Method, c.Request.RequestURI, time.Since(start))
@@ -56,8 +56,8 @@ func help(c *gin.Context) {
start := time.Now()
noCache(c)
c.HTML(http.StatusOK, "templates/help.html", gin.H{
"SentryDSN": config.Config.SentryPublicDSN,
"WebPrefix": config.Config.WebPrefix,
"SentryDSN": config.Config.Sentry.Public,
"WebPrefix": config.Config.Listen.Prefix,
})
log.Infof("[%s] <%d> %s %s took %s", c.ClientIP(), http.StatusOK, c.Request.Method, c.Request.RequestURI, time.Since(start))
}
@@ -249,8 +249,8 @@ func autocomplete(c *gin.Context) {
}
func favicon(c *gin.Context) {
if config.Config.WebPrefix != "/" {
c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, config.Config.WebPrefix)
if config.Config.Listen.Prefix != "/" {
c.Request.URL.Path = strings.TrimPrefix(c.Request.URL.Path, config.Config.Listen.Prefix)
}
faviconFileServer.ServeHTTP(c.Writer, c.Request)
}