Repository: cosmos/relayer
Pull request #657 (2022-04-01 14:56:54)

Support explicit log format configuration
This maintains the existing behavior of "auto" formatting (console
formatting if connected to a terminal, logfmt otherwise), but allows
opting in to logfmt, json, or console encoding specifically if so
desired. This will give operators fine control over log formatting for
any postprocessing they may want, and it will allow us to easily add new
encodings should that need ever arise.


Patch:
diff --git a/cmd/root.go b/cmd/root.go
index e33e344..d4d5345 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -55,7 +55,9 @@ const (
 )
 
 // NewRootCmd returns the root command for relayer.
-func NewRootCmd(log *zap.Logger, atom zap.AtomicLevel) *cobra.Command {
+// If log is nil, a new zap.Logger is set on the app state
+// based on the command line flags regarding logging.
+func NewRootCmd(log *zap.Logger) *cobra.Command {
 	// Use a local app state instance scoped to the new root command,
 	// so that tests don't concurrently access the state.
 	a := &appState{
@@ -79,14 +81,24 @@ func NewRootCmd(log *zap.Logger, atom zap.AtomicLevel) *cobra.Command {
 
 	rootCmd.PersistentPreRunE = func(cmd *cobra.Command, _ []string) error {
 		// Inside persistent pre-run because this takes effect after flags are parsed.
-		if a.Viper.GetBool("debug") {
-			atom.SetLevel(zapcore.DebugLevel)
+		if log == nil {
+			log, err := newRootLogger(a.Viper.GetString("log-format"), a.Viper.GetBool("debug"))
+			if err != nil {
+				return err
+			}
+
+			a.Log = log
 		}
 
 		// reads `homeDir/config/config.yaml` into `a.Config`
 		return initConfig(rootCmd, a)
 	}
 
+	rootCmd.PersistentPostRun = func(cmd *cobra.Command, _ []string) {
+		// Force syncing the logs before exit, if anything is buffered.
+		a.Log.Sync()
+	}
+
 	// Register --home flag
 	rootCmd.PersistentFlags().StringVar(&a.HomePath, flags.FlagHome, defaultHome, "set home directory")
 	if err := a.Viper.BindPFlag(flags.FlagHome, rootCmd.PersistentFlags().Lookup(flags.FlagHome)); err != nil {
@@ -99,6 +111,11 @@ func NewRootCmd(log *zap.Logger, atom zap.AtomicLevel) *cobra.Command {
 		panic(err)
 	}
 
+	rootCmd.PersistentFlags().String("log-format", "auto", "log output format (auto, logfmt, json, or console)")
+	if err := a.Viper.BindPFlag("log-format", rootCmd.PersistentFlags().Lookup("log-format")); err != nil {
+		panic(err)
+	}
+
 	// Register subcommands
 	rootCmd.AddCommand(
 		configCmd(a),
@@ -121,10 +138,7 @@ func NewRootCmd(log *zap.Logger, atom zap.AtomicLevel) *cobra.Command {
 func Execute() {
 	cobra.EnableCommandSorting = false
 
-	log, atom := rootLogger()
-	defer log.Sync()
-
-	rootCmd := NewRootCmd(log, atom)
+	rootCmd := NewRootCmd(nil)
 	rootCmd.SilenceUsage = true
 
 	ctx, cancel := context.WithCancel(context.Background())
@@ -160,14 +174,11 @@ func Execute() {
 	}()
 
 	if err := rootCmd.ExecuteContext(ctx); err != nil {
-		// Flush any remaining logs.
-		log.Sync()
-
 		os.Exit(1)
 	}
 }
 
-func rootLogger() (*zap.Logger, zap.AtomicLevel) {
+func newRootLogger(format string, debug bool) (*zap.Logger, error) {
 	config := zap.NewProductionEncoderConfig()
 	config.EncodeTime = func(ts time.Time, encoder zapcore.PrimitiveArrayEncoder) {
 		encoder.AppendString(ts.UTC().Format("2006-01-02T15:04:05.000000Z07:00"))
@@ -175,20 +186,34 @@ func rootLogger() (*zap.Logger, zap.AtomicLevel) {
 	config.LevelKey = "lvl"
 
 	var enc zapcore.Encoder
-	if term.IsTerminal(int(os.Stderr.Fd())) {
-		// When a user runs relayer in the foreground, use easier to read output.
+	switch format {
+	case "json":
+		enc = zapcore.NewJSONEncoder(config)
+	case "console":
 		enc = zapcore.NewConsoleEncoder(config)
-	} else {
-		// Otherwise, use consistent logfmt format for simplistic machine processing.
+	case "logfmt":
 		enc = zaplogfmt.NewEncoder(config)
+	case "auto":
+		if term.IsTerminal(int(os.Stderr.Fd())) {
+			// When a user runs relayer in the foreground, use easier to read output.
+			enc = zapcore.NewConsoleEncoder(config)
+		} else {
+			// Otherwise, use consistent logfmt format for simplistic machine processing.
+			enc = zaplogfmt.NewEncoder(config)
+		}
+	default:
+		return nil, fmt.Errorf("unrecognized log format %q", format)
 	}
 
-	atom := zap.NewAtomicLevel()
+	level := zap.InfoLevel
+	if debug {
+		level = zap.DebugLevel
+	}
 	return zap.New(zapcore.NewCore(
 		enc,
 		os.Stderr,
-		atom,
-	)), atom
+		level,
+	)), nil
 }
 
 // readLine reads one line from the given reader.


Test patch:
diff --git a/cmd/system_test.go b/cmd/system_test.go
index b3b2fd0..fbf150a 100644
--- a/cmd/system_test.go
+++ b/cmd/system_test.go
@@ -51,7 +51,7 @@ func (s *System) Run(log *zap.Logger, args ...string) RunResult {
 // providing in as the command's standard input,
 // and returns a RunResult that has its Stdout and Stderr populated.
 func (s *System) RunWithInput(log *zap.Logger, in io.Reader, args ...string) RunResult {
-	rootCmd := cmd.NewRootCmd(log, zap.NewAtomicLevel())
+	rootCmd := cmd.NewRootCmd(log)
 	rootCmd.SetIn(in)
 	// cmd.Execute also sets SilenceUsage,
 	// so match that here for more correct assertions.