diff --git a/cmd/program.go b/cmd/program.go index e0809eb545e2529247271a00a43ee4748375b375_Y21kL3Byb2dyYW0uZ28=..39a574cadd10d2efbd4a4437a188ed392cb516a5_Y21kL3Byb2dyYW0uZ28= 100644 --- a/cmd/program.go +++ b/cmd/program.go @@ -133,13 +133,13 @@ func WithOptionsGroup[E any]( name string, description string, getgroup func(*Program[E]) any, ) Option[E] { - return PostInit( func (program *Program[E]) { - if _, err := program.Parser.AddGroup( - name, description, getgroup(program), - ); err != nil { - panic(err) - } - }) + return PostInit(func(program *Program[E]) { + if _, err := program.Parser.AddGroup( + name, description, getgroup(program), + ); err != nil { + panic(err) + } + }) } func WithTokenOptions[E any]() Option[E] { @@ -196,8 +196,7 @@ opt(&program) } - program.LoggingOptions = orusapi.MustLoggingOptions( - orusapi.NewLoggingOptions(&program.Logger, os.Stdout)) + program.LoggingOptions = orusapi.NewLoggingOptions(os.Stdout) bootstrapParser.NamespaceDelimiter = "-" bootstrapParser.EnvNamespaceDelimiter = "_" @@ -209,6 +208,15 @@ panic(err) } + { + g, err := bootstrapParser.AddGroup("Logging", "Logging options", program.LoggingOptions) + if err != nil { + panic(err) + } + g.Namespace = "log" + g.EnvNamespace = "LOG" + } + if _, err := parser.AddGroup("Configuration", "Configuration file", &program.ConfigFileOption); err != nil { panic(err) } @@ -250,6 +258,17 @@ pi(&program) } + program.Parser.CommandHandler = func(command flags.Commander, args []string) error { + program.LoggingOptions.BuildLogger() + program.Logger = *program.LoggingOptions.Logger() + + if command == nil { + return nil + } + + return command.Execute(args) + } + return &program } @@ -265,6 +284,18 @@ return 1 } + + logLevelOption := program.BootstrapParser.FindOptionByLongName("log-level") + logFormatOption := program.BootstrapParser.FindOptionByLongName("log-format") + + program.LoggingOptions.Lock( + logLevelOption.IsSet() && !logLevelOption.IsSetDefault(), + logFormatOption.IsSet() && !logFormatOption.IsSetDefault(), + ) + + program.LoggingOptions.BuildLogger() + program.Logger = *program.LoggingOptions.Logger() + if program.ConfigFileOption.ConfigFile != "" { program.Logger.Debug().Str("configfile", program.ConfigFileOption.ConfigFile).Msg("parsing configuration file") iniParser := flags.NewIniParser(program.Parser) @@ -273,6 +304,8 @@ return 1 } + program.LoggingOptions.BuildLogger() + program.Logger = *program.LoggingOptions.Logger() } if _, err := program.Parser.Parse(); err != nil { diff --git a/logging.go b/logging.go index e0809eb545e2529247271a00a43ee4748375b375_bG9nZ2luZy5nbw==..39a574cadd10d2efbd4a4437a188ed392cb516a5_bG9nZ2luZy5nbw== 100644 --- a/logging.go +++ b/logging.go @@ -1,7 +1,6 @@ package orusapi import ( - "fmt" "io" "github.com/rs/zerolog" @@ -5,8 +4,7 @@ "io" "github.com/rs/zerolog" - "github.com/rs/zerolog/log" "golang.org/x/term" ) // NewLoggingOptions creates a LoggingOptions. @@ -9,6 +7,6 @@ "golang.org/x/term" ) // NewLoggingOptions creates a LoggingOptions. -func NewLoggingOptions(log *zerolog.Logger, output io.Writer) (*LoggingOptions, error) { +func NewLoggingOptions(output io.Writer) *LoggingOptions { var o LoggingOptions @@ -14,5 +12,3 @@ var o LoggingOptions - if err := o.Setup(log, output); err != nil { - return nil, err - } + o.Setup(output) @@ -18,14 +14,5 @@ - return &o, nil -} - -// MustLoggingOptions panic if err is not nil. -func MustLoggingOptions(o *LoggingOptions, err error) *LoggingOptions { - if err != nil { - panic(err) - } - - return o + return &o } // DefaultLogger ... @@ -40,8 +27,10 @@ // LoggingOptions holds the logging options. type LoggingOptions struct { - Level func(string) error `long:"level" env:"LEVEL" ini-name:"log-level" choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" choice:"auto" default:"auto" description:"log level. 'auto' selects 'info' when stdout is a tty, 'error' otherwise."` //nolint:lll - Format func(string) error `long:"format" env:"FORMAT" ini-name:"log-format" choice:"json" choice:"pretty" choice:"auto" default:"auto" description:"Logs format. 'auto' selects 'pretty' if stdout is a tty."` //nolint:lll - Verbose func() `short:"v" long:"verbose" no-ini:"t" description:"Increase log verbosity. Can be repeated"` + Level string `long:"level" env:"LEVEL" ini-name:"level" choice:"trace" choice:"debug" choice:"info" choice:"warn" choice:"error" choice:"fatal" choice:"panic" choice:"auto" default:"auto" description:"log level. 'auto' selects 'info' when stdout is a tty, 'error' otherwise."` //nolint:lll + Format string `long:"format" env:"FORMAT" ini-name:"format" choice:"json" choice:"pretty" choice:"auto" default:"auto" description:"Logs format. 'auto' selects 'pretty' if stdout is a tty."` //nolint:lll + Verbose func() `short:"v" long:"verbose" no-ini:"t" description:"Increase log verbosity. Can be repeated"` + + logOutput io.Writer `no-flag:"t"` logFinalOutput io.Writer `no-flag:"t"` @@ -46,6 +35,5 @@ logFinalOutput io.Writer `no-flag:"t"` - logOutput io.Writer `no-flag:"t"` wrappedOutput io.Writer `no-flag:"t"` logWrappers []func(io.Writer) io.Writer `no-flag:"t"` log *zerolog.Logger `no-flag:"t"` @@ -49,6 +37,22 @@ wrappedOutput io.Writer `no-flag:"t"` logWrappers []func(io.Writer) io.Writer `no-flag:"t"` log *zerolog.Logger `no-flag:"t"` + + minLevel zerolog.Level + verboseCount int `no-flag:"t"` + lockedVerbose bool + lockedLevel string + lockedFormat string +} + +func (o *LoggingOptions) Lock(lockLevel, lockFormat bool) { + o.lockedVerbose = true + if lockLevel { + o.lockedLevel = o.Level + } + if lockFormat { + o.lockedFormat = o.Format + } } // Logger returns the latest configured logger. @@ -52,11 +56,15 @@ } // Logger returns the latest configured logger. -func (o *LoggingOptions) Logger() zerolog.Logger { - return *o.log +func (o *LoggingOptions) Logger() *zerolog.Logger { + if o.log == nil { + o.BuildLogger() + } + + return o.log } func (o *LoggingOptions) Output() io.Writer { return o.wrappedOutput } @@ -57,18 +65,9 @@ } func (o *LoggingOptions) Output() io.Writer { return o.wrappedOutput } -func (o *LoggingOptions) resetOutput() { - out := o.logOutput - for _, wrapper := range o.logWrappers { - out = wrapper(out) - } - o.wrappedOutput = out - *o.log = o.log.Output(out) -} - // AddLogWrapper adds a log wrapper to the stack. func (o *LoggingOptions) AddLogWrapper(wrapper func(io.Writer) io.Writer) { o.logWrappers = append(o.logWrappers, wrapper) @@ -72,8 +71,7 @@ // AddLogWrapper adds a log wrapper to the stack. func (o *LoggingOptions) AddLogWrapper(wrapper func(io.Writer) io.Writer) { o.logWrappers = append(o.logWrappers, wrapper) - o.resetOutput() } // SetMinLoggingLevel makes sure the logging level is not under a given value. func (o *LoggingOptions) SetMinLoggingLevel(level zerolog.Level) { @@ -76,7 +74,46 @@ } // SetMinLoggingLevel makes sure the logging level is not under a given value. func (o *LoggingOptions) SetMinLoggingLevel(level zerolog.Level) { - if level < o.log.GetLevel() { - *o.log = log.Level(level) + o.minLevel = level +} + +func parseOutputFormat(format string, finalOutput io.Writer) io.Writer { + switch format { + case "pretty": + return ConsoleWriter{Out: finalOutput} + case "json": + return finalOutput + default: + panic("unknown 'log-format' value: " + format) + } +} + +func parseLogLevel(value string, finalOutput io.Writer) zerolog.Level { + if value == "auto" || value == "" { + if outputFile, hasFd := finalOutput.(interface{ Fd() uintptr }); hasFd && + term.IsTerminal(int(outputFile.Fd())) { + value = zerolog.LevelInfoValue + } else { + value = zerolog.LevelWarnValue + } + } + + level, err := zerolog.ParseLevel(value) + if err != nil { + panic(err) + } + + return level +} + +func (o LoggingOptions) buildOutput() io.Writer { + format := o.GetFormat() + if format == "auto" || format == "" { + if outputFile, hasFd := o.logFinalOutput.(interface{ Fd() uintptr }); hasFd && + term.IsTerminal(int(outputFile.Fd())) { + format = "pretty" + } else { + format = "json" + } } @@ -82,4 +119,46 @@ } + + out := parseOutputFormat(format, o.logFinalOutput) + + for _, wrapper := range o.logWrappers { + out = wrapper(out) + } + + return out +} + +func (o LoggingOptions) GetLevel() string { + if o.lockedLevel != "" { + return o.lockedLevel + } + + return o.Level +} + +func (o LoggingOptions) GetFormat() string { + if o.lockedFormat != "" { + return o.lockedFormat + } + + return o.Format +} + +// BuildLogger rebuild the logger even if already initialized +func (o *LoggingOptions) BuildLogger() { + out := o.buildOutput() + o.wrappedOutput = out + + level := parseLogLevel(o.GetLevel(), o.logFinalOutput) + level -= zerolog.Level(o.verboseCount) + + log := zerolog. + New(out). + With(). + Timestamp(). + Logger(). + Level(level) + + o.log = &log } // Setup ... @@ -83,8 +162,6 @@ } // Setup ... -func (o *LoggingOptions) Setup(log *zerolog.Logger, output io.Writer) error { - logLevelAutoLocked := false - +func (o *LoggingOptions) Setup(output io.Writer) { o.logFinalOutput = output @@ -89,14 +166,6 @@ o.logFinalOutput = output - o.log = log - *o.log = DefaultLogger(output) - - o.Format = func(format string) error { - if format == "auto" { - if outputFile, hasFd := o.logFinalOutput.(interface{ Fd() uintptr }); hasFd && - term.IsTerminal(int(outputFile.Fd())) { - format = "pretty" - } else { - format = "json" - } + o.Verbose = func() { + if !o.lockedVerbose { + o.verboseCount++ } @@ -102,16 +171,2 @@ } - switch format { - case "pretty": - o.logOutput = ConsoleWriter{Out: o.logFinalOutput} - case "json": - o.logOutput = o.logFinalOutput - default: - return fmt.Errorf("invalid log-format: %s", format) - } - o.resetOutput() - - return nil - } - o.Verbose = func() { - *o.log = o.log.Level(o.log.GetLevel() - zerolog.Level(1)) } @@ -117,34 +172,2 @@ } - o.Level = func(value string) error { - if value == "auto" { - if logLevelAutoLocked { - // The current call is at best redondant, at worse called by - // default after some potential --verbose that would be ignored - return nil - } - if outputFile, hasFd := o.logFinalOutput.(interface{ Fd() uintptr }); hasFd && - term.IsTerminal(int(outputFile.Fd())) { - value = zerolog.LevelInfoValue - } else { - value = zerolog.LevelWarnValue - } - } - - level, err := zerolog.ParseLevel(value) - if err != nil { - return err - } - *o.log = o.log.Level(level) - - return nil - } - if err := o.Format("auto"); err != nil { - return err - } - if err := o.Level("auto"); err != nil { - return err - } - logLevelAutoLocked = true - - return nil } diff --git a/logging_test.go b/logging_test.go index e0809eb545e2529247271a00a43ee4748375b375_bG9nZ2luZ190ZXN0Lmdv..39a574cadd10d2efbd4a4437a188ed392cb516a5_bG9nZ2luZ190ZXN0Lmdv 100644 --- a/logging_test.go +++ b/logging_test.go @@ -6,7 +6,6 @@ "github.com/rs/zerolog" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" "orus.io/orus-io/go-orusapi" ) @@ -14,6 +13,5 @@ func TestLogging(t *testing.T) { var ( buf bytes.Buffer - log zerolog.Logger ) @@ -18,5 +16,6 @@ ) - var o orusapi.LoggingOptions - require.NoError(t, o.Setup(&log, &buf)) + t.Run("verbose flag", func(t *testing.T) { + var o orusapi.LoggingOptions + o.Setup(&buf) @@ -22,3 +21,5 @@ - assert.Equal(t, zerolog.WarnLevel, o.Logger().GetLevel()) + o.Level = "warn" + o.BuildLogger() + assert.Equal(t, zerolog.WarnLevel, o.Logger().GetLevel()) @@ -24,4 +25,5 @@ - o.Verbose() - assert.Equal(t, zerolog.InfoLevel, o.Logger().GetLevel()) + o.Verbose() + o.BuildLogger() + assert.Equal(t, zerolog.InfoLevel, o.Logger().GetLevel()) @@ -27,4 +29,5 @@ - o.Verbose() - assert.Equal(t, zerolog.DebugLevel, o.Logger().GetLevel()) + o.Verbose() + o.BuildLogger() + assert.Equal(t, zerolog.DebugLevel, o.Logger().GetLevel()) @@ -30,4 +33,6 @@ - o.Verbose() - assert.Equal(t, zerolog.TraceLevel, o.Logger().GetLevel()) + o.Verbose() + o.BuildLogger() + assert.Equal(t, zerolog.TraceLevel, o.Logger().GetLevel()) + }) @@ -33,4 +38,8 @@ - require.NoError(t, o.Level(zerolog.LevelFatalValue)) - assert.Equal(t, zerolog.FatalLevel, o.Logger().GetLevel()) + t.Run("level flag", func(t *testing.T) { + var o orusapi.LoggingOptions + o.Setup(&buf) + o.Level = zerolog.LevelFatalValue + o.BuildLogger() + assert.Equal(t, zerolog.FatalLevel, o.Logger().GetLevel()) @@ -36,4 +45,6 @@ - require.NoError(t, o.Level(zerolog.LevelInfoValue)) - assert.Equal(t, zerolog.InfoLevel, o.Logger().GetLevel()) + o.Level = zerolog.LevelInfoValue + o.BuildLogger() + assert.Equal(t, zerolog.InfoLevel, o.Logger().GetLevel()) + }) @@ -39,5 +50,10 @@ - require.NoError(t, o.Format("pretty")) - log.Warn().Msg("this is a warning") - assert.Contains(t, buf.String(), "WRN") + t.Run("format flag", func(t *testing.T) { + var o orusapi.LoggingOptions + o.Setup(&buf) + o.Format = "pretty" + o.BuildLogger() + log := o.Logger() + log.Warn().Msg("this is a warning") + assert.Contains(t, buf.String(), "WRN") @@ -43,6 +59,9 @@ - buf.Reset() - require.NoError(t, o.Format("json")) - log.Warn().Msg("this is a warning") - assert.Contains(t, buf.String(), "{") + buf.Reset() + o.Format = "json" + o.BuildLogger() + log = o.Logger() + log.Warn().Msg("this is a warning") + assert.Contains(t, buf.String(), "{") + }) }