From 935c6290ebc54aa1c5b42ece633f895ee6acce27 Mon Sep 17 00:00:00 2001 From: Hank Donnay Date: Mon, 23 Oct 2023 19:40:28 -0500 Subject: [PATCH] config: add more option-ful logging configuration This moves the logging configuration from the single "level" key to top-level struct like all the new additions. This also does some internal shuffling of types and constants, taking inspiration from the `log/slog` package. Signed-off-by: Hank Donnay --- config/config.go | 33 ++++++++---- config/enums.go | 61 ---------------------- config/enums_string.go | 20 ++++---- config/enums_test.go | 37 +++++++------ config/logging.go | 114 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 168 insertions(+), 97 deletions(-) create mode 100644 config/logging.go diff --git a/config/config.go b/config/config.go index 02638cb747..f5248d818e 100644 --- a/config/config.go +++ b/config/config.go @@ -19,7 +19,7 @@ type Config struct { // environment variable is the recommended way to do that. The release // container has `/var/run/certs` added to the list already. TLS *TLS `yaml:"tls,omitempty" json:"tls,omitempty"` - // Sets which mode the clair instance will run. + // Sets which mode the Clair instance will run. Mode Mode `yaml:"-" json:"-"` // A string in : format where can be an empty string. // @@ -31,15 +31,18 @@ type Config struct { // exposes Clair's metrics and health endpoints. IntrospectionAddr string `yaml:"introspection_addr" json:"introspection_addr"` // Set the logging level. - LogLevel LogLevel `yaml:"log_level" json:"log_level"` - Indexer Indexer `yaml:"indexer,omitempty" json:"indexer,omitempty"` - Matcher Matcher `yaml:"matcher,omitempty" json:"matcher,omitempty"` - Matchers Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` - Updaters Updaters `yaml:"updaters,omitempty" json:"updaters,omitempty"` - Notifier Notifier `yaml:"notifier,omitempty" json:"notifier,omitempty"` - Auth Auth `yaml:"auth,omitempty" json:"auth,omitempty"` - Trace Trace `yaml:"trace,omitempty" json:"trace,omitempty"` - Metrics Metrics `yaml:"metrics,omitempty" json:"metrics,omitempty"` + // + // Deprecated: Use the "Logging" member. + LogLevel *LogLevel `yaml:"log_level,omitempty" json:"log_level,omitempty"` + Indexer Indexer `yaml:"indexer,omitempty" json:"indexer,omitempty"` + Matcher Matcher `yaml:"matcher,omitempty" json:"matcher,omitempty"` + Matchers Matchers `yaml:"matchers,omitempty" json:"matchers,omitempty"` + Updaters Updaters `yaml:"updaters,omitempty" json:"updaters,omitempty"` + Notifier Notifier `yaml:"notifier,omitempty" json:"notifier,omitempty"` + Auth Auth `yaml:"auth,omitempty" json:"auth,omitempty"` + Trace Trace `yaml:"trace,omitempty" json:"trace,omitempty"` + Metrics Metrics `yaml:"metrics,omitempty" json:"metrics,omitempty"` + Logging Logging `yaml:"logging,omitempty" json:"logging,omitempty"` } func (c *Config) validate(mode Mode) ([]Warning, error) { @@ -58,6 +61,10 @@ func (c *Config) validate(mode Mode) ([]Warning, error) { if _, _, err := net.SplitHostPort(c.HTTPListenAddr); err != nil { return nil, err } + if c.LogLevel != nil { + c.Logging.Level = *c.LogLevel + } + return c.lint() } @@ -74,6 +81,12 @@ func (c *Config) lint() (ws []Warning, err error) { msg: `introspection address not provided, default will be used`, }) } + if c.LogLevel != nil { + ws = append(ws, Warning{ + path: ".log_level", + msg: `"log_level" is deprecated, use "logging"`, + }) + } return ws, nil } diff --git a/config/enums.go b/config/enums.go index f76911c8ac..eb26474655 100644 --- a/config/enums.go +++ b/config/enums.go @@ -1,8 +1,6 @@ package config import ( - "encoding" - "errors" "fmt" "strings" ) @@ -35,62 +33,3 @@ func ParseMode(s string) (Mode, error) { } return Mode(-1), fmt.Errorf(`unknown mode %q`, s) } - -// A LogLevel is a log level recognized by Clair. -// -// The zero value is "info". -type LogLevel int - -// The recognized log levels, with their string representations as the comments. -// -// NB "Fatal" and "Panic" are not used in clair or claircore, and will result in -// almost no logging. -const ( - InfoLog LogLevel = iota // info - DebugColorLog // debug-color - DebugLog // debug - WarnLog // warn - ErrorLog // error - FatalLog // fatal - PanicLog // panic -) - -// ParseLogLevel returns the log lever for the given string. -// -// The passed string is case-insensitive. -func ParseLogLevel(s string) (LogLevel, error) { - for i, lim := 0, len(_LogLevel_index); i < lim; i++ { - l := LogLevel(i) - if strings.EqualFold(s, l.String()) { - return l, nil - } - } - return LogLevel(-1), fmt.Errorf(`unknown log level %q`, s) -} - -// UnmarshalText implements encoding.TextUnmarshaler. -func (l *LogLevel) UnmarshalText(b []byte) (err error) { - *l, err = ParseLogLevel(string(b)) - if err != nil { - return err - } - return nil -} - -// MarshalText implements encoding.TextMarshaler. -func (l *LogLevel) MarshalText() ([]byte, error) { - if l == nil { - return nil, errors.New("invalid LogLevel pointer: ") - } - i := *l - if i < 0 || i >= LogLevel(len(_LogLevel_index)-1) { - return nil, fmt.Errorf("invalid LogLevel: %q", l.String()) - } - return []byte(_LogLevel_name[_LogLevel_index[i]:_LogLevel_index[i+1]]), nil -} - -// Assert LogLevel implements TextUnmarshaler and TextMarshaler. -var ( - _ encoding.TextUnmarshaler = (*LogLevel)(nil) - _ encoding.TextMarshaler = (*LogLevel)(nil) -) diff --git a/config/enums_string.go b/config/enums_string.go index 665d39bf5b..565f158b80 100644 --- a/config/enums_string.go +++ b/config/enums_string.go @@ -28,22 +28,24 @@ func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} + _ = x[TraceLog - -3] + _ = x[DebugColorLog - -2] + _ = x[DebugLog - -1] _ = x[InfoLog-0] - _ = x[DebugColorLog-1] - _ = x[DebugLog-2] - _ = x[WarnLog-3] - _ = x[ErrorLog-4] - _ = x[FatalLog-5] - _ = x[PanicLog-6] + _ = x[WarnLog-1] + _ = x[ErrorLog-2] + _ = x[FatalLog-3] + _ = x[PanicLog-4] } -const _LogLevel_name = "infodebug-colordebugwarnerrorfatalpanic" +const _LogLevel_name = "tracedebug-colordebuginfowarnerrorfatalpanic" -var _LogLevel_index = [...]uint8{0, 4, 15, 20, 24, 29, 34, 39} +var _LogLevel_index = [...]uint8{0, 5, 16, 21, 25, 29, 34, 39, 44} func (i LogLevel) String() string { + i -= -3 if i < 0 || i >= LogLevel(len(_LogLevel_index)-1) { - return "LogLevel(" + strconv.FormatInt(int64(i), 10) + ")" + return "LogLevel(" + strconv.FormatInt(int64(i+-3), 10) + ")" } return _LogLevel_name[_LogLevel_index[i]:_LogLevel_index[i+1]] } diff --git a/config/enums_test.go b/config/enums_test.go index 10ecb3938d..77348b25da 100644 --- a/config/enums_test.go +++ b/config/enums_test.go @@ -1,7 +1,6 @@ package config_test import ( - "bytes" "testing" "github.com/quay/clair/config" @@ -9,36 +8,40 @@ import ( func TestEnumMarshal(t *testing.T) { t.Run("LogLevel", func(t *testing.T) { - tt := [][]byte{ - []byte("info"), - []byte("debug-color"), - []byte("debug"), - []byte("warn"), - []byte("error"), - []byte("fatal"), - []byte("panic"), + type testcase struct { + Level config.LogLevel + String string + } + tt := []testcase{ + {Level: config.TraceLog, String: "trace"}, + {Level: config.DebugColorLog, String: "debug-color"}, + {Level: config.DebugLog, String: "debug"}, + {Level: config.InfoLog, String: "info"}, + {Level: config.WarnLog, String: "warn"}, + {Level: config.ErrorLog, String: "error"}, + {Level: config.FatalLog, String: "fatal"}, + {Level: config.PanicLog, String: "panic"}, } t.Run("Marshal", func(t *testing.T) { - for i, want := range tt { - l := config.LogLevel(i) - got, err := l.MarshalText() + for _, tc := range tt { + m, err := tc.Level.MarshalText() if err != nil { t.Error(err) continue } - if !bytes.Equal(got, want) { + if got, want := string(m), tc.String; got != want { t.Errorf("got: %q, want: %q", got, want) } } }) t.Run("Unmarshal", func(t *testing.T) { - for want, in := range tt { - var l config.LogLevel - if err := l.UnmarshalText(in); err != nil { + for _, tc := range tt { + var got config.LogLevel + if err := got.UnmarshalText([]byte(tc.String)); err != nil { t.Error(err) continue } - if got := int(l); got != want { + if want := tc.Level; got != want { t.Errorf("got: %q, want: %q", got, want) } } diff --git a/config/logging.go b/config/logging.go new file mode 100644 index 0000000000..52ad417e4e --- /dev/null +++ b/config/logging.go @@ -0,0 +1,114 @@ +package config + +import ( + "encoding" + "errors" + "fmt" + "strings" +) + +// Logging is all the log configuration. +type Logging struct { + Level LogLevel `yaml:"level,omitempty" json:"level,omitempty"` + OmitTimestamps bool `yaml:"omit_timestamps,omitempty" json:"omit_timestamps,omitempty"` + Prose bool `yaml:"prose,omitempty" json:"prose,omitempty"` +} + +// A LogLevel is a log level recognized by Clair. +// +// The zero value is [InfoLog]. +type LogLevel int + +// The recognized log levels, with their string representations as the comments. +// +// Note [FatalLog] and [PanicLog] are not used in Clair or Claircore, and will +// result in almost no logging. +const ( + TraceLog LogLevel = iota - 3 // trace + // Deprecated: Use the "Prose" toggle to request a non-JSON output format. + DebugColorLog // debug-color + DebugLog // debug + InfoLog // info + WarnLog // warn + ErrorLog // error + FatalLog // fatal + PanicLog // panic +) + +// Assert that the zero value is correct: +var _ = [1]struct{}{{}}[InfoLog] + +// ParseLogLevel returns the log level for the given string. +// +// The passed string is case-insensitive. +func ParseLogLevel(s string) (LogLevel, error) { + const offset = int(TraceLog) + for i, lim := 0, len(_LogLevel_index); i < lim; i++ { + l := LogLevel(i + offset) + if strings.EqualFold(s, l.String()) { + return l, nil + } + } + return LogLevel(-127), fmt.Errorf(`unknown log level %q`, s) +} + +// UnmarshalText implements [encoding.TextUnmarshaler]. +func (l *LogLevel) UnmarshalText(b []byte) (err error) { + *l, err = ParseLogLevel(string(b)) + if err != nil { + return err + } + return nil +} + +// MarshalText implements [encoding.TextMarshaler]. +func (l *LogLevel) MarshalText() ([]byte, error) { + const offset = int(TraceLog) + if l == nil { + return nil, errors.New("invalid LogLevel pointer: ") + } + i := int(*l) - offset + // We never re-serialize the "debug-color" level, because it's an annoying + // wart. + if *l == DebugColorLog { + i++ + } + if i < 0 || i >= len(_LogLevel_index)-1 { + return nil, fmt.Errorf("invalid LogLevel: %q", l.String()) + } + return []byte(_LogLevel_name[_LogLevel_index[i]:_LogLevel_index[i+1]]), nil +} + +// Assert LogLevel implements everything that's needed. +var ( + _ encoding.TextUnmarshaler = (*LogLevel)(nil) + _ encoding.TextMarshaler = (*LogLevel)(nil) +) + +func (l *Logging) validate(mode Mode) ([]Warning, error) { + ws, err := l.lint() + if err != nil { + return ws, err + } + if l.Level == DebugColorLog { + l.Level++ + l.Prose = true + } + return ws, nil +} + +func (l *Logging) lint() (ws []Warning, _ error) { + if l.Level > ErrorLog { + ws = append(ws, Warning{ + path: ".level", + msg: `"fatal" and "panic" levels are not used and will result in almost no logging`, + }) + } + if l.Level == DebugColorLog { + ws = append(ws, Warning{ + path: ".level", + msg: `"debug-color" is deprecated; use "debug" and set the "prose" option`, + }) + } + return ws, nil +}