Skip to content

Commit

Permalink
🪵 logging: revamp usage of zap logger
Browse files Browse the repository at this point in the history
- Add more presets.
- Disable caller info by default.
- Use a fake clock in "notime" presets.
  • Loading branch information
database64128 committed Jul 22, 2024
1 parent ac0b891 commit 7326c29
Show file tree
Hide file tree
Showing 6 changed files with 119 additions and 70 deletions.
44 changes: 7 additions & 37 deletions cmd/swgp-go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,54 +23,24 @@ var (
)

func init() {
flag.BoolVar(&testConf, "testConf", false, "Test the configuration file without starting the services")
flag.StringVar(&confPath, "confPath", "", "Path to JSON configuration file")
flag.StringVar(&zapConf, "zapConf", "", "Preset name or path to JSON configuration file for building the zap logger.\nAvailable presets: console (default), systemd, production, development")
flag.TextVar(&logLevel, "logLevel", zapcore.InvalidLevel, "Override the logger configuration's log level.\nAvailable levels: debug, info, warn, error, dpanic, panic, fatal")
flag.BoolVar(&testConf, "testConf", false, "Test the configuration file and exit without starting the services")
flag.StringVar(&confPath, "confPath", "config.json", "Path to the JSON configuration file")
flag.StringVar(&zapConf, "zapConf", "console", "Preset name or path to the JSON configuration file for building the zap logger.\nAvailable presets: console, console-nocolor, console-notime, systemd, production, development")
flag.TextVar(&logLevel, "logLevel", zapcore.InfoLevel, "Log level for the console and systemd presets.\nAvailable levels: debug, info, warn, error, dpanic, panic, fatal")
}

func main() {
flag.Parse()

if confPath == "" {
fmt.Fprintln(os.Stderr, "Missing -confPath <path>.")
flag.Usage()
os.Exit(1)
}

var (
zc zap.Config
sc service.Config
)

switch zapConf {
case "console", "":
zc = logging.NewProductionConsoleConfig(false)
case "systemd":
zc = logging.NewProductionConsoleConfig(true)
case "production":
zc = zap.NewProductionConfig()
case "development":
zc = zap.NewDevelopmentConfig()
default:
if err := jsonhelper.LoadAndDecodeDisallowUnknownFields(zapConf, &zc); err != nil {
fmt.Fprintln(os.Stderr, "Failed to load zap logger config:", err)
os.Exit(1)
}
}

if logLevel != zapcore.InvalidLevel {
zc.Level.SetLevel(logLevel)
}

logger, err := zc.Build()
logger, err := logging.NewZapLogger(zapConf, logLevel)
if err != nil {
fmt.Fprintln(os.Stderr, "Failed to build logger:", err)
os.Exit(1)
}
defer logger.Sync()

if err = jsonhelper.LoadAndDecodeDisallowUnknownFields(confPath, &sc); err != nil {
var sc service.Config
if err = jsonhelper.OpenAndDecodeDisallowUnknownFields(confPath, &sc); err != nil {
logger.Fatal("Failed to load config",
zap.String("confPath", confPath),
zap.Error(err),
Expand Down
21 changes: 21 additions & 0 deletions jsonhelper/duration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package jsonhelper

import "time"

// Duration is [time.Duration] but implements [encoding.TextMarshaler] and [encoding.TextUnmarshaler].
type Duration time.Duration

// MarshalText implements [encoding.TextMarshaler.MarshalText].
func (d Duration) MarshalText() ([]byte, error) {
return []byte(time.Duration(d).String()), nil
}

// UnmarshalText implements [encoding.TextUnmarshaler.UnmarshalText].
func (d *Duration) UnmarshalText(text []byte) error {
duration, err := time.ParseDuration(string(text))
if err != nil {
return err
}
*d = Duration(duration)
return nil
}
3 changes: 2 additions & 1 deletion jsonhelper/jsonhelper.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import (
"os"
)

func LoadAndDecodeDisallowUnknownFields(path string, v any) error {
// OpenAndDecodeDisallowUnknownFields opens the file at path and decodes it into v, disallowing unknown fields.
func OpenAndDecodeDisallowUnknownFields(path string, v any) error {
f, err := os.Open(path)
if err != nil {
return err
Expand Down
117 changes: 87 additions & 30 deletions logging/zap.go
Original file line number Diff line number Diff line change
@@ -1,45 +1,76 @@
package logging

import (
"fmt"
"os"
"time"

"github.com/database64128/swgp-go/jsonhelper"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)

// NewProductionConsoleConfig is a reasonable production logging configuration.
// Logging is enabled at InfoLevel and above.
// NewZapLogger returns a new [*zap.Logger] with the given preset and log level.
//
// The available presets are:
//
// - "console" (default): Reasonable defaults for production console environments.
// - "console-nocolor": Same as "console", but without color.
// - "console-notime": Same as "console", but without timestamps.
// - "systemd": Reasonable defaults for running as a systemd service. Same as "console", but without color and timestamps.
// - "production": Zap's built-in production preset.
// - "development": Zap's built-in development preset.
//
// It uses a console encoder, writes to standard error, and enables sampling.
// Stacktraces are automatically included on logs of ErrorLevel and above.
func NewProductionConsoleConfig(suppressTimestamps bool) zap.Config {
return zap.Config{
Level: zap.NewAtomicLevelAt(zap.InfoLevel),
Development: false,
Sampling: &zap.SamplingConfig{
Initial: 100,
Thereafter: 100,
},
Encoding: "console",
EncoderConfig: NewProductionConsoleEncoderConfig(suppressTimestamps),
OutputPaths: []string{"stderr"},
ErrorOutputPaths: []string{"stderr"},
// If the preset is not recognized, it is treated as a path to a JSON configuration file.
//
// The log level does not apply to the "production", "development", or custom presets.
func NewZapLogger(preset string, level zapcore.Level) (*zap.Logger, error) {
switch preset {
case "console":
return NewProductionConsoleZapLogger(level, false, false, false), nil
case "console-nocolor":
return NewProductionConsoleZapLogger(level, true, false, false), nil
case "console-notime":
return NewProductionConsoleZapLogger(level, false, true, false), nil
case "systemd":
return NewProductionConsoleZapLogger(level, true, true, false), nil
}

var cfg zap.Config
switch preset {
case "production":
cfg = zap.NewProductionConfig()
case "development":
cfg = zap.NewDevelopmentConfig()
default:
if err := jsonhelper.OpenAndDecodeDisallowUnknownFields(preset, &cfg); err != nil {
return nil, fmt.Errorf("failed to load zap logger config from file %q: %w", preset, err)
}
}
return cfg.Build()
}

// NewProductionConsoleEncoderConfig returns an opinionated EncoderConfig for
// production console environments.
func NewProductionConsoleEncoderConfig(suppressTimestamps bool) zapcore.EncoderConfig {
var (
timeKey string
encodeTime zapcore.TimeEncoder
)

if !suppressTimestamps {
timeKey = "T"
encodeTime = zapcore.ISO8601TimeEncoder
// NewProductionConsoleZapLogger creates a new [*zap.Logger] with reasonable defaults for production console environments.
//
// See [NewProductionConsoleEncoderConfig] for information on the default encoder configuration.
func NewProductionConsoleZapLogger(level zapcore.Level, noColor, noTime, addCaller bool) *zap.Logger {
cfg := NewProductionConsoleEncoderConfig(noColor, noTime)
enc := zapcore.NewConsoleEncoder(cfg)
core := zapcore.NewCore(enc, zapcore.Lock(os.Stderr), level)
var opts []zap.Option
if noTime {
opts = append(opts, zap.WithClock(fakeClock{})) // Note that the sampler requires a real clock.
}
if addCaller {
opts = append(opts, zap.AddCaller())
}
return zap.New(core, opts...)
}

return zapcore.EncoderConfig{
TimeKey: timeKey,
// NewProductionConsoleEncoderConfig returns an opinionated [zapcore.EncoderConfig] for production console environments.
func NewProductionConsoleEncoderConfig(noColor, noTime bool) zapcore.EncoderConfig {
ec := zapcore.EncoderConfig{
TimeKey: "T",
LevelKey: "L",
NameKey: "N",
CallerKey: "C",
Expand All @@ -48,9 +79,35 @@ func NewProductionConsoleEncoderConfig(suppressTimestamps bool) zapcore.EncoderC
StacktraceKey: "S",
LineEnding: zapcore.DefaultLineEnding,
EncodeLevel: zapcore.CapitalColorLevelEncoder,
EncodeTime: encodeTime,
EncodeTime: zapcore.ISO8601TimeEncoder,
EncodeDuration: zapcore.StringDurationEncoder,
EncodeCaller: zapcore.ShortCallerEncoder,
ConsoleSeparator: " ",
}

if noColor {
ec.EncodeLevel = zapcore.CapitalLevelEncoder
}

if noTime {
ec.TimeKey = zapcore.OmitKey
ec.EncodeTime = nil
}

return ec
}

// fakeClock is a fake clock that always returns the zero-value time.
//
// fakeClock implements [zapcore.Clock].
type fakeClock struct{}

// Now implements [zapcore.Clock.Now].
func (fakeClock) Now() time.Time {
return time.Time{}
}

// NewTicker implements [zapcore.Clock.NewTicker].
func (fakeClock) NewTicker(d time.Duration) *time.Ticker {
return time.NewTicker(d)
}
2 changes: 1 addition & 1 deletion service/client_generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

package service

func (c *client) setStartFunc(batchMode string) {
func (c *client) setStartFunc(_ string) {
c.startFunc = c.startGeneric
}
2 changes: 1 addition & 1 deletion service/server_generic.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,6 @@

package service

func (s *server) setStartFunc(batchMode string) {
func (s *server) setStartFunc(_ string) {
s.startFunc = s.startGeneric
}

0 comments on commit 7326c29

Please sign in to comment.