diff --git a/README.md b/README.md index 3c10947..c6ea506 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,8 @@ defer eg.Release() // Return to pool when done eg.Add(err1) eg.Add(err2) -if eg.HasErrors() { - return eg.Error() +if err := eg.Join(); err != nil { + return err } ``` @@ -184,6 +184,11 @@ err := ewrap.New("error occurred", zerologLogger := zerolog.New(os.Stdout) err := ewrap.New("error occurred", ewrap.WithLogger(adapters.NewZerologAdapter(zerologLogger))) + +// Slog logger (Go 1.21+) +slogLogger := slog.New(slog.NewTextHandler(os.Stdout, nil)) +err := ewrap.New("error occurred", + ewrap.WithLogger(adapters.NewSlogAdapter(slogLogger))) ``` ## Error Formatting diff --git a/docs/docs/features/error-groups.md b/docs/docs/features/error-groups.md index 8d7365e..42bb1ab 100644 --- a/docs/docs/features/error-groups.md +++ b/docs/docs/features/error-groups.md @@ -22,9 +22,9 @@ defer eg.Release() // Don't forget to release it back to the pool eg.Add(ewrap.New("validation failed for email")) eg.Add(ewrap.New("validation failed for password")) -// Check if there are any errors -if eg.HasErrors() { - fmt.Printf("Encountered errors: %v\n", eg.Error()) +// Aggregate errors using errors.Join +if err := eg.Join(); err != nil { + fmt.Printf("Encountered errors: %v\n", err) } ``` diff --git a/docs/docs/features/logging.md b/docs/docs/features/logging.md index 21f8eff..40ddeb0 100644 --- a/docs/docs/features/logging.md +++ b/docs/docs/features/logging.md @@ -99,6 +99,24 @@ func setupZerolog() error { } ``` +### Slog Integration (Go 1.21+) + +```go +import ( + "log/slog" + "os" + "github.com/hyp3rd/ewrap/adapters" +) + +func setupSlogLogger() error { + slogLogger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + logger := adapters.NewSlogAdapter(slogLogger) + + return ewrap.New("operation failed", + ewrap.WithLogger(logger)) +} +``` + ## Advanced Logging Patterns ### Contextual Logging diff --git a/error_group.go b/error_group.go index 9522984..8748503 100644 --- a/error_group.go +++ b/error_group.go @@ -1,8 +1,10 @@ package ewrap import ( + "errors" "fmt" "log/slog" + "slices" "strings" "sync" ) @@ -145,10 +147,16 @@ func (eg *ErrorGroup) Errors() []error { eg.mu.RLock() defer eg.mu.RUnlock() - result := make([]error, len(eg.errors)) - copy(result, eg.errors) + return slices.Clone(eg.errors) +} + +// Join aggregates all errors in the group using errors.Join. +// It returns nil if the group is empty. +func (eg *ErrorGroup) Join() error { + eg.mu.RLock() + defer eg.mu.RUnlock() - return result + return errors.Join(eg.errors...) } // Clear removes all errors from the group while preserving capacity. diff --git a/error_group_test.go b/error_group_test.go index f604e1e..d31a27d 100644 --- a/error_group_test.go +++ b/error_group_test.go @@ -1,6 +1,7 @@ package ewrap import ( + "errors" "fmt" "sync" "testing" @@ -103,3 +104,25 @@ func BenchmarkErrorGroupPool(b *testing.B) { } }) } + +func TestErrorGroupJoin(t *testing.T) { + eg := NewErrorGroup() + err1 := fmt.Errorf("first") + err2 := fmt.Errorf("second") + + eg.Add(err1) + eg.Add(err2) + + joined := eg.Join() + if joined == nil { + t.Fatal("expected joined error") + } + if !errors.Is(joined, err1) || !errors.Is(joined, err2) { + t.Fatalf("joined error does not contain original errors") + } + + eg.Clear() + if eg.Join() != nil { + t.Fatal("expected nil when joining empty group") + } +} diff --git a/errors.go b/errors.go index 8552277..39174c7 100644 --- a/errors.go +++ b/errors.go @@ -84,10 +84,8 @@ func Wrap(err error, msg string, opts ...Option) *Error { wrappedErr.mu.RLock() stack = wrappedErr.stack - // Create a new metadata map with the existing values - metadata = make(map[string]any, len(wrappedErr.metadata)) - - maps.Copy(metadata, wrappedErr.metadata) + // Clone metadata map using maps.Clone for simplicity + metadata = maps.Clone(wrappedErr.metadata) wrappedErr.mu.RUnlock() } else { diff --git a/pkg/ewrap/adapters/slog.go b/pkg/ewrap/adapters/slog.go new file mode 100644 index 0000000..71dc5d0 --- /dev/null +++ b/pkg/ewrap/adapters/slog.go @@ -0,0 +1,30 @@ +//go:build go1.21 + +package adapters + +import "log/slog" + +// SlogAdapter adapts slog.Logger to the ewrap.Logger interface. +type SlogAdapter struct { + logger *slog.Logger +} + +// NewSlogAdapter creates a new slog logger adapter. +func NewSlogAdapter(logger *slog.Logger) *SlogAdapter { + return &SlogAdapter{logger: logger} +} + +// Error logs an error message with optional key-value pairs. +func (s *SlogAdapter) Error(msg string, keysAndValues ...any) { + s.logger.Error(msg, keysAndValues...) +} + +// Debug logs a debug message with optional key-value pairs. +func (s *SlogAdapter) Debug(msg string, keysAndValues ...any) { + s.logger.Debug(msg, keysAndValues...) +} + +// Info logs an info message with optional key-value pairs. +func (s *SlogAdapter) Info(msg string, keysAndValues ...any) { + s.logger.Info(msg, keysAndValues...) +} diff --git a/pkg/ewrap/adapters/slog_test.go b/pkg/ewrap/adapters/slog_test.go new file mode 100644 index 0000000..5e2e469 --- /dev/null +++ b/pkg/ewrap/adapters/slog_test.go @@ -0,0 +1,34 @@ +//go:build go1.21 + +package adapters + +import ( + "bytes" + "log/slog" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSlogAdapter(t *testing.T) { + var buf bytes.Buffer + handler := slog.NewTextHandler(&buf, &slog.HandlerOptions{Level: slog.LevelDebug}) + logger := slog.New(handler) + adapter := NewSlogAdapter(logger) + + t.Run("LogLevels", func(t *testing.T) { + buf.Reset() + adapter.Error("error message", "key1", "value1") + output := buf.String() + assert.Contains(t, output, "error message") + assert.Contains(t, output, "key1") + assert.Contains(t, output, "value1") + + buf.Reset() + adapter.Debug("debug message", "key2", "value2") + output = buf.String() + assert.Contains(t, output, "debug message") + assert.Contains(t, output, "key2") + assert.Contains(t, output, "value2") + }) +}