diff --git a/logging/slog/handler.go b/logging/slog/handler.go new file mode 100644 index 0000000..a1b8754 --- /dev/null +++ b/logging/slog/handler.go @@ -0,0 +1,76 @@ +package slog + +import ( + "context" + "errors" + "io" + "log/slog" + + "go.opentelemetry.io/otel/codes" + "go.opentelemetry.io/otel/trace" +) + +const ( + traceIDKey = "trace_id" + spanIDKey = "span_id" + traceFlagsKey = "trace_flags" +) + +type traceConfig struct { + recordStackTraceInSpan bool + errorSpanLevel slog.Level +} + +type traceHandler struct { + slog.Handler + tcfg *traceConfig +} + +func NewTraceHandler(w io.Writer, opts *slog.HandlerOptions, traceConfig *traceConfig) *traceHandler { + if opts == nil { + opts = &slog.HandlerOptions{} + } + return &traceHandler{ + slog.NewJSONHandler(w, opts), + traceConfig, + } +} + +func (t *traceHandler) Enabled(ctx context.Context, level slog.Level) bool { + return t.Handler.Enabled(ctx, level) +} + +func (t *traceHandler) Handle(ctx context.Context, record slog.Record) error { + // trace span add + span := trace.SpanFromContext(ctx) + if span.SpanContext().TraceID().IsValid() { + record.Add(traceIDKey, span.SpanContext().TraceID()) + } + if span.SpanContext().SpanID().IsValid() { + record.Add(spanIDKey, span.SpanContext().SpanID()) + } + if span.SpanContext().TraceFlags().IsSampled() { + record.Add(traceFlagsKey, span.SpanContext().TraceFlags()) + } + + // non recording spans do not support modifying + if !span.IsRecording() { + return t.Handler.Handle(ctx, record) + } + + // set span status + if record.Level >= t.tcfg.errorSpanLevel { + span.SetStatus(codes.Error, "") + span.RecordError(errors.New(record.Message), trace.WithStackTrace(t.tcfg.recordStackTraceInSpan)) + } + + return t.Handler.Handle(ctx, record) +} + +func (t *traceHandler) WithAttrs(attrs []slog.Attr) slog.Handler { + return t.Handler.WithAttrs(attrs) +} + +func (t *traceHandler) WithGroup(name string) slog.Handler { + return t.Handler.WithGroup(name) +} diff --git a/logging/slog/logger.go b/logging/slog/logger.go index c797bba..d5d4f86 100644 --- a/logging/slog/logger.go +++ b/logging/slog/logger.go @@ -1 +1,32 @@ package slog + +import ( + "fmt" + "log/slog" +) + +type Writer struct { + log *slog.Logger + config *config +} + +func NewWriter(opts ...Option) *Writer { + cfg := defaultConfig() + + // apply options + for _, opt := range opts { + opt.apply(cfg) + } + + logger := slog.New(NewTraceHandler(cfg.coreConfig.writer, cfg.coreConfig.opt, cfg.traceConfig)) + + return &Writer{log: logger, config: cfg} +} + +func (w *Writer) Logger() *slog.Logger { + return w.log +} + +func (w *Writer) Printf(format string, v ...interface{}) { + w.log.Info(fmt.Sprintf(format, v...)) +} diff --git a/logging/slog/logger_test.go b/logging/slog/logger_test.go index c797bba..0d4c22e 100644 --- a/logging/slog/logger_test.go +++ b/logging/slog/logger_test.go @@ -1 +1,75 @@ package slog + +import ( + "context" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/stdout/stdouttrace" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "gorm.io/driver/sqlite" + "gorm.io/gorm" + "gorm.io/gorm/logger" + "testing" + "time" +) + +func stdoutProvider(ctx context.Context) func() { + provider := sdktrace.NewTracerProvider() + otel.SetTracerProvider(provider) + + exp, err := stdouttrace.New(stdouttrace.WithPrettyPrint()) + if err != nil { + panic(err) + } + + bsp := sdktrace.NewBatchSpanProcessor(exp) + provider.RegisterSpanProcessor(bsp) + + return func() { + if err := provider.Shutdown(ctx); err != nil { + panic(err) + } + } +} + +func TestLogger(t *testing.T) { + ctx := context.Background() + shutdown := stdoutProvider(ctx) + defer shutdown() + + logger := logger.New( + NewWriter(), + logger.Config{ + SlowThreshold: time.Millisecond, + LogLevel: logger.Warn, + Colorful: false, + }, + ) + db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{Logger: logger}) + if err != nil { + panic(err) + } + + db.Logger.Info(ctx, "log from origin logrus") + + tracer := otel.Tracer("test otel std logger") + + ctx, span := tracer.Start(ctx, "root") + + db.Logger.Info(ctx, "hello %s", "world") + + span.End() + + ctx, child := tracer.Start(ctx, "child") + + db.Logger.Warn(ctx, "foo %s", "bar") + + child.End() + + ctx, errSpan := tracer.Start(ctx, "error") + + db.Logger.Error(ctx, "error %s", "this is a error") + + db.Logger.Info(ctx, "no trace context") + + errSpan.End() +} diff --git a/logging/slog/option.go b/logging/slog/option.go index c797bba..f5da355 100644 --- a/logging/slog/option.go +++ b/logging/slog/option.go @@ -1 +1,92 @@ package slog + +import ( + "io" + "log/slog" + "os" +) + +type Option interface { + apply(cfg *config) +} + +type option func(cfg *config) + +func (fn option) apply(cfg *config) { + fn(cfg) +} + +type coreConfig struct { + opt *slog.HandlerOptions + writer io.Writer + level *slog.LevelVar + withLevel bool + withHandlerOptions bool +} + +type config struct { + coreConfig coreConfig + traceConfig *traceConfig +} + +func defaultConfig() *config { + coreConfig := defaultCoreConfig() + return &config{ + coreConfig: *coreConfig, + traceConfig: &traceConfig{ + recordStackTraceInSpan: true, + errorSpanLevel: slog.LevelError, + }, + } +} + +func defaultCoreConfig() *coreConfig { + level := new(slog.LevelVar) + level.Set(slog.LevelInfo) + return &coreConfig{ + opt: &slog.HandlerOptions{ + Level: level, + }, + writer: os.Stdout, + level: level, + withLevel: false, + withHandlerOptions: false, + } +} + +// WithHandlerOptions slog handler-options +func WithHandlerOptions(opt *slog.HandlerOptions) Option { + return option(func(cfg *config) { + cfg.coreConfig.opt = opt + cfg.coreConfig.withHandlerOptions = true + }) +} + +// WithOutput slog writer +func WithOutput(iow io.Writer) Option { + return option(func(cfg *config) { + cfg.coreConfig.writer = iow + }) +} + +// WithLevel slog level +func WithLevel(lvl *slog.LevelVar) Option { + return option(func(cfg *config) { + cfg.coreConfig.level = lvl + cfg.coreConfig.withLevel = true + }) +} + +// WithTraceErrorSpanLevel trace error span level option +func WithTraceErrorSpanLevel(level slog.Level) Option { + return option(func(cfg *config) { + cfg.traceConfig.errorSpanLevel = level + }) +} + +// WithRecordStackTraceInSpan record stack track option +func WithRecordStackTraceInSpan(recordStackTraceInSpan bool) Option { + return option(func(cfg *config) { + cfg.traceConfig.recordStackTraceInSpan = recordStackTraceInSpan + }) +} diff --git a/logging/slog/utils.go b/logging/slog/utils.go deleted file mode 100644 index c797bba..0000000 --- a/logging/slog/utils.go +++ /dev/null @@ -1 +0,0 @@ -package slog