diff --git a/cmd/cli.go b/cmd/cli.go index 5bdfa7735..1a4e4c1a3 100644 --- a/cmd/cli.go +++ b/cmd/cli.go @@ -42,5 +42,5 @@ func setCliOptions() { fmt.Printf("unable to bind flag '%s': %+v", flag, err) } - rootCmd.Flags().CountVarP(&cliOpts.Verbosity, "verbose", "v", "increase verbosity (-v, -vv, -vvv ...)") + rootCmd.Flags().CountVarP(&cliOpts.Verbosity, "verbose", "v", "increase verbosity (-v = info, -vv = debug)") } diff --git a/cmd/init.go b/cmd/init.go index 3a9bd9f57..2f82ec8bf 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -1,15 +1,16 @@ package cmd import ( - "encoding/json" "fmt" "os" "github.com/anchore/imgbom/imgbom" "github.com/anchore/imgbom/internal/config" + "github.com/anchore/imgbom/internal/format" "github.com/anchore/imgbom/internal/log" "github.com/anchore/imgbom/internal/logger" "github.com/spf13/viper" + "gopkg.in/yaml.v2" ) var appConfig *config.Application @@ -25,10 +26,10 @@ func initAppConfig() { func initLogging() { config := logger.LogConfig{ - EnableConsole: appConfig.Log.FileLocation == "" && !appConfig.Quiet, + EnableConsole: (appConfig.Log.FileLocation == "" || appConfig.CliOptions.Verbosity > 0) && !appConfig.Quiet, EnableFile: appConfig.Log.FileLocation != "", Level: appConfig.Log.LevelOpt, - FormatAsJSON: appConfig.Log.FormatAsJSON, + Structured: appConfig.Log.Structured, FileLocation: appConfig.Log.FileLocation, } @@ -36,10 +37,11 @@ func initLogging() { } func logAppConfig() { - appCfgStr, err := json.MarshalIndent(&appConfig, " ", " ") + appCfgStr, err := yaml.Marshal(&appConfig) + if err != nil { log.Debugf("Could not display application config: %+v", err) } else { - log.Debugf("Application config:\n%+v", string(appCfgStr)) + log.Debugf("Application config:\n%+v", format.Magenta.Format(string(appCfgStr))) } } diff --git a/go.mod b/go.mod index 7e808a8c9..2f313d7b8 100644 --- a/go.mod +++ b/go.mod @@ -14,4 +14,5 @@ require ( github.com/spf13/cobra v1.0.0 github.com/spf13/viper v1.7.0 go.uber.org/zap v1.15.0 + gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index 8cce2777e..bb8d56a26 100644 --- a/go.sum +++ b/go.sum @@ -131,6 +131,7 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= +github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= @@ -280,8 +281,10 @@ github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= +github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= diff --git a/imgbom/presenter/json/presenter.go b/imgbom/presenter/json/presenter.go index 785acec03..d2ff415ae 100644 --- a/imgbom/presenter/json/presenter.go +++ b/imgbom/presenter/json/presenter.go @@ -95,7 +95,6 @@ func (pres *Presenter) Present(output io.Writer, img *stereoscopeImg.Image, cata "id": src.ID(), "presenter": "json", }).Errorf("could not get metadata from catalog") - } srcObj := source{ diff --git a/internal/config/config.go b/internal/config/config.go index 3dd3f4c37..85f913527 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -20,9 +20,9 @@ type CliOnlyOptions struct { } type Application struct { - configPath string + ConfigPath string PresenterOpt presenter.Option - Presenter string `mapstructure:"output"` + Output string `mapstructure:"output"` ScopeOpt scope.Option Scope string `mapstructure:"scope"` Quiet bool `mapstructure:"quiet"` @@ -31,7 +31,7 @@ type Application struct { } type Logging struct { - FormatAsJSON bool `mapstructure:"structured"` + Structured bool `mapstructure:"structured"` LevelOpt zapcore.Level Level string `mapstructure:"level"` FileLocation string `mapstructure:"file"` @@ -59,7 +59,7 @@ func LoadConfigFromFile(v *viper.Viper, cliOpts *CliOnlyOptions) (*Application, if err != nil { return nil, fmt.Errorf("unable to parse config: %w", err) } - config.configPath = v.ConfigFileUsed() + config.ConfigPath = v.ConfigFileUsed() err = config.Build() if err != nil { @@ -71,9 +71,9 @@ func LoadConfigFromFile(v *viper.Viper, cliOpts *CliOnlyOptions) (*Application, func (cfg *Application) Build() error { // set the presenter - presenterOption := presenter.ParseOption(cfg.Presenter) + presenterOption := presenter.ParseOption(cfg.Output) if presenterOption == presenter.UnknownPresenter { - return fmt.Errorf("bad --output value '%s'", cfg.Presenter) + return fmt.Errorf("bad --output value '%s'", cfg.Output) } cfg.PresenterOpt = presenterOption @@ -100,10 +100,10 @@ func (cfg *Application) Build() error { } } else { // set the log level implicitly - switch cfg.CliOptions.Verbosity { - case 1: + switch v := cfg.CliOptions.Verbosity; { + case v == 1: cfg.Log.LevelOpt = zapcore.InfoLevel - case 2: + case v >= 2: cfg.Log.LevelOpt = zapcore.DebugLevel default: cfg.Log.LevelOpt = zapcore.ErrorLevel @@ -133,14 +133,21 @@ func readConfig(v *viper.Viper, configPath string) error { // start searching for valid configs in order... - // 1. look for ./config.yaml (in the current directory) + // 1. look for ..yaml (in the current directory) + v.AddConfigPath(".") + v.SetConfigName(internal.ApplicationName) + if err := v.ReadInConfig(); err == nil { + return nil + } + + // 2. look for ./config.yaml (in the current directory) v.AddConfigPath("." + internal.ApplicationName) v.SetConfigName("config") if err := v.ReadInConfig(); err == nil { return nil } - // 2. look for ~/..yaml + // 3. look for ~/..yaml home, err := homedir.Dir() if err == nil { v.AddConfigPath(home) @@ -150,7 +157,7 @@ func readConfig(v *viper.Viper, configPath string) error { } } - // 3. look for /config.yaml in xdg locations (starting with xdg home config dir, then moving upwards) + // 4. look for /config.yaml in xdg locations (starting with xdg home config dir, then moving upwards) v.AddConfigPath(path.Join(xdg.ConfigHome, internal.ApplicationName)) for _, dir := range xdg.ConfigDirs { v.AddConfigPath(path.Join(dir, internal.ApplicationName)) diff --git a/internal/format/color.go b/internal/format/color.go new file mode 100644 index 000000000..fa1757c34 --- /dev/null +++ b/internal/format/color.go @@ -0,0 +1,21 @@ +package format + +import "fmt" + +const ( + DefaultColor Color = iota + 30 + Red + Green + Yellow + Blue + Magenta + Cyan + White +) + +type Color uint8 + +// TODO: not cross platform (windows...) +func (c Color) Format(s string) string { + return fmt.Sprintf("\x1b[%dm%s\x1b[0m", c, s) +} diff --git a/internal/logger/zap.go b/internal/logger/zap.go index 53b02bde1..d8e56c807 100644 --- a/internal/logger/zap.go +++ b/internal/logger/zap.go @@ -4,14 +4,25 @@ import ( "os" "github.com/anchore/imgbom/imgbom/logger" + "github.com/anchore/imgbom/internal/format" "go.uber.org/zap" "go.uber.org/zap/zapcore" ) +var levelToColor = map[zapcore.Level]format.Color{ + zapcore.DebugLevel: format.Magenta, + zapcore.InfoLevel: format.Blue, + zapcore.WarnLevel: format.Yellow, + zapcore.ErrorLevel: format.Red, + zapcore.DPanicLevel: format.Red, + zapcore.PanicLevel: format.Red, + zapcore.FatalLevel: format.Red, +} + type LogConfig struct { EnableConsole bool EnableFile bool - FormatAsJSON bool + Structured bool Level zapcore.Level FileLocation string } @@ -20,6 +31,12 @@ type ZapLogger struct { sugaredLogger *zap.SugaredLogger } +// TODO: Consider a human readable text encoder for better field handeling: +// - https://github.com/uber-go/zap/issues/570 +// - https://github.com/uber-go/zap/pull/123 +// - TextEncoder w/ old interface: https://github.com/uber-go/zap/blob/6c2107996402d47d559199b78e1c44747fe732f9/text_encoder.go +// - New interface example: https://github.com/uber-go/zap/blob/c2633d6de2d6e1170ad8f150660e3cf5310067c8/zapcore/json_encoder.go +// - Register the encoder: https://github.com/uber-go/zap/blob/v1.15.0/encoder.go func NewZapLogger(config LogConfig) *ZapLogger { cores := []zapcore.Core{} @@ -31,8 +48,8 @@ func NewZapLogger(config LogConfig) *ZapLogger { } if config.EnableFile { - writer := zapcore.AddSync(getLogWriter(config.FileLocation)) - core := zapcore.NewCore(getFileEncoder(config), writer, config.Level) + writer := zapcore.AddSync(logFileWriter(config.FileLocation)) + core := zapcore.NewCore(fileEncoder(config), writer, config.Level) cores = append(cores, core) } @@ -50,27 +67,53 @@ func NewZapLogger(config LogConfig) *ZapLogger { } } +func (l *ZapLogger) GetNamedLogger(name string) *ZapLogger { + return &ZapLogger{ + sugaredLogger: l.sugaredLogger.Named(name), + } +} + func getConsoleEncoder(config LogConfig) zapcore.Encoder { encoderConfig := zap.NewProductionEncoderConfig() - if config.FormatAsJSON { + if config.Structured { + encoderConfig.EncodeName = zapcore.FullNameEncoder + encoderConfig.EncodeCaller = zapcore.FullCallerEncoder return zapcore.NewJSONEncoder(encoderConfig) } encoderConfig.EncodeTime = nil encoderConfig.EncodeCaller = nil - encoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder + encoderConfig.EncodeLevel = consoleLevelEncoder + encoderConfig.EncodeName = nameEncoder return zapcore.NewConsoleEncoder(encoderConfig) } -func getFileEncoder(config LogConfig) zapcore.Encoder { +func nameEncoder(loggerName string, enc zapcore.PrimitiveArrayEncoder) { + enc.AppendString("[" + loggerName + "]") +} + +func consoleLevelEncoder(level zapcore.Level, enc zapcore.PrimitiveArrayEncoder) { + if level != zapcore.InfoLevel { + color, ok := levelToColor[level] + if !ok { + enc.AppendString("[" + level.CapitalString() + "]") + } else { + enc.AppendString("[" + color.Format(level.CapitalString()) + "]") + } + } +} + +func fileEncoder(config LogConfig) zapcore.Encoder { encoderConfig := zap.NewProductionEncoderConfig() encoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder - if config.FormatAsJSON { + encoderConfig.EncodeName = zapcore.FullNameEncoder + encoderConfig.EncodeCaller = zapcore.FullCallerEncoder + if config.Structured { return zapcore.NewJSONEncoder(encoderConfig) } return zapcore.NewConsoleEncoder(encoderConfig) } -func getLogWriter(location string) zapcore.WriteSyncer { +func logFileWriter(location string) zapcore.WriteSyncer { file, _ := os.Create(location) return zapcore.AddSync(file) }