Files
ctrld/log.go
Cuong Manh Le ddbb0f0db4 refactor: migrate from zerolog to zap logging library
Replace github.com/rs/zerolog with go.uber.org/zap throughout the codebase
to improve performance and provide better structured logging capabilities.

Key changes:
- Replace zerolog imports with zap and zapcore
- Implement custom Logger wrapper in log.go to maintain zerolog-like API
- Add LogEvent struct with chained methods (Str, Int, Err, Bool, etc.)
- Update all logging calls to use the new zap-based wrapper
- Replace JSON encoders with Console encoders for better readability

Benefits:
- Better performance with zap's optimized logging
- Consistent structured logging across all components
- Maintained zerolog-like API for easy migration
- Proper field context preservation for debugging
- Multi-core logging architecture for better output control

All tests pass and build succeeds.
2025-10-09 17:49:21 +07:00

247 lines
6.1 KiB
Go

package ctrld
import (
"context"
"fmt"
"io"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// LoggerCtxKey is the context.Context key for a logger.
type LoggerCtxKey struct{}
// LoggerCtx returns a context.Context with LoggerCtxKey set.
func LoggerCtx(ctx context.Context, l *Logger) context.Context {
return context.WithValue(ctx, LoggerCtxKey{}, l)
}
// A Logger provides fast, leveled, structured logging.
type Logger struct {
*zap.Logger
}
var noOpZapLogger = zap.NewNop()
// NopLogger returns a logger which all operation are no-op.
var NopLogger = &Logger{noOpZapLogger}
// LoggerFromCtx returns the logger associated with given ctx.
//
// If there's no logger, a no-op logger will be returned.
func LoggerFromCtx(ctx context.Context) *Logger {
if logger, ok := ctx.Value(LoggerCtxKey{}).(*Logger); ok && logger != nil {
return logger
}
return NopLogger
}
// ReqIdCtxKey is the context.Context key for a request id.
type ReqIdCtxKey struct{}
// LogEvent represents a logging event with structured fields
type LogEvent struct {
logger *zap.Logger
level zapcore.Level
fields []zap.Field
}
// Msg logs the message with the collected fields
func (e *LogEvent) Msg(msg string) {
e.logger.Check(e.level, msg).Write(e.fields...)
}
// Msgf logs a formatted message with the collected fields
func (e *LogEvent) Msgf(format string, v ...any) {
e.Msg(fmt.Sprintf(format, v...))
}
// MsgFunc logs a message from a function with the collected fields
func (e *LogEvent) MsgFunc(fn func() string) {
e.Msg(fn())
}
// Str adds a string field to the event
func (e *LogEvent) Str(key, val string) *LogEvent {
e.fields = append(e.fields, zap.String(key, val))
return e
}
// Int adds an integer field to the event
func (e *LogEvent) Int(key string, val int) *LogEvent {
e.fields = append(e.fields, zap.Int(key, val))
return e
}
// Int64 adds an int64 field to the event
func (e *LogEvent) Int64(key string, val int64) *LogEvent {
e.fields = append(e.fields, zap.Int64(key, val))
return e
}
// Err adds an error field to the event
func (e *LogEvent) Err(err error) *LogEvent {
if err != nil {
e.fields = append(e.fields, zap.Error(err))
}
return e
}
// Bool adds a boolean field to the event
func (e *LogEvent) Bool(key string, val bool) *LogEvent {
e.fields = append(e.fields, zap.Bool(key, val))
return e
}
// Interface adds an interface field to the event
func (e *LogEvent) Interface(key string, val interface{}) *LogEvent {
e.fields = append(e.fields, zap.Any(key, val))
return e
}
// Any adds an interface field to the event (alias for Interface)
func (e *LogEvent) Any(key string, val interface{}) *LogEvent {
return e.Interface(key, val)
}
// Strs adds a string slice field to the event
func (e *LogEvent) Strs(key string, vals []string) *LogEvent {
e.fields = append(e.fields, zap.Strings(key, vals))
return e
}
// Log emits the logs for a particular logging event.
// The request id associated with the context will be included if presents.
func Log(ctx context.Context, e *LogEvent, format string, v ...any) {
id, ok := ctx.Value(ReqIdCtxKey{}).(string)
if !ok {
e.Msgf(format, v...)
return
}
e.MsgFunc(func() string {
return fmt.Sprintf("[%s] %s", id, fmt.Sprintf(format, v...))
})
}
// Logger methods that mimic zerolog API
func (l *Logger) Debug() *LogEvent {
return &LogEvent{
logger: l.Logger,
level: zapcore.DebugLevel,
fields: []zap.Field{},
}
}
func (l *Logger) Info() *LogEvent {
return &LogEvent{
logger: l.Logger,
level: zapcore.InfoLevel,
fields: []zap.Field{},
}
}
func (l *Logger) Warn() *LogEvent {
return &LogEvent{
logger: l.Logger,
level: zapcore.WarnLevel,
fields: []zap.Field{},
}
}
func (l *Logger) Error() *LogEvent {
return &LogEvent{
logger: l.Logger,
level: zapcore.ErrorLevel,
fields: []zap.Field{},
}
}
func (l *Logger) Fatal() *LogEvent {
return &LogEvent{
logger: l.Logger,
level: zapcore.FatalLevel,
fields: []zap.Field{},
}
}
func (l *Logger) Notice() *LogEvent {
return &LogEvent{
logger: l.Logger,
level: zapcore.InfoLevel, // zap doesn't have Notice level, use Info
fields: []zap.Field{},
}
}
// With returns a logger with additional fields
func (l *Logger) With() *Logger {
return l
}
// Str adds a string field to the logger
func (l *Logger) Str(key, val string) *Logger {
// Create a new logger with the field added
newLogger := l.Logger.With(zap.String(key, val))
return &Logger{newLogger}
}
// Err adds an error field to the logger
func (l *Logger) Err(err error) *Logger {
// Create a new logger with the error field added
newLogger := l.Logger.With(zap.Error(err))
return &Logger{newLogger}
}
// Any adds an interface field to the logger
func (l *Logger) Any(key string, val interface{}) *Logger {
// Create a new logger with the field added
newLogger := l.Logger.With(zap.Any(key, val))
return &Logger{newLogger}
}
// Bool adds a boolean field to the logger
func (l *Logger) Bool(key string, val bool) *Logger {
// Create a new logger with the field added
newLogger := l.Logger.With(zap.Bool(key, val))
return &Logger{newLogger}
}
// Msgf logs a formatted message at info level
func (l *Logger) Msgf(format string, v ...any) {
l.Info().Msgf(format, v...)
}
// Msg logs a message at info level
func (l *Logger) Msg(msg string) {
l.Info().Msg(msg)
}
// Output returns a logger with the specified output
func (l *Logger) Output(w io.Writer) *Logger {
// Create a new zap logger with the writer
encoderConfig := zap.NewDevelopmentEncoderConfig()
encoderConfig.TimeKey = "time"
encoderConfig.EncodeTime = zapcore.TimeEncoderOfLayout(time.RFC3339)
encoder := zapcore.NewConsoleEncoder(encoderConfig)
core := zapcore.NewCore(encoder, zapcore.AddSync(w), zapcore.InfoLevel)
newLogger := zap.New(core)
return &Logger{newLogger}
}
// GetLogger returns the underlying logger
func (l *Logger) GetLogger() *Logger {
return l
}
// Write implements io.Writer to allow direct writing to the logger
func (l *Logger) Write(p []byte) (n int, err error) {
l.Info().Msg(string(p))
return len(p), nil
}
// Printf logs a formatted message at info level
func (l *Logger) Printf(format string, v ...any) {
l.Info().Msgf(format, v...)
}