diff --git a/Gopkg.lock b/Gopkg.lock index e315a3ea0..bc8319cff 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -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 diff --git a/assets/templates/index.html b/assets/templates/index.html index b2eb79523..5d093ab5c 100644 --- a/assets/templates/index.html +++ b/assets/templates/index.html @@ -41,7 +41,7 @@ autocomplete="off" value="{{ .QFilter }}" data-default-used="{{ .DefaultUsed }}" - data-default-filter="{{ .Config.FilterDefault }}" + data-default-filter="{{ .DefaultFilter }}" autofocus> diff --git a/internal/alertmanager/dedup.go b/internal/alertmanager/dedup.go index f4e29cb95..145dd868d 100644 --- a/internal/alertmanager/dedup.go +++ b/internal/alertmanager/dedup.go @@ -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() diff --git a/internal/alertmanager/dedup_test.go b/internal/alertmanager/dedup_test.go index bf8b424ab..068d956dd 100644 --- a/internal/alertmanager/dedup_test.go +++ b/internal/alertmanager/dedup_test.go @@ -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)) diff --git a/internal/config/config.go b/internal/config/config.go index a262e9916..0a8a44a31 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -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 -// -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()) + } } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 905d0f0d9..352d99418 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -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 { diff --git a/internal/config/legacy.go b/internal/config/legacy.go new file mode 100644 index 000000000..5d37ed1e8 --- /dev/null +++ b/internal/config/legacy.go @@ -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 + } +} diff --git a/internal/config/models.go b/internal/config/models.go new file mode 100644 index 000000000..95b778d64 --- /dev/null +++ b/internal/config/models.go @@ -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 + } +} diff --git a/internal/config/sanitize.go b/internal/config/sanitize.go new file mode 100644 index 000000000..f21ec2d06 --- /dev/null +++ b/internal/config/sanitize.go @@ -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 +} diff --git a/internal/models/annotation.go b/internal/models/annotation.go index 573a40536..861fb3a4d 100644 --- a/internal/models/annotation.go +++ b/internal/models/annotation.go @@ -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 } diff --git a/internal/models/jira.go b/internal/models/jira.go new file mode 100644 index 000000000..bd02ce178 --- /dev/null +++ b/internal/models/jira.go @@ -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 +} diff --git a/internal/transform/color_test.go b/internal/transform/color_test.go index cf42f72c7..a72e30c3e 100644 --- a/internal/transform/color_test.go +++ b/internal/transform/color_test.go @@ -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) diff --git a/internal/transform/colors.go b/internal/transform/colors.go index cd346960a..17e20e9f0 100644 --- a/internal/transform/colors.go +++ b/internal/transform/colors.go @@ -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) } diff --git a/internal/transform/jira.go b/internal/transform/jira.go index 47eeab2fc..7f549e6c7 100644 --- a/internal/transform/jira.go +++ b/internal/transform/jira.go @@ -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) } diff --git a/internal/transform/jira_test.go b/internal/transform/jira_test.go index 4d30603d9..7cceab69e 100644 --- a/internal/transform/jira_test.go +++ b/internal/transform/jira_test.go @@ -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{ diff --git a/main.go b/main.go index 216c677da..d64e72d4c 100644 --- a/main.go +++ b/main.go @@ -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)) } diff --git a/views.go b/views.go index 3f0da66b1..f06bf70d2 100644 --- a/views.go +++ b/views.go @@ -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) }