diff --git a/logger/logger.go b/logger/logger.go index 8df1523b..e2942648 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -48,8 +48,10 @@ type Logger interface { Fatal(ctx context.Context, msg string, attrs ...interface{}) // Log logs message with needed level Log(ctx context.Context, level Level, msg string, attrs ...interface{}) - // String returns the name of logger + // String returns the type name of logger String() string + // String returns the name of logger + Name() string } // Info writes formatted msg to default logger on info level diff --git a/logger/options.go b/logger/options.go index c0335316..02f586e2 100644 --- a/logger/options.go +++ b/logger/options.go @@ -35,6 +35,10 @@ type Options struct { MessageKey string // SourceKey is the key used for the source file and line of the log call SourceKey string + // StacktraceKey is the key used for the stacktrace + StacktraceKey string + // Stacktrace controls writing of stacktaces on error + Stacktrace bool } // NewOptions creates new options struct @@ -116,6 +120,9 @@ func WithZapKeys() options.Option { if err = options.Set(src, "caller", ".SourceKey"); err != nil { return err } + if err = options.Set(src, "stacktrace", ".StacktraceKey"); err != nil { + return err + } return nil } } @@ -135,6 +142,9 @@ func WithZerologKeys() options.Option { if err = options.Set(src, "caller", ".SourceKey"); err != nil { return err } + if err = options.Set(src, "stacktrace", ".StacktraceKey"); err != nil { + return err + } return nil } } @@ -154,6 +164,9 @@ func WithSlogKeys() options.Option { if err = options.Set(src, slog.SourceKey, ".SourceKey"); err != nil { return err } + if err = options.Set(src, "stacktrace", ".StacktraceKey"); err != nil { + return err + } return nil } } @@ -173,6 +186,16 @@ func WithMicroKeys() options.Option { if err = options.Set(src, "caller", ".SourceKey"); err != nil { return err } + if err = options.Set(src, "stacktrace", ".StacktraceKey"); err != nil { + return err + } return nil } } + +// WithStacktrace controls writing stacktrace on error +func WithStacktrace(v bool) options.Option { + return func(src interface{}) error { + return options.Set(src, v, ".Stacktrace") + } +} diff --git a/logger/slog/slog.go b/logger/slog/slog.go index cd9155c2..3a9c1731 100644 --- a/logger/slog/slog.go +++ b/logger/slog/slog.go @@ -4,8 +4,10 @@ import ( "context" "log/slog" "os" + "regexp" "runtime" "strconv" + "sync" "time" "go.unistack.org/micro/v4/logger" @@ -13,6 +15,8 @@ import ( "go.unistack.org/micro/v4/tracer" ) +var reTrace = regexp.MustCompile(`.*/slog/logger\.go.*\n`) + var ( traceValue = slog.StringValue("trace") debugValue = slog.StringValue("debug") @@ -61,13 +65,15 @@ type slogLogger struct { slog *slog.Logger leveler *slog.LevelVar opts logger.Options + mu sync.RWMutex } func (s *slogLogger) Clone(opts ...options.Option) logger.Logger { + s.mu.RLock() options := s.opts for _, o := range opts { - o(&options) + _ = o(&options) } l := &slogLogger{ @@ -91,6 +97,8 @@ func (s *slogLogger) Clone(opts ...options.Option) logger.Logger { handler := slog.NewJSONHandler(options.Out, handleOpt) l.slog = slog.New(handler).With(options.Attrs...) + s.mu.RUnlock() + return l } @@ -107,6 +115,7 @@ func (s *slogLogger) Options() logger.Options { } func (s *slogLogger) Attrs(attrs ...interface{}) logger.Logger { + s.mu.RLock() nl := &slogLogger{opts: s.opts} nl.leveler = new(slog.LevelVar) nl.leveler.Set(s.leveler.Level()) @@ -120,6 +129,8 @@ func (s *slogLogger) Attrs(attrs ...interface{}) logger.Logger { handler := slog.NewJSONHandler(s.opts.Out, handleOpt) nl.slog = slog.New(handler).With(attrs...) + s.mu.RUnlock() + return nl } @@ -219,6 +230,15 @@ func (s *slogLogger) Error(ctx context.Context, msg string, attrs ...interface{} for _, fn := range s.opts.ContextAttrFuncs { attrs = append(attrs, fn(ctx)...) } + if s.opts.Stacktrace { + stackInfo := make([]byte, 1024*1024) + if stackSize := runtime.Stack(stackInfo, false); stackSize > 0 { + traceLines := reTrace.Split(string(stackInfo[:stackSize]), -1) + if len(traceLines) != 0 { + attrs = append(attrs, slog.String("stacktrace", traceLines[len(traceLines)-1])) + } + } + } r.Add(attrs...) r.Attrs(func(a slog.Attr) bool { if a.Key == "error" { diff --git a/logger/slog/slog_test.go b/logger/slog/slog_test.go index 20e9d66f..3455a68c 100644 --- a/logger/slog/slog_test.go +++ b/logger/slog/slog_test.go @@ -9,6 +9,19 @@ import ( "go.unistack.org/micro/v4/logger" ) +func TestError(t *testing.T) { + ctx := context.TODO() + buf := bytes.NewBuffer(nil) + l := NewLogger(logger.WithLevel(logger.ErrorLevel), logger.WithOutput(buf), logger.WithStacktrace(true)) + if err := l.Init(); err != nil { + t.Fatal(err) + } + l.Error(ctx, "message") + if !bytes.Contains(buf.Bytes(), []byte(`"stacktrace":"`)) { + t.Fatalf("logger stacktrace not works, buf contains: %s", buf.Bytes()) + } +} + func TestContext(t *testing.T) { ctx := context.TODO() buf := bytes.NewBuffer(nil)