Files
ctrld/log.go
Cuong Manh Le 69b192c6fa feat: add custom NOTICE log level between INFO and WARN
- Add NoticeLevel constant using zapcore.WarnLevel value (1)
- Implement custom level encoders (noticeLevelEncoder, noticeColorLevelEncoder)
- Update Notice() method to use custom level
- Add "notice" case to log level parsing in main.go
- Update encoder configurations to handle NOTICE level properly
- Add comprehensive test (TestNoticeLevel) to verify behavior

The NOTICE level provides visual distinction from INFO and ERROR levels,
with cyan color in development and proper level filtering. When log level
is set to NOTICE, it shows NOTICE and above (WARN, ERROR) while filtering
out DEBUG and INFO messages.

Note: NOTICE and WARN share the same numeric value (1) due to zap's
integer-based level system, so both display as "NOTICE" in logs for
visual consistency.

Usage:
- logger.Notice().Msg("message")
- log_level = "notice" in config
- Supports structured logging with fields
2025-10-09 17:49:21 +07:00

255 lines
6.6 KiB
Go

package ctrld
import (
"context"
"fmt"
"io"
"time"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
)
// Custom log level for NOTICE (between INFO and WARN)
// DEBUG = -1, INFO = 0, WARN = 1, ERROR = 2, FATAL = 3
// Since there's no integer between INFO (0) and WARN (1), we'll use the same value as WARN
// but handle NOTICE specially in the encoder to display it differently.
// Note: NOTICE and WARN share the same numeric value (1), so they will both display as "NOTICE"
// when using the custom encoder. This is the intended behavior for visual distinction.
const NoticeLevel = zapcore.Level(zapcore.WarnLevel) // Same value as WARN, but handled specially
// 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: NoticeLevel, // Custom NOTICE level between INFO and WARN
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...)
}