From 930859a537aa0e7b6f4fc0c56dfff2a389590511 Mon Sep 17 00:00:00 2001 From: Vasiliy Tolstov Date: Thu, 8 Feb 2024 08:17:53 +0300 Subject: [PATCH] logger/slog: initial import Signed-off-by: Vasiliy Tolstov --- config/options.go | 2 +- logger/default.go | 230 --------------------- logger/noop.go | 90 +++++++++ logger/slog/options.go | 27 +++ logger/slog/slog.go | 419 +++++++++++++++++++++++++++++++++++++++ logger/slog/slog_test.go | 141 +++++++++++++ 6 files changed, 678 insertions(+), 231 deletions(-) delete mode 100644 logger/default.go create mode 100644 logger/noop.go create mode 100644 logger/slog/options.go create mode 100644 logger/slog/slog.go create mode 100644 logger/slog/slog_test.go diff --git a/config/options.go b/config/options.go index 67074bdc..38a3fbec 100644 --- a/config/options.go +++ b/config/options.go @@ -72,9 +72,9 @@ type LoadOption func(o *LoadOptions) // LoadOptions struct type LoadOptions struct { Struct interface{} + Context context.Context Override bool Append bool - Context context.Context } // NewLoadOptions create LoadOptions struct with provided opts diff --git a/logger/default.go b/logger/default.go deleted file mode 100644 index 45be6c64..00000000 --- a/logger/default.go +++ /dev/null @@ -1,230 +0,0 @@ -package logger - -import ( - "context" - "encoding/json" - "fmt" - "os" - "runtime" - "strings" - "sync" - "time" -) - -type defaultLogger struct { - enc *json.Encoder - opts Options - sync.RWMutex -} - -// Init(opts...) should only overwrite provided options -func (l *defaultLogger) Init(opts ...Option) error { - l.Lock() - for _, o := range opts { - o(&l.opts) - } - l.enc = json.NewEncoder(l.opts.Out) - // wrap the Log func - l.Unlock() - return nil -} - -func (l *defaultLogger) String() string { - return "micro" -} - -func (l *defaultLogger) Clone(opts ...Option) Logger { - newopts := NewOptions(opts...) - oldopts := l.opts - for _, o := range opts { - o(&newopts) - o(&oldopts) - } - - l.Lock() - cl := &defaultLogger{opts: oldopts, enc: json.NewEncoder(l.opts.Out)} - l.Unlock() - - return cl -} - -func (l *defaultLogger) V(level Level) bool { - l.RLock() - ok := l.opts.Level.Enabled(level) - l.RUnlock() - return ok -} - -func (l *defaultLogger) Level(level Level) { - l.Lock() - l.opts.Level = level - l.Unlock() -} - -func (l *defaultLogger) Fields(fields ...interface{}) Logger { - l.RLock() - nl := &defaultLogger{opts: l.opts, enc: l.enc} - if len(fields) == 0 { - l.RUnlock() - return nl - } else if len(fields)%2 != 0 { - fields = fields[:len(fields)-1] - } - nl.opts.Fields = copyFields(l.opts.Fields) - nl.opts.Fields = append(nl.opts.Fields, fields...) - l.RUnlock() - return nl -} - -func copyFields(src []interface{}) []interface{} { - dst := make([]interface{}, len(src)) - copy(dst, src) - return dst -} - -// logCallerfilePath returns a package/file:line description of the caller, -// preserving only the leaf directory name and file name. -func logCallerfilePath(loggingFilePath string) string { - // To make sure we trim the path correctly on Windows too, we - // counter-intuitively need to use '/' and *not* os.PathSeparator here, - // because the path given originates from Go stdlib, specifically - // runtime.Caller() which (as of Mar/17) returns forward slashes even on - // Windows. - // - // See https://github.com/golang/go/issues/3335 - // and https://github.com/golang/go/issues/18151 - // - // for discussion on the issue on Go side. - idx := strings.LastIndexByte(loggingFilePath, '/') - if idx == -1 { - return loggingFilePath - } - idx = strings.LastIndexByte(loggingFilePath[:idx], '/') - if idx == -1 { - return loggingFilePath - } - return loggingFilePath[idx+1:] -} - -func (l *defaultLogger) Info(ctx context.Context, args ...interface{}) { - l.Log(ctx, InfoLevel, args...) -} - -func (l *defaultLogger) Error(ctx context.Context, args ...interface{}) { - l.Log(ctx, ErrorLevel, args...) -} - -func (l *defaultLogger) Debug(ctx context.Context, args ...interface{}) { - l.Log(ctx, DebugLevel, args...) -} - -func (l *defaultLogger) Warn(ctx context.Context, args ...interface{}) { - l.Log(ctx, WarnLevel, args...) -} - -func (l *defaultLogger) Trace(ctx context.Context, args ...interface{}) { - l.Log(ctx, TraceLevel, args...) -} - -func (l *defaultLogger) Fatal(ctx context.Context, args ...interface{}) { - l.Log(ctx, FatalLevel, args...) - os.Exit(1) -} - -func (l *defaultLogger) Infof(ctx context.Context, msg string, args ...interface{}) { - l.Logf(ctx, InfoLevel, msg, args...) -} - -func (l *defaultLogger) Errorf(ctx context.Context, msg string, args ...interface{}) { - l.Logf(ctx, ErrorLevel, msg, args...) -} - -func (l *defaultLogger) Debugf(ctx context.Context, msg string, args ...interface{}) { - l.Logf(ctx, DebugLevel, msg, args...) -} - -func (l *defaultLogger) Warnf(ctx context.Context, msg string, args ...interface{}) { - l.Logf(ctx, WarnLevel, msg, args...) -} - -func (l *defaultLogger) Tracef(ctx context.Context, msg string, args ...interface{}) { - l.Logf(ctx, TraceLevel, msg, args...) -} - -func (l *defaultLogger) Fatalf(ctx context.Context, msg string, args ...interface{}) { - l.Logf(ctx, FatalLevel, msg, args...) - os.Exit(1) -} - -func (l *defaultLogger) Log(ctx context.Context, level Level, args ...interface{}) { - if !l.V(level) { - return - } - - l.RLock() - fields := copyFields(l.opts.Fields) - l.RUnlock() - - fields = append(fields, "level", level.String()) - - if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok { - fields = append(fields, "caller", fmt.Sprintf("%s:%d", logCallerfilePath(file), line)) - } - fields = append(fields, "timestamp", time.Now().Format("2006-01-02 15:04:05")) - - if len(args) > 0 { - fields = append(fields, "msg", fmt.Sprint(args...)) - } - - out := make(map[string]interface{}, len(fields)/2) - for i := 0; i < len(fields); i += 2 { - out[fields[i].(string)] = fields[i+1] - } - l.RLock() - _ = l.enc.Encode(out) - l.RUnlock() -} - -func (l *defaultLogger) Logf(ctx context.Context, level Level, msg string, args ...interface{}) { - if !l.V(level) { - return - } - - l.RLock() - fields := copyFields(l.opts.Fields) - l.RUnlock() - - fields = append(fields, "level", level.String()) - - if _, file, line, ok := runtime.Caller(l.opts.CallerSkipCount); ok { - fields = append(fields, "caller", fmt.Sprintf("%s:%d", logCallerfilePath(file), line)) - } - - fields = append(fields, "timestamp", time.Now().Format("2006-01-02 15:04:05")) - if len(args) > 0 { - fields = append(fields, "msg", fmt.Sprintf(msg, args...)) - } else if msg != "" { - fields = append(fields, "msg", msg) - } - - out := make(map[string]interface{}, len(fields)/2) - for i := 0; i < len(fields); i += 2 { - out[fields[i].(string)] = fields[i+1] - } - l.RLock() - _ = l.enc.Encode(out) - l.RUnlock() -} - -func (l *defaultLogger) Options() Options { - return l.opts -} - -// NewLogger builds a new logger based on options -func NewLogger(opts ...Option) Logger { - l := &defaultLogger{ - opts: NewOptions(opts...), - } - l.enc = json.NewEncoder(l.opts.Out) - return l -} diff --git a/logger/noop.go b/logger/noop.go new file mode 100644 index 00000000..04f0515b --- /dev/null +++ b/logger/noop.go @@ -0,0 +1,90 @@ +package logger + +import ( + "context" +) + +type noopLogger struct { + opts Options +} + +func NewLogger(opts ...Option) Logger { + options := NewOptions(opts...) + return &noopLogger{opts: options} +} + +func (l *noopLogger) V(lvl Level) bool { + return false +} + +func (l *noopLogger) Level(lvl Level) { +} + +func (l *noopLogger) Init(opts ...Option) error { + for _, o := range opts { + o(&l.opts) + } + return nil +} + +func (l *noopLogger) Clone(opts ...Option) Logger { + nl := &noopLogger{opts: l.opts} + for _, o := range opts { + o(&nl.opts) + } + return nl +} + +func (l *noopLogger) Fields(attrs ...interface{}) Logger { + return l +} + +func (l *noopLogger) Options() Options { + return l.opts +} + +func (l *noopLogger) String() string { + return "noop" +} + +func (l *noopLogger) Log(ctx context.Context, lvl Level, attrs ...interface{}) { +} + +func (l *noopLogger) Info(ctx context.Context, attrs ...interface{}) { +} + +func (l *noopLogger) Debug(ctx context.Context, attrs ...interface{}) { +} + +func (l *noopLogger) Error(ctx context.Context, attrs ...interface{}) { +} + +func (l *noopLogger) Trace(ctx context.Context, attrs ...interface{}) { +} + +func (l *noopLogger) Warn(ctx context.Context, attrs ...interface{}) { +} + +func (l *noopLogger) Fatal(ctx context.Context, attrs ...interface{}) { +} + +func (l *noopLogger) Logf(ctx context.Context, lvl Level, msg string, attrs ...interface{}) { +} + +func (l *noopLogger) Infof(ctx context.Context, msg string, attrs ...interface{}) { +} + +func (l *noopLogger) Debugf(ctx context.Context, msg string, attrs ...interface{}) { +} + +func (l *noopLogger) Errorf(ctx context.Context, msg string, attrs ...interface{}) { +} + +func (l *noopLogger) Tracef(ctx context.Context, msg string, attrs ...interface{}) { +} + +func (l *noopLogger) Warnf(ctx context.Context, msg string, attrs ...interface{}) { +} + +func (l *noopLogger) Fatalf(ctx context.Context, msg string, attrs ...interface{}) { +} diff --git a/logger/slog/options.go b/logger/slog/options.go new file mode 100644 index 00000000..58e277bc --- /dev/null +++ b/logger/slog/options.go @@ -0,0 +1,27 @@ +package slog + +import "go.unistack.org/micro/v3/logger" + +type sourceKey struct{} + +func WithSourceKey(v string) logger.Option { + return logger.SetOption(sourceKey{}, v) +} + +type timeKey struct{} + +func WithTimeKey(v string) logger.Option { + return logger.SetOption(timeKey{}, v) +} + +type messageKey struct{} + +func WithMessageKey(v string) logger.Option { + return logger.SetOption(messageKey{}, v) +} + +type levelKey struct{} + +func WithLevelKey(v string) logger.Option { + return logger.SetOption(levelKey{}, v) +} diff --git a/logger/slog/slog.go b/logger/slog/slog.go new file mode 100644 index 00000000..7627e88f --- /dev/null +++ b/logger/slog/slog.go @@ -0,0 +1,419 @@ +package slog + +import ( + "context" + "fmt" + "log/slog" + "os" + "runtime" + "strconv" + "time" + + "go.unistack.org/micro/v3/logger" + "go.unistack.org/micro/v3/tracer" +) + +var ( + DefaultSourceKey string = slog.SourceKey + DefaultTimeKey string = slog.TimeKey + DefaultMessageKey string = slog.MessageKey + DefaultLevelKey string = slog.LevelKey +) + +var ( + traceValue = slog.StringValue("trace") + debugValue = slog.StringValue("debug") + infoValue = slog.StringValue("info") + warnValue = slog.StringValue("warn") + errorValue = slog.StringValue("error") + fatalValue = slog.StringValue("fatal") +) + +func (s *slogLogger) renameAttr(_ []string, a slog.Attr) slog.Attr { + switch a.Key { + case slog.SourceKey: + source := a.Value.Any().(*slog.Source) + a.Value = slog.StringValue(source.File + ":" + strconv.Itoa(source.Line)) + a.Key = s.sourceKey + case slog.TimeKey: + a.Key = s.timeKey + case slog.MessageKey: + a.Key = s.messageKey + case slog.LevelKey: + level := a.Value.Any().(slog.Level) + lvl := slogToLoggerLevel(level) + a.Key = s.levelKey + switch { + case lvl < logger.DebugLevel: + a.Value = traceValue + case lvl < logger.InfoLevel: + a.Value = debugValue + case lvl < logger.WarnLevel: + a.Value = infoValue + case lvl < logger.ErrorLevel: + a.Value = warnValue + case lvl < logger.FatalLevel: + a.Value = errorValue + case lvl >= logger.FatalLevel: + a.Value = fatalValue + default: + a.Value = infoValue + } + } + + return a +} + +type slogLogger struct { + slog *slog.Logger + leveler *slog.LevelVar + levelKey string + messageKey string + sourceKey string + timeKey string + opts logger.Options +} + +func (s *slogLogger) Clone(opts ...logger.Option) logger.Logger { + options := s.opts + + for _, o := range opts { + o(&options) + } + + l := &slogLogger{ + opts: options, + levelKey: s.levelKey, + messageKey: s.messageKey, + sourceKey: s.sourceKey, + timeKey: s.timeKey, + } + + if v, ok := l.opts.Context.Value(levelKey{}).(string); ok && v != "" { + l.levelKey = v + } + if v, ok := l.opts.Context.Value(messageKey{}).(string); ok && v != "" { + l.messageKey = v + } + if v, ok := l.opts.Context.Value(sourceKey{}).(string); ok && v != "" { + l.sourceKey = v + } + if v, ok := l.opts.Context.Value(timeKey{}).(string); ok && v != "" { + l.timeKey = v + } + + l.leveler = new(slog.LevelVar) + handleOpt := &slog.HandlerOptions{ + ReplaceAttr: s.renameAttr, + Level: l.leveler, + AddSource: true, + } + l.leveler.Set(loggerToSlogLevel(l.opts.Level)) + handler := slog.NewJSONHandler(options.Out, handleOpt) + l.slog = slog.New(handler).With(options.Fields...) + + return l +} + +func (s *slogLogger) V(level logger.Level) bool { + return s.opts.Level.Enabled(level) +} + +func (s *slogLogger) Level(level logger.Level) { + s.leveler.Set(loggerToSlogLevel(level)) +} + +func (s *slogLogger) Options() logger.Options { + return s.opts +} + +func (s *slogLogger) Fields(attrs ...interface{}) logger.Logger { + nl := &slogLogger{ + opts: s.opts, + levelKey: s.levelKey, + messageKey: s.messageKey, + sourceKey: s.sourceKey, + timeKey: s.timeKey, + } + nl.leveler = new(slog.LevelVar) + nl.leveler.Set(s.leveler.Level()) + + handleOpt := &slog.HandlerOptions{ + ReplaceAttr: nl.renameAttr, + Level: s.leveler, + AddSource: true, + } + + handler := slog.NewJSONHandler(s.opts.Out, handleOpt) + nl.slog = slog.New(handler).With(attrs...) + + return nl +} + +func (s *slogLogger) Init(opts ...logger.Option) error { + for _, o := range opts { + o(&s.opts) + } + + if v, ok := s.opts.Context.Value(levelKey{}).(string); ok && v != "" { + s.levelKey = v + } + if v, ok := s.opts.Context.Value(messageKey{}).(string); ok && v != "" { + s.messageKey = v + } + if v, ok := s.opts.Context.Value(sourceKey{}).(string); ok && v != "" { + s.sourceKey = v + } + if v, ok := s.opts.Context.Value(timeKey{}).(string); ok && v != "" { + s.timeKey = v + } + + s.leveler = new(slog.LevelVar) + handleOpt := &slog.HandlerOptions{ + ReplaceAttr: s.renameAttr, + Level: s.leveler, + AddSource: true, + } + s.leveler.Set(loggerToSlogLevel(s.opts.Level)) + handler := slog.NewJSONHandler(s.opts.Out, handleOpt) + s.slog = slog.New(handler).With(s.opts.Fields...) + + slog.SetDefault(s.slog) + + return nil +} + +func (s *slogLogger) Log(ctx context.Context, lvl logger.Level, attrs ...interface{}) { + if !s.V(lvl) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), loggerToSlogLevel(lvl), fmt.Sprintf("%s", attrs[0]), pcs[0]) + r.Add(attrs[1:]...) + _ = s.slog.Handler().Handle(ctx, r) +} + +func (s *slogLogger) Logf(ctx context.Context, lvl logger.Level, msg string, attrs ...interface{}) { + if !s.V(lvl) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), loggerToSlogLevel(lvl), msg, pcs[0]) + r.Add(attrs...) + _ = s.slog.Handler().Handle(ctx, r) +} + +func (s *slogLogger) Info(ctx context.Context, attrs ...interface{}) { + if !s.V(logger.InfoLevel) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), slog.LevelInfo, fmt.Sprintf("%s", attrs[0]), pcs[0]) + r.Add(attrs[1:]...) + _ = s.slog.Handler().Handle(ctx, r) +} + +func (s *slogLogger) Infof(ctx context.Context, msg string, attrs ...interface{}) { + if !s.V(logger.InfoLevel) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), slog.LevelInfo, msg, pcs[0]) + r.Add(attrs...) + _ = s.slog.Handler().Handle(ctx, r) +} + +func (s *slogLogger) Debug(ctx context.Context, attrs ...interface{}) { + if !s.V(logger.DebugLevel) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), slog.LevelDebug, fmt.Sprintf("%s", attrs[0]), pcs[0]) + r.Add(attrs[1:]...) + _ = s.slog.Handler().Handle(ctx, r) +} + +func (s *slogLogger) Debugf(ctx context.Context, msg string, attrs ...interface{}) { + if !s.V(logger.DebugLevel) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), slog.LevelDebug, msg, pcs[0]) + r.Add(attrs...) + _ = s.slog.Handler().Handle(ctx, r) +} + +func (s *slogLogger) Trace(ctx context.Context, attrs ...interface{}) { + if !s.V(logger.TraceLevel) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), slog.LevelDebug-1, fmt.Sprintf("%s", attrs[0]), pcs[0]) + r.Add(attrs[1:]...) + _ = s.slog.Handler().Handle(ctx, r) +} + +func (s *slogLogger) Tracef(ctx context.Context, msg string, attrs ...interface{}) { + if !s.V(logger.TraceLevel) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), slog.LevelDebug-1, msg, pcs[0]) + r.Add(attrs...) + _ = s.slog.Handler().Handle(ctx, r) +} + +func (s *slogLogger) Error(ctx context.Context, attrs ...interface{}) { + if !s.V(logger.ErrorLevel) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), slog.LevelError, fmt.Sprintf("%s", attrs[0]), pcs[0]) + r.Add(attrs[1:]...) + r.Attrs(func(a slog.Attr) bool { + if a.Key == "error" { + if span, ok := tracer.SpanFromContext(ctx); ok { + span.SetStatus(tracer.SpanStatusError, a.Value.String()) + return false + } + } + return true + }) + _ = s.slog.Handler().Handle(ctx, r) +} + +func (s *slogLogger) Errorf(ctx context.Context, msg string, attrs ...interface{}) { + if !s.V(logger.ErrorLevel) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), slog.LevelError, msg, pcs[0]) + r.Add(attrs...) + r.Attrs(func(a slog.Attr) bool { + if a.Key == "error" { + if span, ok := tracer.SpanFromContext(ctx); ok { + span.SetStatus(tracer.SpanStatusError, a.Value.String()) + return false + } + } + return true + }) + _ = s.slog.Handler().Handle(ctx, r) +} + +func (s *slogLogger) Fatal(ctx context.Context, attrs ...interface{}) { + if !s.V(logger.FatalLevel) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), slog.LevelError+1, fmt.Sprintf("%s", attrs[0]), pcs[0]) + r.Add(attrs[1:]...) + _ = s.slog.Handler().Handle(ctx, r) + os.Exit(1) +} + +func (s *slogLogger) Fatalf(ctx context.Context, msg string, attrs ...interface{}) { + if !s.V(logger.FatalLevel) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), slog.LevelError+1, msg, pcs[0]) + r.Add(attrs...) + _ = s.slog.Handler().Handle(ctx, r) + os.Exit(1) +} + +func (s *slogLogger) Warn(ctx context.Context, attrs ...interface{}) { + if !s.V(logger.WarnLevel) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), slog.LevelWarn, fmt.Sprintf("%s", attrs[0]), pcs[0]) + r.Add(attrs[1:]...) + _ = s.slog.Handler().Handle(ctx, r) +} + +func (s *slogLogger) Warnf(ctx context.Context, msg string, attrs ...interface{}) { + if !s.V(logger.WarnLevel) { + return + } + var pcs [1]uintptr + runtime.Callers(s.opts.CallerSkipCount, pcs[:]) // skip [Callers, Infof] + r := slog.NewRecord(time.Now(), slog.LevelWarn, msg, pcs[0]) + r.Add(attrs...) + _ = s.slog.Handler().Handle(ctx, r) +} + +func (s *slogLogger) String() string { + return "slog" +} + +func NewLogger(opts ...logger.Option) logger.Logger { + s := &slogLogger{ + opts: logger.NewOptions(opts...), + sourceKey: DefaultSourceKey, + timeKey: DefaultTimeKey, + messageKey: DefaultMessageKey, + levelKey: DefaultLevelKey, + } + if v, ok := s.opts.Context.Value(levelKey{}).(string); ok && v != "" { + s.levelKey = v + } + if v, ok := s.opts.Context.Value(messageKey{}).(string); ok && v != "" { + s.messageKey = v + } + if v, ok := s.opts.Context.Value(sourceKey{}).(string); ok && v != "" { + s.sourceKey = v + } + if v, ok := s.opts.Context.Value(timeKey{}).(string); ok && v != "" { + s.timeKey = v + } + return s +} + +func loggerToSlogLevel(level logger.Level) slog.Level { + switch level { + case logger.DebugLevel: + return slog.LevelDebug + case logger.WarnLevel: + return slog.LevelWarn + case logger.ErrorLevel: + return slog.LevelError + case logger.TraceLevel: + return slog.LevelDebug - 1 + case logger.FatalLevel: + return slog.LevelError + 1 + default: + return slog.LevelInfo + } +} + +func slogToLoggerLevel(level slog.Level) logger.Level { + switch level { + case slog.LevelDebug: + return logger.DebugLevel + case slog.LevelWarn: + return logger.WarnLevel + case slog.LevelError: + return logger.ErrorLevel + case slog.LevelDebug - 1: + return logger.TraceLevel + case slog.LevelError + 1: + return logger.FatalLevel + default: + return logger.InfoLevel + } +} diff --git a/logger/slog/slog_test.go b/logger/slog/slog_test.go new file mode 100644 index 00000000..a6846c6f --- /dev/null +++ b/logger/slog/slog_test.go @@ -0,0 +1,141 @@ +package slog + +import ( + "bytes" + "context" + "log" + "testing" + + "go.unistack.org/micro/v3/logger" +) + +func TestContext(t *testing.T) { + ctx := context.TODO() + buf := bytes.NewBuffer(nil) + l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf)) + if err := l.Init(); err != nil { + t.Fatal(err) + } + + nl, ok := logger.FromContext(logger.NewContext(ctx, l.Fields("key", "val"))) + if !ok { + t.Fatal("context without logger") + } + nl.Info(ctx, "message") + if !bytes.Contains(buf.Bytes(), []byte(`"key":"val"`)) { + t.Fatalf("logger fields not works, buf contains: %s", buf.Bytes()) + } +} + +func TestFields(t *testing.T) { + ctx := context.TODO() + buf := bytes.NewBuffer(nil) + l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf)) + if err := l.Init(); err != nil { + t.Fatal(err) + } + + nl := l.Fields("key", "val") + + nl.Info(ctx, "message") + if !bytes.Contains(buf.Bytes(), []byte(`"key":"val"`)) { + t.Fatalf("logger fields not works, buf contains: %s", buf.Bytes()) + } +} + +func TestFromContextWithFields(t *testing.T) { + ctx := context.TODO() + buf := bytes.NewBuffer(nil) + var ok bool + l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf)) + if err := l.Init(); err != nil { + t.Fatal(err) + } + nl := l.Fields("key", "val") + + ctx = logger.NewContext(ctx, nl) + + l, ok = logger.FromContext(ctx) + if !ok { + t.Fatalf("context does not have logger") + } + + l.Info(ctx, "message") + if !bytes.Contains(buf.Bytes(), []byte(`"key":"val"`)) { + t.Fatalf("logger fields not works, buf contains: %s", buf.Bytes()) + } +} + +func TestClone(t *testing.T) { + ctx := context.TODO() + buf := bytes.NewBuffer(nil) + l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf)) + if err := l.Init(); err != nil { + t.Fatal(err) + } + nl := l.Clone(logger.WithLevel(logger.ErrorLevel)) + if err := nl.Init(); err != nil { + t.Fatal(err) + } + nl.Info(ctx, "info message") + if len(buf.Bytes()) != 0 { + t.Fatal("message must not be logged") + } + l.Info(ctx, "info message") + if len(buf.Bytes()) == 0 { + t.Fatal("message must be logged") + } +} + +func TestRedirectStdLogger(t *testing.T) { + buf := bytes.NewBuffer(nil) + l := NewLogger(logger.WithLevel(logger.ErrorLevel), logger.WithOutput(buf)) + if err := l.Init(); err != nil { + t.Fatal(err) + } + fn := logger.RedirectStdLogger(l, logger.ErrorLevel) + defer fn() + log.Print("test") + if !(bytes.Contains(buf.Bytes(), []byte(`"level":"error"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"test"`))) { + t.Fatalf("logger error, buf %s", buf.Bytes()) + } +} + +func TestStdLogger(t *testing.T) { + buf := bytes.NewBuffer(nil) + l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf)) + if err := l.Init(); err != nil { + t.Fatal(err) + } + lg := logger.NewStdLogger(l, logger.ErrorLevel) + lg.Print("test") + if !(bytes.Contains(buf.Bytes(), []byte(`"level":"error"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"test"`))) { + t.Fatalf("logger error, buf %s", buf.Bytes()) + } +} + +func TestLogger(t *testing.T) { + ctx := context.TODO() + buf := bytes.NewBuffer(nil) + l := NewLogger(logger.WithLevel(logger.TraceLevel), logger.WithOutput(buf)) + if err := l.Init(); err != nil { + t.Fatal(err) + } + l.Trace(ctx, "trace_msg1") + l.Warn(ctx, "warn_msg1") + l.Fields("error", "test").Info(ctx, "error message") + l.Warn(ctx, "first second") + + if !(bytes.Contains(buf.Bytes(), []byte(`"level":"trace"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"trace_msg1"`))) { + t.Fatalf("logger tracer, buf %s", buf.Bytes()) + } + if !(bytes.Contains(buf.Bytes(), []byte(`"level":"warn"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"warn_msg1"`))) { + t.Fatalf("logger warn, buf %s", buf.Bytes()) + } + if !(bytes.Contains(buf.Bytes(), []byte(`"level":"info"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"error message","error":"test"`))) { + t.Fatalf("logger info, buf %s", buf.Bytes()) + } + if !(bytes.Contains(buf.Bytes(), []byte(`"level":"warn"`)) && bytes.Contains(buf.Bytes(), []byte(`"msg":"first second"`))) { + t.Fatalf("logger warn, buf %s", buf.Bytes()) + } +}