mirror of
https://github.com/prymitive/karma
synced 2026-05-05 03:16:51 +00:00
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:
68
Gopkg.lock
generated
68
Gopkg.lock
generated
@@ -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
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
autocomplete="off"
|
||||
value="{{ .QFilter }}"
|
||||
data-default-used="{{ .DefaultUsed }}"
|
||||
data-default-filter="{{ .Config.FilterDefault }}"
|
||||
data-default-filter="{{ .DefaultFilter }}"
|
||||
autofocus>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
78
internal/config/legacy.go
Normal 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
58
internal/config/models.go
Normal 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
|
||||
}
|
||||
}
|
||||
17
internal/config/sanitize.go
Normal file
17
internal/config/sanitize.go
Normal 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
|
||||
}
|
||||
@@ -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
8
internal/models/jira.go
Normal 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
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
56
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))
|
||||
}
|
||||
|
||||
16
views.go
16
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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user