mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-02-03 22:18:39 +00:00
- 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
255 lines
6.6 KiB
Go
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...)
|
|
}
|