diff --git a/go.mod b/go.mod index f09f254..04bea2f 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/apus-run/van -go 1.22 +go 1.23 replace github.com/ugorji/go => github.com/ugorji/go v1.2.12 diff --git a/log/slog/attr.go b/log/slog/attr.go deleted file mode 100644 index 3b0da41..0000000 --- a/log/slog/attr.go +++ /dev/null @@ -1,13 +0,0 @@ -package slog - -import "log/slog" - -var ErrorKey = "error" - -func ErrorString(err error) slog.Attr { - return slog.String(ErrorKey, err.Error()) -} - -func ErrorValue(err error) slog.Value { - return slog.StringValue(err.Error()) -} diff --git a/log/slog/attrs.go b/log/slog/attrs.go new file mode 100644 index 0000000..6ca72a1 --- /dev/null +++ b/log/slog/attrs.go @@ -0,0 +1,68 @@ +package slog + +import ( + "log/slog" + "time" +) + +var ErrorKey = "error" + +func ErrorString(err error) Attr { + return slog.String(ErrorKey, err.Error()) +} + +func ErrorValue(err error) slog.Value { + return slog.StringValue(err.Error()) +} + +// TimeValue returns a Value for a time.Time. +// It discards the monotonic portion. +func TimeValue(v time.Time) any { + return uint64(v.UnixNano()) +} + +// DurationValue returns a Value for a time.Duration. +func DurationValue(v time.Duration) uint64 { + return uint64(v.Nanoseconds()) +} + +func String(key, v string) Attr { + return slog.String(key, v) +} + +func Int64(key string, v int64) Attr { + return slog.Int64(key, v) +} + +func Int(key string, v int) Attr { + return slog.Int(key, v) +} + +// Uint64 returns an Attr for a uint64. +func Uint64(key string, v uint64) Attr { + return slog.Uint64(key, v) +} + +func Float64(key string, v float64) Attr { + return slog.Float64(key, v) +} + +func Bool(key string, v bool) Attr { + return slog.Bool(key, v) +} + +func Time(key string, v time.Time) Attr { + return slog.Time(key, v) +} + +func Duration(key string, v time.Duration) Attr { + return slog.Duration(key, v) +} + +func Group(key string, args ...any) Attr { + return slog.Group(key, args...) +} + +func Any(key string, v any) Attr { + return slog.Any(key, v) +} diff --git a/log/slog/context.go b/log/slog/context.go index b536ae6..b6eb411 100644 --- a/log/slog/context.go +++ b/log/slog/context.go @@ -2,29 +2,30 @@ package slog import ( "context" - "log/slog" ) type ContextLogKey struct{} -func NewContext(ctx context.Context, l *slog.Logger) context.Context { +func NewContext(ctx context.Context, l *SlogLogger) context.Context { return context.WithValue(ctx, ContextLogKey{}, l) } -func WithContext(ctx context.Context, l *slog.Logger) context.Context { - if _, ok := ctx.Value(ContextLogKey{}).(*slog.Logger); ok { +func WithContext(ctx context.Context, l *SlogLogger) context.Context { + if _, ok := ctx.Value(ContextLogKey{}).(*SlogLogger); ok { return ctx } return context.WithValue(ctx, ContextLogKey{}, l) } -func FromContext(ctx context.Context) *slog.Logger { - if l, ok := ctx.Value(ContextLogKey{}).(*slog.Logger); ok { +func FromContext(ctx context.Context) *SlogLogger { + if l, ok := ctx.Value(ContextLogKey{}).(*SlogLogger); ok { return l } - return slog.Default() + return nil } -//func log(ctx context.Context, level slog.Level, msg string, args ...any) { -// FromContext(ctx).Log(ctx, level, msg, args...) -//} +// C represents for `FromContext` with empty keyvals. +// slog.C(ctx).Info("Set function called") +func C(ctx context.Context) *SlogLogger { + return FromContext(ctx) +} diff --git a/log/slog/format.go b/log/slog/format.go new file mode 100644 index 0000000..5df144a --- /dev/null +++ b/log/slog/format.go @@ -0,0 +1,53 @@ +package slog + +import ( + "fmt" +) + +// sprint is a function that takes a variadic parameter of type interface{} and returns a string. +// The function works as follows: +// - If no arguments are provided, it returns an empty string. +// - If a single argument is provided: +// - If the argument is of type string, it returns the string as is. +// - If the argument is not of type string but implements the fmt.Stringer interface, it returns the string representation of the argument. +// - If the argument is not of type string and does not implement the fmt.Stringer interface, it converts the argument to a string using fmt.Sprint and returns the result. +// +// - If more than one argument is provided, it converts all arguments to a string using fmt.Sprint and returns the result. +func sprint(a ...any) string { + if len(a) == 0 { + return "" + } else if len(a) == 1 { + if s, ok := a[0].(string); ok { + return s + } else if v, ok := a[0].(fmt.Stringer); ok { + return v.String() + } else { + return fmt.Sprint(a...) + } + } else { + return fmt.Sprint(a...) + } +} + +// sprintf is a function that takes a string template and a variadic parameter of type interface{} and returns a string. +// The function works as follows: +// - If no arguments are provided, it returns the template string as is. +// - If the template string is not empty, it formats the string using fmt.Sprintf with the provided arguments and returns the result. +// - If only one argument is provided and it is of type string, it returns the string as is. +// - Otherwise, it converts the arguments to a string using the sprint function and returns the result. +func sprintf(template string, args ...any) string { + if len(args) == 0 { + return template + } + + if template != "" { + return fmt.Sprintf(template, args...) + } + + if len(args) == 1 { + if str, ok := args[0].(string); ok { + return str + } + } + return sprint(args...) +} diff --git a/log/slog/format_test.go b/log/slog/format_test.go new file mode 100644 index 0000000..e5f9f30 --- /dev/null +++ b/log/slog/format_test.go @@ -0,0 +1,120 @@ +package slog + +import ( + "fmt" + "testing" +) + +func Test_sprint(t *testing.T) { + tests := []struct { + name string + args []interface{} + want string + }{ + { + name: "NoArgs", + args: []interface{}{}, + want: "", + }, + { + name: "WithOneArgString", + args: []interface{}{"arg1"}, + want: "arg1", + }, + { + name: "WithOneArgNotString", + args: []interface{}{123}, + want: "123", + }, + { + name: "WithMultipleArgsString", + args: []interface{}{"arg1", "arg2"}, + want: "arg1arg2", + }, + { + name: "WithMultipleArgsNotString", + args: []interface{}{123, 456}, + want: "123 456", + }, + { + name: "WithErrorArgs", + args: []interface{}{fmt.Errorf("error message")}, + want: "error message", + }, + { + name: "WithStringerArgs", + args: []interface{}{stringer{str: "stringer"}}, + want: "stringer", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := sprint(tt.args...); got != tt.want { + t.Errorf("sprint() = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_sprintf(t *testing.T) { + type args struct { + template string + args []interface{} + } + tests := []struct { + name string + args args + want string + }{ + { + name: "NoArgs", + args: args{template: "template", args: []interface{}{}}, + want: "template", + }, + { + name: "WithTemplateAndOneArg", + args: args{template: "template %s", args: []interface{}{"arg1"}}, + want: "template arg1", + }, + { + name: "WithTemplateAndMultipleArgs", + args: args{template: "template %s %s", args: []interface{}{"arg1", "arg2"}}, + want: "template arg1 arg2", + }, + { + name: "WithOneArgNotString", + args: args{template: "", args: []interface{}{123}}, + want: "123", + }, + { + name: "WithMultipleArgsNotString", + args: args{template: "", args: []interface{}{123, 456}}, + want: "123 456", + }, + { + name: "WithErrorArgs", + args: args{template: "", args: []interface{}{fmt.Errorf("error message")}}, + want: "error message", + }, + { + name: "WithStringerArgs", + args: args{template: "", args: []interface{}{stringer{str: "stringer"}}}, + want: "stringer", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := sprintf(tt.args.template, tt.args.args...); got != tt.want { + t.Errorf("sprintf() = %v, want %v", got, tt.want) + } + }) + } +} + +type stringer struct { + str string +} + +func (s stringer) String() string { + return s.str +} diff --git a/log/slog/global.go b/log/slog/global.go index ad225f7..29089fc 100644 --- a/log/slog/global.go +++ b/log/slog/global.go @@ -2,7 +2,6 @@ package slog import ( "context" - "log/slog" "sync" ) @@ -13,16 +12,16 @@ var global = &loggerAppliance{} // make logger change will affect all sub-logger. type loggerAppliance struct { lock sync.Mutex - slog.Logger + Logger } func init() { logger := NewLogger() - global.SetLogger(*logger) + global.SetLogger(logger) } -func (a *loggerAppliance) SetLogger(in slog.Logger) { +func (a *loggerAppliance) SetLogger(in Logger) { a.lock.Lock() defer a.lock.Unlock() a.Logger = in @@ -30,52 +29,101 @@ func (a *loggerAppliance) SetLogger(in slog.Logger) { // SetLogger should be called before any other log call. // And it is NOT THREAD SAFE. -func SetLogger(logger slog.Logger) { +func SetLogger(logger Logger) { global.SetLogger(logger) } // GetLogger returns global logger appliance as logger in current process. -func GetLogger() slog.Logger { +func GetLogger() Logger { return global.Logger } // L 是 GetLogger 简写 -func L() slog.Logger { +func L() Logger { + return global.Logger +} + +// Default 是 GetLogger +func Default() Logger { return global.Logger } -func Info(msg string, args ...any) { - global.Info(msg, args...) +func Info(msg string, args ...Attr) { + L().Info(msg, args...) +} + +func Infof(format string, args ...any) { + L().Infof(format, args...) +} + +func InfoContext(ctx context.Context, msg string, args ...Attr) { + L().InfoContext(ctx, msg, args...) +} + +func Error(msg string, args ...Attr) { + L().Error(msg, args...) +} + +func Errorf(format string, args ...any) { + L().Errorf(format, args...) +} + +func ErrorContext(ctx context.Context, msg string, args ...Attr) { + L().ErrorContext(ctx, msg, args...) +} + +func Debug(msg string, args ...Attr) { + L().Debug(msg, args...) +} + +func Debugf(format string, args ...any) { + L().Debugf(format, args...) +} + +func DebugContext(ctx context.Context, msg string, args ...Attr) { + L().DebugContext(ctx, msg, args...) +} + +func Warn(msg string, args ...Attr) { + L().Warn(msg, args...) +} + +func Warnf(format string, args ...any) { + L().Warnf(format, args...) +} + +func WarnContext(ctx context.Context, msg string, args ...Attr) { + L().WarnContext(ctx, msg, args...) } -func InfoContext(ctx context.Context, msg string, args ...any) { - global.InfoContext(ctx, msg, args...) +func Fatal(msg string, args ...Attr) { + L().Fatal(msg, args...) } -func Error(msg string, args ...any) { - global.Error(msg, args...) +func Fatalf(format string, args ...any) { + L().Fatalf(format, args...) } -func ErrorContext(ctx context.Context, msg string, args ...any) { - global.ErrorContext(ctx, msg, args...) +func FatalContext(ctx context.Context, msg string, args ...Attr) { + L().FatalContext(ctx, msg, args...) } -func Debug(msg string, args ...any) { - global.Debug(msg, args...) +func Panic(msg string, args ...Attr) { + L().Panic(msg, args...) } -func DebugContext(ctx context.Context, msg string, args ...any) { - global.DebugContext(ctx, msg, args...) +func Panicf(format string, args ...any) { + L().Panicf(format, args...) } -func Warn(msg string, args ...any) { - global.Warn(msg, args...) +func PanicContext(ctx context.Context, msg string, args ...Attr) { + L().PanicContext(ctx, msg, args...) } -func WarnContext(ctx context.Context, msg string, args ...any) { - global.WarnContext(ctx, msg, args...) +func With(args ...any) *SlogLogger { + return L().With(args...) } -func WithGroup(name string) *slog.Logger { - return global.WithGroup(name) +func WithGroup(name string) *SlogLogger { + return L().WithGroup(name) } diff --git a/log/slog/global_test.go b/log/slog/global_test.go index 856ae85..5b896ca 100644 --- a/log/slog/global_test.go +++ b/log/slog/global_test.go @@ -7,7 +7,7 @@ import ( func TestGlobalLog(t *testing.T) { la := &loggerAppliance{} logger := NewLogger() - la.SetLogger(*logger) + la.SetLogger(logger) Info("test info") Error("test error") diff --git a/log/slog/gorm/logger.go b/log/slog/gorm/logger.go new file mode 100644 index 0000000..88c8f7c --- /dev/null +++ b/log/slog/gorm/logger.go @@ -0,0 +1,80 @@ +package gorm + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" + + "gorm.io/gorm" + "gorm.io/gorm/logger" + + log "github.com/apus-run/van/log/slog" +) + +// Logger adapts to gorm logger +type Logger struct { + *log.SlogLogger + *Config +} + +func NewLogger(options ...Option) *Logger { + + // 修改配置 + config := Apply(options...) + l := log.NewLogger(log.WithOptions(func(lopts *log.Options) { + lopts.LogLevel = config.LogLevel + lopts.Format = config.Format + lopts.Writer = config.Writer + lopts.LogGroup = config.LogGroup + lopts.LogAttrs = config.LogAttrs + })) + + return &Logger{ + l, + config, + } +} + +func (l *Logger) LogMode(level logger.LogLevel) logger.Interface { + return l +} + +func (l *Logger) Info(ctx context.Context, msg string, i ...interface{}) { + l.Log(ctx, 5, nil, slog.LevelInfo, msg, i...) +} + +func (l *Logger) Warn(ctx context.Context, msg string, i ...interface{}) { + l.Log(ctx, 5, nil, slog.LevelWarn, msg, i...) +} + +func (l *Logger) Error(ctx context.Context, msg string, i ...interface{}) { + l.Log(ctx, 5, nil, slog.LevelError, msg, i...) +} + +func (l *Logger) Trace(ctx context.Context, begin time.Time, fc func() (sql string, rowsAffected int64), err error) { + elapsed := time.Since(begin) + sql, rows := fc() + switch { + case err != nil && (!l.IgnoreRecordNotFoundError || !errors.Is(err, gorm.ErrRecordNotFound)): + l.Log(ctx, 5, err, slog.LevelError, "query error", + slog.String("elapsed", elapsed.String()), + slog.Int64("rows", rows), + slog.String("sql", sql), + ) + case l.SlowThreshold != 0 && elapsed > l.SlowThreshold: + msg := fmt.Sprintf("slow threshold >= %v", l.SlowThreshold) + l.Log(ctx, 5, nil, slog.LevelWarn, msg, + slog.String("elapsed", elapsed.String()), + slog.Int64("rows", rows), + slog.String("sql", sql), + ) + case l.LogInfo: + l.Log(ctx, 5, nil, slog.LevelInfo, "query info", + slog.String("elapsed", elapsed.String()), + slog.Int64("rows", rows), + slog.String("sql", sql), + ) + } +} diff --git a/log/slog/gorm/types.go b/log/slog/gorm/types.go new file mode 100644 index 0000000..e98b9ba --- /dev/null +++ b/log/slog/gorm/types.go @@ -0,0 +1,70 @@ +package gorm + +import ( + "time" + + log "github.com/apus-run/van/log/slog" +) + +// Config is logger config +type Config struct { + SlowThreshold time.Duration + IgnoreRecordNotFoundError bool + LogInfo bool + + *log.Options +} + +type Option func(*Config) + +func DefaultDBConfig() *Config { + return &Config{ + SlowThreshold: time.Second * 2, + IgnoreRecordNotFoundError: true, + LogInfo: true, + Options: log.DefaultOptions(), + } +} + +func Apply(opts ...Option) *Config { + config := DefaultDBConfig() + for _, opt := range opts { + opt(config) + } + return config +} + +// WithSlowThreshold set slow threshold +func WithSlowThreshold(threshold time.Duration) Option { + return func(c *Config) { + c.SlowThreshold = threshold + } +} + +// WithIgnoreRecordNotFoundError ignore record not found error +func WithIgnoreRecordNotFoundError() Option { + return func(c *Config) { + c.IgnoreRecordNotFoundError = true + } +} + +// WithLogInfo set log info +func WithLogInfo(logInfo bool) Option { + return func(c *Config) { + c.LogInfo = logInfo + } +} + +// WithConfig set all config +func WithConfig(fn func(config *Config)) Option { + return func(config *Config) { + fn(config) + } +} + +// WithSlogOptions 设置 slog.Options +func WithSlogOptions(options *log.Options) Option { + return func(conf *Config) { + conf.Options = options + } +} diff --git a/log/slog/log.go b/log/slog/log.go index 33dfdd0..e77fb6d 100644 --- a/log/slog/log.go +++ b/log/slog/log.go @@ -3,8 +3,8 @@ package slog import ( + "context" "fmt" - "go/build" "io" "log/slog" @@ -12,53 +12,74 @@ import ( "path/filepath" "runtime" "strings" - "sync" "time" - errorsx "github.com/apus-run/van/log/slog/errors" - "gopkg.in/natefinch/lumberjack.v2" + "github.com/pkg/errors" ) const LOGGER_KEY = "slogLogger" -const AttrErrorKey = "error" -var factory = &LoggerFactory{ - loggers: make(map[string]*slog.Logger), +const ( + LevelDebug = slog.LevelDebug + LevelInfo = slog.LevelInfo + LevelWarn = slog.LevelWarn + LevelError = slog.LevelError + LevelPanic = slog.Level(10) + LevelFatal = slog.Level(12) +) + +var LevelNames = map[slog.Leveler]string{ + LevelFatal: "FATAL", + LevelError: "ERROR", + LevelWarn: "WARN", + LevelInfo: "INFO", + LevelDebug: "DEBUG", + LevelPanic: "PANIC", } -type LoggerFactory struct { - mu sync.Mutex - loggers map[string]*slog.Logger +type SlogLogger struct { + *slog.Logger } var defaultHandler *Handler -func NewLogger(opts ...Option) *slog.Logger { +// NewSlogLogger 只包装了 slog +func NewSlogLogger(l *slog.Logger) Logger { + if l == nil { + l = slog.Default() + } + return &SlogLogger{l} +} + +func NewLogger(opts ...Option) *SlogLogger { options := Apply(opts...) - factory.mu.Lock() - if logger, ok := factory.loggers[options.LogFilename]; ok { - factory.mu.Unlock() - return logger - } - defer factory.mu.Unlock() + handler := createHandler(options) + logger := slog.New(handler) - // 日志文件切割归档 - writerSyncer := getLogWriter(options) + return &SlogLogger{ + Logger: logger, + } +} +func createHandler(options *Options) slog.Handler { + var handler slog.Handler // 日志级别 level := getLogLevel(options.LogLevel) - var handler slog.Handler handlerOptions := &slog.HandlerOptions{ Level: level, AddSource: true, ReplaceAttr: ReplaceAttr, } - if len(options.LogFilename) == 0 && strings.ToLower(options.Encoding) == "console" { + + switch f := options.Format; f { + case FormatText: handler = slog.NewTextHandler(os.Stdout, handlerOptions) - } else { - handler = slog.NewJSONHandler(writerSyncer, handlerOptions) + case FormatJSON: + handler = slog.NewJSONHandler(options.Writer, handlerOptions) + default: + handler = slog.NewJSONHandler(options.Writer, handlerOptions) } if options.LogGroup != "" { @@ -67,34 +88,31 @@ func NewLogger(opts ...Option) *slog.Logger { if len(options.LogAttrs) > 0 { handler = handler.WithAttrs(options.LogAttrs) } - defaultHandler = NewHandler(handler).(*Handler) - logger := slog.New(defaultHandler) - // 此处设置默认日志, 最好手动设置 - // slog.SetDefault(l) - factory.loggers[options.LogFilename] = logger - - logger.Info("the log module has been initialized successfully.", slog.Any("options", options)) - - return logger + return defaultHandler } -func getLogWriter(opts *Options) io.WriteCloser { - return &lumberjack.Logger{ - Filename: opts.LogFilename, - MaxSize: opts.MaxSize, // megabytes - MaxBackups: opts.MaxBackups, - MaxAge: opts.MaxAge, //days - Compress: opts.Compress, +// NewNop returns a no-op logger +func NewNop() *slog.Logger { + nopLevel := slog.Level(-99) + ops := &slog.HandlerOptions{ + Level: nopLevel, } + handler := slog.NewTextHandler(io.Discard, ops) + return slog.New(handler) +} + +// NewWithHandler build *slog.Logger with slog Handler +func NewWithHandler(handler slog.Handler) *slog.Logger { + return slog.New(handler) } func getLogLevel(logLevel string) slog.Level { level := new(slog.Level) err := level.UnmarshalText([]byte(logLevel)) if err != nil { - return slog.LevelError + return LevelError } return *level @@ -104,6 +122,8 @@ func ApplyHandlerOption(opt HandlerOption) { defaultHandler.Apply(opt) } +const AttrErrorKey = "error" + // ReplaceAttr handle log key-value pair func ReplaceAttr(groups []string, a slog.Attr) slog.Attr { switch a.Key { @@ -118,13 +138,13 @@ func ReplaceAttr(groups []string, a slog.Attr) slog.Attr { return a case AttrErrorKey: v, ok := a.Value.Any().(interface { - StackTrace() errorsx.StackTrace + StackTrace() errors.StackTrace }) if ok { st := v.StackTrace() return slog.Any(a.Key, slog.GroupValue( slog.String("msg", a.Value.String()), - slog.Any("stack", errorsx.StackTrace(st)), + slog.Any("stack", errors.StackTrace(st)), )) } return a @@ -148,3 +168,126 @@ func getBriefSource(source string) string { pp := filepath.ToSlash(projectPath()) return strings.TrimPrefix(source, pp+"/") } + +// Log send log records with caller depth +func (l *SlogLogger) Log(ctx context.Context, depth int, err error, level slog.Level, msg string, attrs ...any) { + if !l.Enabled(ctx, level) { + return + } + + // 记录日志 + l.Logger.Log(ctx, level, msg, attrs...) +} + +func (l *SlogLogger) LogAttrs(ctx context.Context, level slog.Level, msg string, attrs ...Attr) { + if !l.Enabled(ctx, level) { + return + } + l.Logger.LogAttrs(ctx, level, msg, attrs...) +} + +// 实现 Logger 接口的方法 +func (l *SlogLogger) Info(msg string, attrs ...Attr) { + l.Logger.LogAttrs(context.Background(), LevelInfo, msg, attrs...) +} + +func (l *SlogLogger) Infof(msg string, args ...any) { + l.Logger.Log(context.Background(), LevelInfo, sprintf(msg, args...)) +} + +func (l *SlogLogger) InfoContext(ctx context.Context, msg string, attrs ...Attr) { + l.Logger.LogAttrs(ctx, LevelInfo, msg, attrs...) +} + +func (l *SlogLogger) Error(msg string, attrs ...Attr) { + l.Logger.LogAttrs(context.Background(), LevelError, msg, attrs...) +} + +func (l *SlogLogger) Errorf(msg string, args ...any) { + l.Logger.Log(context.Background(), LevelError, sprintf(msg, args...)) +} + +func (l *SlogLogger) ErrorContext(ctx context.Context, msg string, attrs ...Attr) { + l.Logger.LogAttrs(ctx, LevelError, msg, attrs...) +} + +func (l *SlogLogger) Debug(msg string, attrs ...Attr) { + l.Logger.LogAttrs(context.Background(), LevelDebug, msg, attrs...) +} + +func (l *SlogLogger) Debugf(msg string, args ...any) { + l.Logger.Log(context.Background(), LevelDebug, sprintf(msg, args...)) +} + +func (l *SlogLogger) DebugContext(ctx context.Context, msg string, attrs ...Attr) { + l.Logger.LogAttrs(ctx, LevelDebug, msg, attrs...) +} + +func (l *SlogLogger) Warn(msg string, attrs ...Attr) { + l.Logger.LogAttrs(context.Background(), LevelWarn, msg, attrs...) +} + +func (l *SlogLogger) Warnf(msg string, args ...any) { + l.Logger.Log(context.Background(), LevelWarn, sprintf(msg, args...)) +} + +func (l *SlogLogger) WarnContext(ctx context.Context, msg string, attrs ...Attr) { + l.Logger.LogAttrs(ctx, LevelWarn, msg, attrs...) +} + +func (l *SlogLogger) Fatal(msg string, attrs ...Attr) { + l.Logger.LogAttrs(context.Background(), LevelFatal, msg, attrs...) + os.Exit(1) +} + +func (l *SlogLogger) FatalContext(ctx context.Context, msg string, attrs ...Attr) { + l.Logger.LogAttrs(ctx, LevelFatal, msg, attrs...) + os.Exit(1) +} + +func (l *SlogLogger) Fatalf(msg string, args ...any) { + l.Logger.Log(context.Background(), LevelFatal, sprintf(msg, args...)) + os.Exit(1) +} + +func (l *SlogLogger) Panic(msg string, attrs ...Attr) { + l.Logger.LogAttrs(context.Background(), LevelPanic, msg, attrs...) + panic(SprintfWithAttrs(msg, attrs...)) +} + +func (l *SlogLogger) PanicContext(ctx context.Context, msg string, attrs ...Attr) { + l.Logger.LogAttrs(ctx, LevelPanic, msg, attrs...) + panic(SprintfWithAttrs(msg, attrs...)) +} + +func (l *SlogLogger) Panicf(msg string, args ...any) { + l.Logger.Log(context.Background(), LevelPanic, sprintf(msg, args...)) + panic(fmt.Sprintf(msg, args...)) +} + +func SprintfWithAttrs(format string, attrs ...Attr) string { + // 处理 Attr 类型并将其转换为字符串 + attrStr := "" + for _, attr := range attrs { + attrStr += fmt.Sprintf("%v ", attr) // 根据需要格式化 Attr + } + return fmt.Sprintf(format, attrStr) +} + +func (l *SlogLogger) With(args ...any) *SlogLogger { + lc := l.clone() + lc.Logger = lc.Logger.With(args...) + return lc +} + +func (l *SlogLogger) WithGroup(name string) *SlogLogger { + lc := l.clone() + lc.Logger = lc.Logger.WithGroup(name) + return lc +} + +// clone 深度拷贝 SlogLogger. +func (l *SlogLogger) clone() *SlogLogger { + copied := *l + return &copied +} diff --git a/log/slog/log_test.go b/log/slog/log_test.go index b7fdfa8..e077292 100644 --- a/log/slog/log_test.go +++ b/log/slog/log_test.go @@ -2,11 +2,12 @@ package slog import ( "context" - "log" "log/slog" "sync/atomic" "testing" "time" + + "gopkg.in/natefinch/lumberjack.v2" ) type Password string @@ -16,26 +17,30 @@ func (Password) LogValue() slog.Value { } func TestLog(t *testing.T) { - logger := NewLogger(WithEncoding("json"), WithFilename("test.log")) - logger.Debug("This is a debug message", slog.Any("key", "value")) + lumberJackLogger := &lumberjack.Logger{ + Filename: "logs.log", + MaxSize: 500, // megabytes + MaxBackups: 3, + MaxAge: 28, //days + Compress: true, + } + + logger := NewLogger(WithFormat(FormatJSON), WithWriter(lumberJackLogger)) + logger.Debug("This is a debug message", Any("key", "value")) logger.Info("This is a info message") logger.Warn("This is a warn message") logger.Error("This is a error message") + logger.Fatalf("ssssss%v", "22222") logger.Info("WebServer服务信息", - slog.Group("http", - slog.Int("status", 200), - slog.String("method", "POST"), - slog.Time("time", time.Now()), + Group("http", + Int("status", 200), + String("method", "POST"), + Time("time", time.Now()), ), ) - log.Print("This is a print message") - - slog.Info("敏感数据", slog.Any("password", Password("1234567890"))) - - // 设置slog为默认日志 - slog.SetDefault(logger) + logger.Info("敏感数据", Any("password", Password("1234567890"))) } func TestNewLogger(t *testing.T) { @@ -43,22 +48,20 @@ func TestNewLogger(t *testing.T) { ctx := context.WithValue(context.Background(), "foobar", "helloworld") logger := NewLogger( - WithEncoding("json"), + WithFormat(FormatJSON), WithLogLevel("debug"), - WithFilename("test.log"), ) ApplyHandlerOption(WithHandleFunc(func(ctx context.Context, r *slog.Record) { - r.AddAttrs(slog.String("value", ctx.Value("foobar").(string))) + r.AddAttrs(String("value", ctx.Value("foobar").(string))) atomic.AddInt32(&called, 1) })) - //logger = logger.With(slog.String("sub_logger", "true")) - //ctx = NewContext(ctx, logger) - //logger = FromContext(ctx) - //logger.InfoContext(ctx, "print something") - - logger = logger.With(slog.String("sub_logger", "true")) - ctx = WithContext(ctx, logger) + // logger = With(String("sub_logger", "true")) + // ctx = NewContext(ctx, logger) + // logger = FromContext(ctx) + // logger.InfoContext(ctx, "print something") + l := logger.WithGroup("moocss").With(String("sub_logger", "true")) + ctx = WithContext(ctx, l) logger = FromContext(ctx) t.Logf("%#v", logger) logger.InfoContext(ctx, "print something") diff --git a/log/slog/logs.log b/log/slog/logs.log new file mode 100644 index 0000000..23186b6 --- /dev/null +++ b/log/slog/logs.log @@ -0,0 +1,4 @@ +{"time":"2024-11-22T17:12:40+08:00","level":"info","source":"log/slog/log.go:191","msg":"This is a info message"} +{"time":"2024-11-22T17:12:40+08:00","level":"warn","source":"log/slog/log.go:227","msg":"This is a warn message"} +{"time":"2024-11-22T17:12:40+08:00","level":"error","source":"log/slog/log.go:203","msg":"This is a error message","stack":"goroutine 18 [running]:\nruntime/debug.Stack()\n\t/usr/local/go/src/runtime/debug/stack.go:26 +0x5e\ngithub.com/apus-run/van/log/slog.(*Handler).errorLogWithStackTrack(0xc0000aec00, {0x36a2ee0?, 0x37c0fa0?}, 0xc0000f4360)\n\t/Users/moocss/work/GoProjects/van/log/slog/handler.go:76 +0x85\ngithub.com/apus-run/van/log/slog.(*Handler).Handle(_, {_, _}, {{0xc1c830a232ab0608, 0xf362b, 0x37a1200}, {0x3626940, 0x17}, 0x8, 0x3620a86, ...})\n\t/Users/moocss/work/GoProjects/van/log/slog/handler.go:53 +0xb3\nlog/slog.(*Logger).logAttrs(0xc0000a03b0, {0x36a2ee0, 0x37c0fa0}, 0x8, {0x3626940, 0x17}, {0x0, 0x0, 0x0})\n\t/usr/local/go/src/log/slog/logger.go:277 +0x1e4\nlog/slog.(*Logger).LogAttrs(...)\n\t/usr/local/go/src/log/slog/logger.go:195\ngithub.com/apus-run/van/log/slog.(*SlogLogger).Error(...)\n\t/Users/moocss/work/GoProjects/van/log/slog/log.go:203\ngithub.com/apus-run/van/log/slog.TestLog(0xc0000b4680?)\n\t/Users/moocss/work/GoProjects/van/log/slog/log_test.go:32 +0x27f\ntesting.tRunner(0xc0000b4680, 0x36a0768)\n\t/usr/local/go/src/testing/testing.go:1690 +0xf4\ncreated by testing.(*T).Run in goroutine 1\n\t/usr/local/go/src/testing/testing.go:1743 +0x390\n"} +{"time":"2024-11-22T17:12:40+08:00","level":"error+4","source":"log/slog/log.go:249","msg":"ssssss22222"} diff --git a/log/slog/options.go b/log/slog/options.go index 2fff630..ccdc611 100644 --- a/log/slog/options.go +++ b/log/slog/options.go @@ -1,36 +1,29 @@ package slog -import "log/slog" +import ( + "io" + "log/slog" + "os" +) // Option is config option. type Option func(*Options) type Options struct { // logger options - LogLevel string // debug, info, warn, error - Encoding string // console or json - LogGroup string // slog group - LogAttrs []slog.Attr - - // lumberjack options - LogFilename string - MaxSize int - MaxBackups int - MaxAge int - Compress bool + LogLevel string // debug, info, warn, error,debug, panic + Format Format // text or json + Writer io.Writer // 日志输出 + LogGroup string // slog group + LogAttrs []slog.Attr // 日志属性 } // DefaultOptions . func DefaultOptions() *Options { return &Options{ LogLevel: "info", - Encoding: "console", - - LogFilename: "logs.log", - MaxSize: 500, // megabytes - MaxBackups: 3, - MaxAge: 28, //days - Compress: true, + Format: FormatText, + Writer: os.Stdout, } } @@ -57,50 +50,29 @@ func WithLogGroup(group string) Option { } // WithLogAttrs 日志属性 -func WithLogAttrs(attrs []slog.Attr) Option { +func WithLogAttrs(attrs []Attr) Option { return func(o *Options) { o.LogAttrs = attrs } } -// WithEncoding 日志编码 -func WithEncoding(encoding string) Option { - return func(o *Options) { - o.Encoding = encoding - } -} - -// WithFilename 日志文件 -func WithFilename(filename string) Option { +// WithFormat 日志格式 +func WithFormat(format Format) Option { return func(o *Options) { - o.LogFilename = filename + o.Format = format } } -// WithMaxSize 日志文件大小 -func WithMaxSize(maxSize int) Option { +// WithWriter 日志输出 +func WithWriter(writer io.Writer) Option { return func(o *Options) { - o.MaxSize = maxSize + o.Writer = writer } } -// WithMaxBackups 日志文件最大备份数 -func WithMaxBackups(maxBackups int) Option { - return func(o *Options) { - o.MaxBackups = maxBackups - } -} - -// WithMaxAge 日志文件最大保存时间 -func WithMaxAge(maxAge int) Option { - return func(o *Options) { - o.MaxAge = maxAge - } -} - -// WithCompress 日志文件是否压缩 -func WithCompress(compress bool) Option { - return func(o *Options) { - o.Compress = compress +// WithOptions 设置所有配置 +func WithOptions(fn func(options *Options)) Option { + return func(options *Options) { + fn(options) } } diff --git a/log/slog/types.go b/log/slog/types.go new file mode 100644 index 0000000..cc64314 --- /dev/null +++ b/log/slog/types.go @@ -0,0 +1,38 @@ +package slog + +import ( + "context" + "log/slog" +) + +type Logger interface { + Info(msg string, attrs ...Attr) + Infof(format string, attrs ...any) + InfoContext(ctx context.Context, msg string, attrs ...Attr) + Error(msg string, attrs ...Attr) + Errorf(format string, args ...any) + ErrorContext(ctx context.Context, msg string, attrs ...Attr) + Debug(msg string, attrs ...Attr) + Debugf(format string, args ...any) + DebugContext(ctx context.Context, msg string, attrs ...Attr) + Warn(msg string, attrs ...Attr) + Warnf(format string, args ...any) + WarnContext(ctx context.Context, msg string, attrs ...Attr) + Panic(msg string, attrs ...Attr) + Panicf(format string, args ...any) + PanicContext(ctx context.Context, msg string, attrs ...Attr) + Fatal(msg string, attrs ...Attr) + Fatalf(format string, args ...any) + FatalContext(ctx context.Context, msg string, attrs ...Attr) + With(args ...any) *SlogLogger + WithGroup(name string) *SlogLogger +} + +type Attr = slog.Attr + +type Format string + +const ( + FormatText Format = "text" + FormatJSON Format = "json" +) diff --git a/log/zlog/global.go b/log/zlog/global.go index b1e9da7..91fdd4a 100644 --- a/log/zlog/global.go +++ b/log/zlog/global.go @@ -5,7 +5,7 @@ import ( ) // globalLogger is designed as a global logger in current process. -var global = loggerAppliance{} +var global = &loggerAppliance{} // loggerAppliance is the proxy of `Logger` to // make logger change will affect all sub-logger. diff --git a/log/zlog/global_test.go b/log/zlog/global_test.go index 2c1c8fe..dcfec3c 100644 --- a/log/zlog/global_test.go +++ b/log/zlog/global_test.go @@ -11,7 +11,7 @@ import ( func TestGlobalLogInit(t *testing.T) { la := &loggerAppliance{} - logger := NewLogger(WithEncoding("json"), WithFilename("test.log")) + logger := NewLogger(WithFormat(FormatJSON)) la.SetLogger(logger) la.Info("test info") @@ -43,6 +43,7 @@ func TestGlobalLog(t *testing.T) { zap.Time("time", time.Now()), zap.Duration("duration", time.Duration(int64(10))), ) + } func TestContext(t *testing.T) { diff --git a/log/zlog/gorm/logger.go b/log/zlog/gorm/logger.go index 5543ce5..f9dbdeb 100644 --- a/log/zlog/gorm/logger.go +++ b/log/zlog/gorm/logger.go @@ -23,15 +23,12 @@ func NewLogger(options ...Option) *Logger { // 修改配置 config := Apply(options...) z := zlog.NewLogger(zlog.WithOptions(func(zopts *zlog.Options) { - zopts.Encoding = config.Encoding - zopts.LogFilename = config.LogFilename - zopts.MaxSize = config.MaxSize - zopts.MaxBackups = config.MaxBackups - zopts.MaxAge = config.MaxAge - zopts.Compress = config.Compress zopts.Mode = config.Mode + zopts.Writer = config.Writer zopts.LogLevel = config.LogLevel + zopts.Format = config.Format })) + return &Logger{ z, config, diff --git a/log/zlog/gorm/types.go b/log/zlog/gorm/types.go index 63110e8..8908d6a 100644 --- a/log/zlog/gorm/types.go +++ b/log/zlog/gorm/types.go @@ -6,6 +6,16 @@ import ( "github.com/apus-run/van/log/zlog" ) +// Config is logger config +type Config struct { + SlowThreshold time.Duration + IgnoreRecordNotFoundError bool + LogInfo bool + + // 集成 zlog 配置 + *zlog.Options +} + type Option func(*Config) func DefaultDBConfig() *Config { @@ -55,13 +65,3 @@ func WithZlogOptions(options *zlog.Options) Option { conf.Options = options } } - -// Config is logger config -type Config struct { - SlowThreshold time.Duration - IgnoreRecordNotFoundError bool - LogInfo bool - - // 集成 zlog 配置 - *zlog.Options -} diff --git a/log/zlog/log.go b/log/zlog/log.go index 09cf10f..fcd1c5f 100644 --- a/log/zlog/log.go +++ b/log/zlog/log.go @@ -2,12 +2,10 @@ package zlog import ( "fmt" - "os" "time" "go.uber.org/zap" "go.uber.org/zap/zapcore" - "gopkg.in/natefinch/lumberjack.v2" ) const LOGGER_KEY = "zapLogger" @@ -35,17 +33,13 @@ func ZapProdConfig() zap.Config { func NewLogger(opts ...Option) *ZapLogger { options := Apply(opts...) - // 日志文件切割归档 - // writerSyncer := getLogWriter(opts...) - writerSyncer := getLogConsoleWriter(options) - // 编码器配置 - encoder := getEncoder(options.Encoding) + encoder := getEncoder(options) // 日志级别 level := getLogLevel(options.LogLevel) - core := zapcore.NewCore(encoder, writerSyncer, level) + core := zapcore.NewCore(encoder, options.Writer, level) if options.Mode != "prod" { return &ZapLogger{ zap.New(core, zap.Development(), zap.AddCaller(), zap.AddCallerSkip(1), zap.AddStacktrace(zap.ErrorLevel)), @@ -56,10 +50,12 @@ func NewLogger(opts ...Option) *ZapLogger { } } -func getEncoder(encoding string) zapcore.Encoder { - if encoding == "console" { +func getEncoder(options *Options) zapcore.Encoder { + var encoder zapcore.Encoder + switch f := options.Format; f { + case FormatText: // NewConsoleEncoder 打印更符合人们观察的方式 - return zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ + encoder = zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ TimeKey: "ts", LevelKey: "level", NameKey: "Logger", @@ -72,7 +68,7 @@ func getEncoder(encoding string) zapcore.Encoder { EncodeDuration: zapcore.SecondsDurationEncoder, EncodeCaller: zapcore.FullCallerEncoder, }) - } else { + case FormatJSON: // 创建一个默认的 encoder 配置 encoderConfig := zap.NewProductionEncoderConfig() // 自定义 MessageKey 为 message,message 语义更明确 @@ -89,8 +85,26 @@ func getEncoder(encoding string) zapcore.Encoder { encoderConfig.EncodeDuration = func(d time.Duration, enc zapcore.PrimitiveArrayEncoder) { enc.AppendFloat64(float64(d) / float64(time.Millisecond)) } - return zapcore.NewJSONEncoder(encoderConfig) + encoder = zapcore.NewJSONEncoder(encoderConfig) + default: + // NewConsoleEncoder 打印更符合人们观察的方式 + encoder = zapcore.NewConsoleEncoder(zapcore.EncoderConfig{ + TimeKey: "ts", + LevelKey: "level", + NameKey: "Logger", + CallerKey: "caller", + MessageKey: "msg", + StacktraceKey: "stacktrace", + LineEnding: zapcore.DefaultLineEnding, + EncodeLevel: zapcore.CapitalColorLevelEncoder, // 在日志文件中使用大写字母记录日志级别 + EncodeTime: zapcore.ISO8601TimeEncoder, + EncodeDuration: zapcore.SecondsDurationEncoder, + EncodeCaller: zapcore.FullCallerEncoder, + }) } + + return encoder + } // 自定义时间编码器 @@ -99,31 +113,6 @@ func timeEncoder(t time.Time, enc zapcore.PrimitiveArrayEncoder) { enc.AppendString(t.Format("2006-01-02 15:04:05.000000000")) } -func getLogWriter(opts *Options) zapcore.WriteSyncer { - lumberJackLogger := &lumberjack.Logger{ - Filename: opts.LogFilename, - MaxSize: opts.MaxSize, // megabytes - MaxBackups: opts.MaxBackups, - MaxAge: opts.MaxAge, //days - Compress: opts.Compress, - } - return zapcore.AddSync(lumberJackLogger) -} - -func getLogConsoleWriter(opts *Options) zapcore.WriteSyncer { - // 日志文件切割归档 - lumberJackLogger := &lumberjack.Logger{ - Filename: opts.LogFilename, - MaxSize: opts.MaxSize, // megabytes - MaxBackups: opts.MaxBackups, - MaxAge: opts.MaxAge, //days - Compress: opts.Compress, - } - - // 打印到控制台和文件 - return zapcore.NewMultiWriteSyncer(zapcore.AddSync(os.Stdout), zapcore.AddSync(lumberJackLogger)) -} - func getLogLevel(logLevel string) zapcore.Level { level := new(zapcore.Level) err := level.UnmarshalText([]byte(logLevel)) @@ -202,10 +191,6 @@ func (l *ZapLogger) Printf(format string, args ...any) { l.Logger.Sugar().Infof(format, args...) } -func (l *ZapLogger) Println(args ...any) { - l.Logger.Info(fmt.Sprintln(args...)) -} - func (l *ZapLogger) Close() error { return l.Logger.Sync() } diff --git a/log/zlog/log_test.go b/log/zlog/log_test.go index a3bfe55..1e26db3 100644 --- a/log/zlog/log_test.go +++ b/log/zlog/log_test.go @@ -1,13 +1,28 @@ package zlog import ( + "io" + "os" "testing" "go.uber.org/zap" + "gopkg.in/natefinch/lumberjack.v2" ) +func getLogWriter() io.Writer { + lumberJackLogger := &lumberjack.Logger{ + Filename: "logs.log", + MaxSize: 500, // megabytes + MaxBackups: 3, + MaxAge: 28, //days + Compress: true, + } + return lumberJackLogger +} + func TestLog(t *testing.T) { - logger := NewLogger(WithEncoding("json"), WithFilename("test.log")) + wr := getLogWriter() + logger := NewLogger(WithFormat(FormatJSON), WithWriters(os.Stdout, wr)) defer logger.Close() logger.Info("This is an info message", zap.String("route", "/hello"), zap.Int64("port", 8090)) diff --git a/log/zlog/logs.log b/log/zlog/logs.log new file mode 100644 index 0000000..17494bd --- /dev/null +++ b/log/zlog/logs.log @@ -0,0 +1,9 @@ +{"level":"info","timestamp":"2024-11-22 17:13:34.482","caller":"zlog/log_test.go:28","message":"This is an info message","route":"/hello","port":""} +{"level":"info","timestamp":"2024-11-22 17:13:34.482","caller":"zlog/log_test.go:29","message":"我是日志: {route 15 0 /hello }, {port 11 8090 }"} +{"level":"error","timestamp":"2024-11-22 17:13:34.482","caller":"zlog/log_test.go:30","message":"This is an error message","stacktrace":"github.com/apus-run/van/log/zlog.TestLog\n\t/Users/moocss/work/GoProjects/van/log/zlog/log_test.go:30\ntesting.tRunner\n\t/usr/local/go/src/testing/testing.go:1690"} +{"level":"info","timestamp":"2024-11-22 17:26:35.953","caller":"zlog/log_test.go:28","message":"This is an info message","route":"/hello","port":""} +{"level":"info","timestamp":"2024-11-22 17:26:35.953","caller":"zlog/log_test.go:29","message":"我是日志: {route 15 0 /hello }, {port 11 8090 }"} +{"level":"error","timestamp":"2024-11-22 17:26:35.953","caller":"zlog/log_test.go:30","message":"This is an error message","stacktrace":"github.com/apus-run/van/log/zlog.TestLog\n\t/Users/moocss/work/GoProjects/van/log/zlog/log_test.go:30\ntesting.tRunner\n\t/usr/local/go/src/testing/testing.go:1690"} +{"level":"info","timestamp":"2024-11-22 17:28:52.293","caller":"zlog/log_test.go:28","message":"This is an info message","route":"/hello","port":""} +{"level":"info","timestamp":"2024-11-22 17:28:52.293","caller":"zlog/log_test.go:29","message":"我是日志: {route 15 0 /hello }, {port 11 8090 }"} +{"level":"error","timestamp":"2024-11-22 17:28:52.293","caller":"zlog/log_test.go:30","message":"This is an error message","stacktrace":"github.com/apus-run/van/log/zlog.TestLog\n\t/Users/moocss/work/GoProjects/van/log/zlog/log_test.go:30\ntesting.tRunner\n\t/usr/local/go/src/testing/testing.go:1690"} diff --git a/log/zlog/options.go b/log/zlog/options.go index 01e5924..b7cb29b 100644 --- a/log/zlog/options.go +++ b/log/zlog/options.go @@ -1,34 +1,30 @@ package zlog +import ( + "io" + "os" + + "go.uber.org/zap/zapcore" +) + // Option is config option. type Option func(*Options) type Options struct { // logger options - Mode string // dev or prod - LogLevel string // debug, info, warn, error, panic, panic, fatal - Encoding string // console or json - - // lumberjack options - LogFilename string - MaxSize int - MaxBackups int - MaxAge int - Compress bool + Mode string // dev or prod + Writer zapcore.WriteSyncer // 日志输出 + LogLevel string // debug, info, warn, error, panic, panic, fatal + Format Format // text or json } // DefaultOptions . func DefaultOptions() *Options { return &Options{ Mode: "dev", + Writer: zapcore.AddSync(os.Stdout), LogLevel: "info", - Encoding: "console", - - LogFilename: "logs.log", - MaxSize: 500, // megabytes - MaxBackups: 3, - MaxAge: 28, //days - Compress: true, + Format: FormatText, } } @@ -54,45 +50,28 @@ func WithLogLevel(level string) Option { } } -// WithEncoding 日志编码 -func WithEncoding(encoding string) Option { - return func(o *Options) { - o.Encoding = encoding - } -} - -// WithFilename 日志文件路径,建议 /logs/log.log,如果为空则不输出日志到文件 -func WithFilename(filename string) Option { - return func(o *Options) { - o.LogFilename = filename - } -} - -// WithMaxSize 日志文件大小 -func WithMaxSize(maxSize int) Option { - return func(o *Options) { - o.MaxSize = maxSize - } -} - -// WithMaxBackups 日志文件最大备份数, 保留日志文件最大的数量,为 0 是保留所有旧的日志文件 -func WithMaxBackups(maxBackups int) Option { +// WithFormat 日志格式 +func WithFormat(format Format) Option { return func(o *Options) { - o.MaxBackups = maxBackups + o.Format = format } } -// WithMaxAge 日志文件最大保存时间 -func WithMaxAge(maxAge int) Option { +// WithWriter 日志输出 +func WithWriter(writer io.Writer) Option { return func(o *Options) { - o.MaxAge = maxAge + o.Writer = zapcore.AddSync(writer) } } -// WithCompress 日志文件是否压缩 -func WithCompress(compress bool) Option { +// WithWriters 日志输出 +func WithWriters(writers ...io.Writer) Option { return func(o *Options) { - o.Compress = compress + wrs := make([]zapcore.WriteSyncer, 0, len(writers)) + for _, w := range writers { + wrs = append(wrs, zapcore.AddSync(w)) + } + o.Writer = zapcore.NewMultiWriteSyncer(wrs...) } } diff --git a/log/zlog/types.go b/log/zlog/types.go index c8cadfc..57b0e20 100644 --- a/log/zlog/types.go +++ b/log/zlog/types.go @@ -27,15 +27,21 @@ type Logger interface { Print(args ...any) Printf(format string, args ...any) - Println(args ...any) With(fields ...Field) Logger AddCallerSkip(skip int) Logger Close() error - Sync() error + // Sync() error WithContext(ctx context.Context, keyvals ...any) context.Context } type Field = zapcore.Field + +type Format string + +const ( + FormatText Format = "text" + FormatJSON Format = "json" +) diff --git a/pkg/stringbuilder/stringbuilder.go b/pkg/stringbuilder/stringbuilder.go index b39c3d8..e9f2e3e 100644 --- a/pkg/stringbuilder/stringbuilder.go +++ b/pkg/stringbuilder/stringbuilder.go @@ -1 +1,74 @@ package stringbuilder + +import ( + "io" + "strings" +) + +type stringBuilder struct { + builder *strings.Builder +} + +var _ io.Writer = new(stringBuilder) + +func newStringBuilder() *stringBuilder { + return &stringBuilder{ + builder: &strings.Builder{}, + } +} + +// WriteLeadingString writes s to internal buffer. +// If it's not the first time to write the string, a blank (" ") will be written before s. +func (sb *stringBuilder) WriteLeadingString(s string) { + if sb.builder.Len() > 0 { + sb.builder.WriteString(" ") + } + + sb.builder.WriteString(s) +} + +func (sb *stringBuilder) WriteString(s string) { + sb.builder.WriteString(s) +} + +func (sb *stringBuilder) WriteStrings(ss []string, sep string) { + if len(ss) == 0 { + return + } + + firstAdded := false + if len(ss[0]) != 0 { + sb.WriteString(ss[0]) + firstAdded = true + } + + for _, s := range ss[1:] { + if len(s) != 0 { + if firstAdded { + sb.WriteString(sep) + } + sb.WriteString(s) + firstAdded = true + } + } +} + +func (sb *stringBuilder) WriteRune(r rune) { + sb.builder.WriteRune(r) +} + +func (sb *stringBuilder) Write(data []byte) (int, error) { + return sb.builder.Write(data) +} + +func (sb *stringBuilder) String() string { + return sb.builder.String() +} + +func (sb *stringBuilder) Reset() { + sb.builder.Reset() +} + +func (sb *stringBuilder) Grow(n int) { + sb.builder.Grow(n) +} diff --git a/validator/options.go b/validator/options.go new file mode 100644 index 0000000..b9122c1 --- /dev/null +++ b/validator/options.go @@ -0,0 +1,65 @@ +package validator + +import ( + "database/sql/driver" + "reflect" + + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" +) + +// Option 验证器选项 +type Option func(v *validator.Validate, trans ut.Translator) + +// WithTag 设置Tag名称,默认:valid +func WithTag(s string) Option { + return func(v *validator.Validate, trans ut.Translator) { + v.SetTagName(s) + } +} + +// WithValuerType 注册自定义验证类型 +func WithValuerType(types ...driver.Valuer) Option { + customTypes := make([]any, 0, len(types)) + for _, t := range types { + customTypes = append(customTypes, t) + } + + return func(validate *validator.Validate, trans ut.Translator) { + validate.RegisterCustomTypeFunc(func(field reflect.Value) any { + if valuer, ok := field.Interface().(driver.Valuer); ok { + v, _ := valuer.Value() + return v + } + + return nil + }, customTypes...) + } +} + +// WithValidation 注册自定义验证器 +func WithValidation(tag string, fn validator.Func, callValidationEvenIfNull ...bool) Option { + return func(validate *validator.Validate, trans ut.Translator) { + validate.RegisterValidation(tag, fn, callValidationEvenIfNull...) + } +} + +// WithValidationCtx 注册带Context的自定义验证器 +func WithValidationCtx(tag string, fn validator.FuncCtx, callValidationEvenIfNull ...bool) Option { + return func(validate *validator.Validate, trans ut.Translator) { + validate.RegisterValidationCtx(tag, fn, callValidationEvenIfNull...) + } +} + +// WithTranslation 注册自定义错误翻译 +// 参数 `text` 示例:{0}为必填字段 或 {0}必须大于{1} +func WithTranslation(tag, text string, override bool) Option { + return func(validate *validator.Validate, trans ut.Translator) { + validate.RegisterTranslation(tag, trans, func(ut ut.Translator) error { + return ut.Add(tag, text, override) + }, func(ut ut.Translator, fe validator.FieldError) string { + t, _ := ut.T(tag, fe.Field(), fe.Param()) + return t + }) + } +} diff --git a/validator/validator.go b/validator/validator.go new file mode 100644 index 0000000..5e368e0 --- /dev/null +++ b/validator/validator.go @@ -0,0 +1,118 @@ +package validator + +import ( + "context" + "errors" + "maps" + "reflect" + "slices" + "strings" + "sync" + + "github.com/go-playground/locales/zh" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + zhcn "github.com/go-playground/validator/v10/translations/zh" +) + +var ( + once sync.Once + vd *Validator +) + +type Validator struct { + validate *validator.Validate + translator ut.Translator +} + +func (v *Validator) Validate(s any) error { + return v.valid(s) +} + +func (v *Validator) ValidateContext(ctx context.Context, s any) error { + return v.validCtx(ctx, s) +} + +func (v *Validator) ValidatePartial(s any, partials map[string]struct{}) error { + return v.validate.StructPartial(s, slices.Collect(maps.Keys(partials))...) +} + +func (v *Validator) valid(obj any) error { + if reflect.Indirect(reflect.ValueOf(obj)).Kind() != reflect.Struct { + return nil + } + + e := v.validate.Struct(obj) + if e != nil { + err, ok := e.(validator.ValidationErrors) + if !ok { + return e + } + return removeStructName(err.Translate(v.translator)) + } + return nil +} + +func (v *Validator) validCtx(ctx context.Context, obj any) error { + if reflect.Indirect(reflect.ValueOf(obj)).Kind() != reflect.Struct { + return nil + } + + e := v.validate.StructCtx(ctx, obj) + if e != nil { + err, ok := e.(validator.ValidationErrors) + if !ok { + return e + } + return removeStructName(err.Translate(v.translator)) + } + return nil +} + +func removeStructName(fields map[string]string) error { + errs := make([]string, 0, len(fields)) + for _, err := range fields { + errs = append(errs, err) + } + return errors.New(strings.Join(errs, ";")) +} + +func NewValidator(opts ...Option) *Validator { + once.Do(func() { + validate := validator.New(validator.WithRequiredStructEnabled()) + + zhTrans := zh.New() + trans, _ := ut.New(zhTrans, zhTrans).GetTranslator("zh") + zhcn.RegisterDefaultTranslations(validate, trans) + + for _, f := range opts { + f(validate, trans) + } + + vd = &Validator{ + validate: validate, + translator: trans, + } + }) + + return vd +} + +// V 是 NewValidator 简写 +func V(opts ...Option) *Validator { + return NewValidator(opts...) +} + +// ValidateStruct 验证结构体 +func ValidateStruct(obj any) error { + return vd.Validate(obj) +} + +// ValidateStructCtx 验证结构体,带Context +func ValidateStructContext(ctx context.Context, obj any) error { + return vd.ValidateContext(ctx, obj) +} + +func ValidatePartial(obj any, partials map[string]struct{}) error { + return vd.ValidatePartial(obj, partials) +} diff --git a/validator/validator_test.go b/validator/validator_test.go new file mode 100644 index 0000000..02d6827 --- /dev/null +++ b/validator/validator_test.go @@ -0,0 +1,173 @@ +package validator + +import ( + "database/sql" + "reflect" + "strconv" + "strings" + "testing" + + "github.com/go-playground/locales/en" + "github.com/go-playground/locales/zh" + ut "github.com/go-playground/universal-translator" + "github.com/go-playground/validator/v10" + zhtrans "github.com/go-playground/validator/v10/translations/zh" + "github.com/stretchr/testify/assert" + "golang.org/x/text/language" +) + +func NullStringRequired(fl validator.FieldLevel) bool { + return len(fl.Field().String()) != 0 +} + +func NullIntGTE(fl validator.FieldLevel) bool { + i, err := strconv.ParseInt(fl.Param(), 0, 64) + if err != nil { + return false + } + + return fl.Field().Int() >= i +} + +type ParamsValidate struct { + ID sql.NullInt64 `valid:"nullint_gte=10"` + Desc sql.NullString `valid:"nullstring_required"` +} + +func TestValidator(t *testing.T) { + v := NewValidator( + WithTag("valid"), + WithValuerType(sql.NullString{}, sql.NullInt64{}), + WithValidation("nullint_gte", NullIntGTE), + WithTranslation("nullint_gte", "{0}必须大于或等于{1}", true), + WithValidation("nullstring_required", NullStringRequired), + WithTranslation("nullstring_required", "{0}为必填字段", true), + ) + + params1 := new(ParamsValidate) + params1.ID = sql.NullInt64{ + Int64: 9, + Valid: true, + } + err := v.Validate(params1) + assert.NotNil(t, err) + + params2 := &ParamsValidate{ + ID: sql.NullInt64{ + Int64: 13, + Valid: true, + }, + Desc: sql.NullString{ + String: "yiigo", + Valid: true, + }, + } + err = v.Validate(params2) + assert.Nil(t, err) +} + +type Student struct { + Name string `json:"name" validate:"required"` + Email string `json:"email" validate:"email"` + Age int `json:"age" validate:"max=30,min=12"` +} + +func TestValidateErr(t *testing.T) { + en := en.New() //英文翻译器 + zh := zh.New() //中文翻译器 + + // 第一个参数是必填,如果没有其他的语言设置,就用这第一个 + // 后面的参数是支持多语言环境( + // uni := ut.New(en, en) 也是可以的 + // uni := ut.New(en, zh, tw) + uni := ut.New(en, zh) + locale := language.Chinese.String() + trans, ok := uni.GetTranslator(locale) //获取需要的语言 + if !ok { + t.Errorf("uni.GetTranslator(%s) failed", locale) + return + } + student := Student{ + Name: "tom", + Email: "testemal", + Age: 40, + } + validate := validator.New() + validate.RegisterTagNameFunc(func(field reflect.StructField) string { + name := strings.SplitN(field.Tag.Get("json"), ",", 2)[0] + if name == "-" { + return "" + } + return name + }) + + zhtrans.RegisterDefaultTranslations(validate, trans) + //entrans.RegisterDefaultTranslations(validate, trans) + err := validate.Struct(student) + if err != nil { + // fmt.Println(err) + + errs := err.(validator.ValidationErrors) + t.Log(removeStructName(errs.Translate(trans))) + } +} + +//func removeStructName(fields safemap[string]string) safemap[string]string { +// result := safemap[string]string{} +// +// for field, err := range fields { +// result[field[strings.Index(field, ".")+1:]] = err +// } +// return result +//} + +func TestGetLanguage1(t *testing.T) { + accept := "zh-CN,zh;q=0.9" + tag, q, err := language.ParseAcceptLanguage(accept) + if err != nil { + t.Error(err) + return + } + t.Log(q) + t.Log(tag) + t.Log(language.Chinese.String()) + t.Log(language.SimplifiedChinese.String()) + t.Log(language.TraditionalChinese.String()) + for _, tag := range tag { + t.Log(tag.String()) + switch tag { + case language.Chinese: + t.Log("China") + case language.SimplifiedChinese: + t.Log("China Simplified") + case language.TraditionalChinese: + t.Log("traditional china") + default: + t.Log("Other") + } + } + +} + +func TestGetLanguage2(t *testing.T) { + accept := "zh-CN,zh;q=0.9" + lang := language.Make(accept) + var matcher = language.NewMatcher([]language.Tag{ + language.English, + language.Spanish, + language.Chinese, + }) + tag, idx := language.MatchStrings(matcher, lang.String()) + t.Log(tag.String()) + switch tag { + case language.Chinese: + t.Log("China") + case language.SimplifiedChinese: + t.Log("China Simplified") + case language.TraditionalChinese: + t.Log("traditional china") + default: + t.Log("Other") + } + t.Log(idx) +}