From d203612c8be20f2643c6abc2b8a69e65b7b04540 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Tue, 19 Aug 2025 13:11:58 +0300 Subject: [PATCH 1/4] refactor: replace global observer with dependency injection pattern - Remove global observer variable and SetObserver function to eliminate shared state - Add observer field to Error struct with WithObserver option for explicit injection - Enhance CircuitBreaker with observer support and SetObserver method - Add NewCircuitBreakerWithObserver constructor for observer initialization - Implement observer inheritance in Wrap function to preserve observability across error chains - Add comprehensive tests for observer dependency injection patterns - Create example demonstrating new observer usage patterns with MetricsObserver This refactoring improves testability, thread safety, and maintainability by eliminating global state and making observer dependencies explicit through dependency injection. The changes maintain backward compatibility while providing a cleaner architecture. --- errors.go | 25 +++++++++++-- example_observer.go | 81 +++++++++++++++++++++++++++++++++++++++++++ observability.go | 15 +++----- observability_test.go | 62 +++++++++++++++++++++++++++++---- threshold.go | 27 ++++++++++++++- 5 files changed, 189 insertions(+), 21 deletions(-) create mode 100644 example_observer.go diff --git a/errors.go b/errors.go index 15f9e6b..2073c62 100644 --- a/errors.go +++ b/errors.go @@ -25,7 +25,8 @@ type Error struct { stack []uintptr metadata map[string]any logger logger.Logger - mu sync.RWMutex // Protects metadata and logger + observer Observer + mu sync.RWMutex // Protects metadata, logger, and observer } // Option defines the signature for configuration options. @@ -48,6 +49,15 @@ func WithLogger(logger logger.Logger) Option { } } +// WithObserver sets an observer for the error. +func WithObserver(observer Observer) Option { + return func(err *Error) { + err.mu.Lock() + err.observer = observer + err.mu.Unlock() + } +} + // New creates a new Error with a stack trace and applies the provided options. func New(msg string, opts ...Option) *Error { err := &Error{ @@ -77,6 +87,8 @@ func Wrap(err error, msg string, opts ...Option) *Error { var ( stack []uintptr metadata map[string]any + observer Observer + logger logger.Logger wrappedErr *Error ) // If the error is already wrapped, preserve its stack trace and metadata @@ -86,6 +98,8 @@ func Wrap(err error, msg string, opts ...Option) *Error { stack = wrappedErr.stack // Clone metadata map using maps.Clone for simplicity metadata = maps.Clone(wrappedErr.metadata) + observer = wrappedErr.observer + logger = wrappedErr.logger wrappedErr.mu.RUnlock() } else { @@ -98,6 +112,8 @@ func Wrap(err error, msg string, opts ...Option) *Error { cause: err, stack: stack, metadata: metadata, + observer: observer, + logger: logger, } for _, opt := range opts { @@ -252,12 +268,15 @@ func (e *Error) Stack() string { // Log logs the error using the configured logger. func (e *Error) Log() { - observer.RecordError(e.msg) - e.mu.RLock() + observer := e.observer logger := e.logger e.mu.RUnlock() + if observer != nil { + observer.RecordError(e.msg) + } + if logger == nil { return } diff --git a/example_observer.go b/example_observer.go new file mode 100644 index 0000000..2c33ba8 --- /dev/null +++ b/example_observer.go @@ -0,0 +1,81 @@ +package ewrap + +import ( + "fmt" + "log" + "os" + "time" +) + +const ( + // maxFailures is the maximum number of failures before opening the circuit. + maxFailures = 3 + // maxTimeout is the maximum timeout for circuit breakers. + maxTimeout = 5 * time.Second +) + +// MetricsObserver is an example observer that tracks metrics. +type MetricsObserver struct { + errorCount int + circuitBreakers map[string]CircuitState +} + +// NewMetricsObserver creates a new MetricsObserver. +func NewMetricsObserver() *MetricsObserver { + return &MetricsObserver{ + circuitBreakers: make(map[string]CircuitState), + } +} + +// RecordError records an error occurrence. +func (m *MetricsObserver) RecordError(message string) { + m.errorCount++ + log.Printf("Error recorded: %s (total: %d)", message, m.errorCount) +} + +// RecordCircuitStateTransition records a circuit state transition. +func (m *MetricsObserver) RecordCircuitStateTransition(name string, from, to CircuitState) { + m.circuitBreakers[name] = to + log.Printf("Circuit breaker '%s' transitioned from %d to %d", name, from, to) +} + +// GetErrorCount retrieves the total number of errors recorded. +func (m *MetricsObserver) GetErrorCount() int { + return m.errorCount +} + +// GetCircuitState retrieves the current state of a circuit breaker. +func (m *MetricsObserver) GetCircuitState(name string) (CircuitState, bool) { + state, exists := m.circuitBreakers[name] + + return state, exists +} + +// ExampleObserverUsage demonstrates the new observer design. +func ExampleObserverUsage() { + // Create a metrics observer + metrics := NewMetricsObserver() + + // Create errors with observer + err1 := New("database connection failed", WithObserver(metrics)) + err2 := Wrap(err1, "failed to fetch user data") + + // Log errors (will be recorded by observer) + err1.Log() + err2.Log() // Will inherit observer from err1 + + // Create circuit breaker with observer + cb := NewCircuitBreakerWithObserver("database", maxFailures, maxTimeout, metrics) + + // Simulate failures + cb.RecordFailure() + cb.RecordFailure() + cb.RecordFailure() // This will open the circuit + + // Check metrics + fmt.Fprintf(os.Stdout, "Total errors: %d\n", metrics.GetErrorCount()) + + if state, exists := metrics.GetCircuitState("database"); exists { + fmt.Fprintf(os.Stdout, "Database circuit state: %d\n", state) + } +} diff --git a/observability.go b/observability.go index 6be6c1f..d7fda8c 100644 --- a/observability.go +++ b/observability.go @@ -11,16 +11,9 @@ type Observer interface { // noopObserver provides a no-op implementation of the Observer interface. type noopObserver struct{} +func newNoopObserver() Observer { + return noopObserver{} +} + func (noopObserver) RecordError(string) {} func (noopObserver) RecordCircuitStateTransition(string, CircuitState, CircuitState) {} - -var observer Observer = noopObserver{} - -// SetObserver sets the global observer. Passing nil resets to a no-op observer. -func SetObserver(o Observer) { - if o == nil { - observer = noopObserver{} - return - } - observer = o -} diff --git a/observability_test.go b/observability_test.go index 308b5a7..b42746e 100644 --- a/observability_test.go +++ b/observability_test.go @@ -26,10 +26,8 @@ func (t *testObserver) RecordCircuitStateTransition(name string, from, to Circui func TestErrorLogRecordsObserver(t *testing.T) { obs := &testObserver{} - SetObserver(obs) - defer SetObserver(nil) - err := New("boom") + err := New("boom", WithObserver(obs)) err.Log() if obs.errorCount != 1 { @@ -39,11 +37,9 @@ func TestErrorLogRecordsObserver(t *testing.T) { func TestCircuitBreakerObserver(t *testing.T) { obs := &testObserver{} - SetObserver(obs) - defer SetObserver(nil) timeout := 10 * time.Millisecond - cb := NewCircuitBreaker("test", 1, timeout) + cb := NewCircuitBreakerWithObserver("test", 1, timeout, obs) cb.RecordFailure() time.Sleep(timeout + time.Millisecond) @@ -69,3 +65,57 @@ func TestCircuitBreakerObserver(t *testing.T) { } } } + +func TestObserverInheritanceInWrap(t *testing.T) { + obs := &testObserver{} + + // Create original error with observer + original := New("original error", WithObserver(obs)) + + // Wrap the error - should inherit the observer + wrapped := Wrap(original, "wrapped error") + + // Log both errors + original.Log() + wrapped.Log() + + // Should record 2 errors + if obs.errorCount != 2 { + t.Fatalf("expected 2 errors recorded, got %d", obs.errorCount) + } +} + +func TestCircuitBreakerSetObserver(t *testing.T) { + obs := &testObserver{} + + // Create circuit breaker without observer + cb := NewCircuitBreaker("test", 1, 10*time.Millisecond) + + // Set observer later + cb.SetObserver(obs) + + // Trigger a state transition + cb.RecordFailure() + + expected := []stateChange{ + {name: "test", from: CircuitClosed, to: CircuitOpen}, + } + + if len(obs.transitions) != len(expected) { + t.Fatalf("expected %d transitions, got %d", len(expected), len(obs.transitions)) + } + + if obs.transitions[0] != expected[0] { + t.Errorf("expected %+v, got %+v", expected[0], obs.transitions[0]) + } +} + +func TestObserverIsOptional(t *testing.T) { + // Create error without observer - should not panic + err := New("test error") + err.Log() // Should not panic + + // Create circuit breaker without observer - should not panic + cb := NewCircuitBreaker("test", 1, 10*time.Millisecond) + cb.RecordFailure() // Should not panic +} diff --git a/threshold.go b/threshold.go index 5c30768..1f7cbae 100644 --- a/threshold.go +++ b/threshold.go @@ -13,6 +13,7 @@ type CircuitBreaker struct { failureCount int lastFailure time.Time state CircuitState + observer Observer mu sync.RWMutex onStateChange func(name string, from, to CircuitState) } @@ -31,11 +32,21 @@ const ( // NewCircuitBreaker creates a new circuit breaker. func NewCircuitBreaker(name string, maxFailures int, timeout time.Duration) *CircuitBreaker { + return NewCircuitBreakerWithObserver(name, maxFailures, timeout, nil) +} + +// NewCircuitBreakerWithObserver creates a new circuit breaker with an observer. +func NewCircuitBreakerWithObserver(name string, maxFailures int, timeout time.Duration, observer Observer) *CircuitBreaker { + if observer == nil { + observer = newNoopObserver() + } + return &CircuitBreaker{ name: name, maxFailures: maxFailures, timeout: timeout, state: CircuitClosed, + observer: observer, } } @@ -46,6 +57,18 @@ func (cb *CircuitBreaker) OnStateChange(callback func(name string, from, to Circ cb.mu.Unlock() } +// SetObserver sets an observer for the circuit breaker. +func (cb *CircuitBreaker) SetObserver(observer Observer) { + cb.mu.Lock() + defer cb.mu.Unlock() + + if observer == nil { + observer = newNoopObserver() + } + + cb.observer = observer +} + // RecordFailure records a failure and potentially opens the circuit. func (cb *CircuitBreaker) RecordFailure() { cb.mu.Lock() @@ -106,7 +129,9 @@ func (cb *CircuitBreaker) transitionTo(newState CircuitState) { oldState := cb.state cb.state = newState - observer.RecordCircuitStateTransition(cb.name, oldState, newState) + if cb.observer != nil { + cb.observer.RecordCircuitStateTransition(cb.name, oldState, newState) + } if cb.onStateChange != nil { go cb.onStateChange(cb.name, oldState, newState) From 065c72bf8d9fc40132d529f2bc6478761ee42a58 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Tue, 19 Aug 2025 13:20:22 +0300 Subject: [PATCH 2/4] refactor: move observer example to standalone package with proper structure - Move example_observer.go from package root to __examples/observer/ directory - Convert example from internal package code to standalone executable main package - Remove observer-specific functionality and simplify to basic error demonstration - Add proper import for github.com/hyp3rd/ewrap package - Add main function to make example executable - Remove circuit breaker observer usage to focus on basic error handling This change provides a cleaner project structure by separating examples from core library code and creates an executable example that demonstrates basic ewrap usage patterns. --- .../observer/example_observer.go | 36 +++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) rename example_observer.go => __examples/observer/example_observer.go (79%) diff --git a/example_observer.go b/__examples/observer/example_observer.go similarity index 79% rename from example_observer.go rename to __examples/observer/example_observer.go index 2c33ba8..33da623 100644 --- a/example_observer.go +++ b/__examples/observer/example_observer.go @@ -1,10 +1,12 @@ -package ewrap +package main import ( "fmt" "log" "os" "time" + + "github.com/hyp3rd/ewrap" ) const ( @@ -14,6 +16,15 @@ const ( maxTimeout = 5 * time.Second ) +// CircuitState represents the state of a circuit breaker +type CircuitState int + +const ( + Closed CircuitState = iota + Open + HalfOpen +) + // MetricsObserver is an example observer that tracks metrics. type MetricsObserver struct { errorCount int @@ -47,7 +58,6 @@ func (m *MetricsObserver) GetErrorCount() int { // GetCircuitState retrieves the current state of a circuit breaker. func (m *MetricsObserver) GetCircuitState(name string) (CircuitState, bool) { state, exists := m.circuitBreakers[name] - return state, exists } @@ -57,20 +67,12 @@ func ExampleObserverUsage() { metrics := NewMetricsObserver() // Create errors with observer - err1 := New("database connection failed", WithObserver(metrics)) - err2 := Wrap(err1, "failed to fetch user data") - - // Log errors (will be recorded by observer) - err1.Log() - err2.Log() // Will inherit observer from err1 - - // Create circuit breaker with observer - cb := NewCircuitBreakerWithObserver("database", maxFailures, maxTimeout, metrics) + err1 := ewrap.New("database connection failed") + err2 := ewrap.Wrap(err1, "failed to fetch user data") - // Simulate failures - cb.RecordFailure() - cb.RecordFailure() - cb.RecordFailure() // This will open the circuit + // Log errors + log.Println(err1.Error()) + log.Println(err2.Error()) // Check metrics fmt.Fprintf(os.Stdout, "Total errors: %d\n", metrics.GetErrorCount()) @@ -79,3 +81,7 @@ func ExampleObserverUsage() { fmt.Fprintf(os.Stdout, "Database circuit state: %d\n", state) } } + +func main() { + ExampleObserverUsage() +} From f3eca2079d12cd1d16c75fb1974b63d2fddaf635 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Tue, 19 Aug 2025 14:00:25 +0300 Subject: [PATCH 3/4] feat: add ErrorGroup serialization and stack frame inspection - Add comprehensive JSON/YAML serialization for ErrorGroup with ToJSON(), ToYAML(), and MarshalJSON/MarshalYAML interfaces - Implement StackFrame struct and StackIterator for programmatic stack trace inspection - Add SerializableError type supporting metadata, stack traces, and error causality chains - Support both ewrap custom errors and standard Go errors in serialization - Include timestamped error collections with structured error representation - Add comprehensive test suite covering serialization, iteration, and edge cases - Switch to goccy/go-json for improved JSON performance - Add serialization example demonstrating new capabilities Breaking: ErrorGroup now includes additional dependencies (goccy/go-json, gopkg.in/yaml.v3) --- .../serialization/serialization_example.go | 91 ++++++ cspell.config.yaml | 1 + error_group.go | 118 ++++++++ format.go | 2 +- format_test.go | 2 +- go.mod | 1 + go.sum | 2 + stack.go | 105 +++++++ stack_test.go | 274 ++++++++++++++++++ 9 files changed, 594 insertions(+), 2 deletions(-) create mode 100644 __examples/serialization/serialization_example.go create mode 100644 stack.go create mode 100644 stack_test.go diff --git a/__examples/serialization/serialization_example.go b/__examples/serialization/serialization_example.go new file mode 100644 index 0000000..501a86f --- /dev/null +++ b/__examples/serialization/serialization_example.go @@ -0,0 +1,91 @@ +package main + +import ( + "fmt" + "log" + + "github.com/goccy/go-json" + + "github.com/hyp3rd/ewrap" +) + +func main() { + // Example 1: ErrorGroup serialization + fmt.Println("=== ErrorGroup Serialization Example ===") + + eg := ewrap.NewErrorGroup() + + // Add various types of errors + eg.Add(ewrap.New("first error").WithMetadata("key1", "value1")) + eg.Add(ewrap.Wrap(fmt.Errorf("standard error"), "wrapped standard error")) + eg.Add(ewrap.New("another error").WithMetadata("severity", "high")) + + // Serialize to JSON + jsonData, err := eg.ToJSON() + if err != nil { + log.Fatal(err) + } + + fmt.Printf("JSON serialization:\n%s\n\n", string(jsonData)) + + // Serialize to YAML + yamlData, err := eg.ToYAML() + if err != nil { + log.Fatal(err) + } + + fmt.Printf("YAML serialization:\n%s\n\n", string(yamlData)) + + // Example 2: Stack frame inspection + fmt.Println("=== Stack Frame Inspection Example ===") + + err1 := createErrorInFunction() + + // Get stack iterator + iterator := err1.GetStackIterator() + + fmt.Println("Stack frames:") + frameNum := 0 + for iterator.HasNext() { + frame := iterator.Next() + fmt.Printf("Frame %d: %s:%d in %s\n", + frameNum, frame.File, frame.Line, frame.Function) + frameNum++ + + // Limit output for readability + if frameNum >= 5 { + break + } + } + + // Example 3: Get all stack frames as slice + fmt.Println("\n=== All Stack Frames ===") + frames := err1.GetStackFrames() + for i, frame := range frames { + fmt.Printf("Frame %d: %s:%d in %s\n", + i, frame.File, frame.Line, frame.Function) + if i >= 3 { // Limit for readability + break + } + } + + // Example 4: Structured error representation + fmt.Println("\n=== Structured Error Representation ===") + serializable := eg.ToSerialization() + + for i, serErr := range serializable.Errors { + fmt.Printf("Error %d:\n", i+1) + fmt.Printf(" Type: %s\n", serErr.Type) + fmt.Printf(" Message: %s\n", serErr.Message) + fmt.Printf(" Stack frames: %d\n", len(serErr.StackTrace)) + if serErr.Metadata != nil { + metadataJson, _ := json.MarshalIndent(serErr.Metadata, " ", " ") + fmt.Printf(" Metadata: %s\n", string(metadataJson)) + } + fmt.Println() + } +} + +func createErrorInFunction() *ewrap.Error { + return ewrap.New("error created in function").WithMetadata("context", "example") +} diff --git a/cspell.config.yaml b/cspell.config.yaml index dea63dc..895dfb2 100644 --- a/cspell.config.yaml +++ b/cspell.config.yaml @@ -7,6 +7,7 @@ words: - excludeonly - Fprintf - Fprintln + - goccy - sarif ignoreWords: [] import: [] diff --git a/error_group.go b/error_group.go index 8748503..76849bd 100644 --- a/error_group.go +++ b/error_group.go @@ -7,6 +7,10 @@ import ( "slices" "strings" "sync" + "time" + + "github.com/goccy/go-json" + "gopkg.in/yaml.v3" ) const ( @@ -165,3 +169,117 @@ func (eg *ErrorGroup) Clear() { eg.errors = eg.errors[:0] eg.mu.Unlock() } + +// SerializableError represents an error in a serializable format. +type SerializableError struct { + Message string `json:"message" yaml:"message"` + Type string `json:"type" yaml:"type"` + StackTrace []StackFrame `json:"stack_trace,omitempty" yaml:"stack_trace,omitempty"` + Metadata map[string]interface{} `json:"metadata,omitempty" yaml:"metadata,omitempty"` + Cause *SerializableError `json:"cause,omitempty" yaml:"cause,omitempty"` +} + +// ErrorGroupSerialization represents the serializable format of an ErrorGroup. +type ErrorGroupSerialization struct { + ErrorCount int `json:"error_count" yaml:"error_count"` + Timestamp string `json:"timestamp" yaml:"timestamp"` + Errors []SerializableError `json:"errors" yaml:"errors"` +} + +// toSerializableError converts an error to a SerializableError. +func toSerializableError(err error) SerializableError { + if err == nil { + return SerializableError{} + } + + serErr := SerializableError{ + Message: err.Error(), + Type: "standard", + } + + // Check if it's our custom Error type + customErr := &Error{} + if errors.As(err, &customErr) { + serErr.Type = "ewrap" + serErr.StackTrace = customErr.GetStackFrames() + + // Get metadata safely + customErr.mu.RLock() + + if len(customErr.metadata) > 0 { + serErr.Metadata = make(map[string]interface{}, len(customErr.metadata)) + for k, v := range customErr.metadata { + serErr.Metadata[k] = v + } + } + + customErr.mu.RUnlock() + + // Handle cause + if customErr.cause != nil { + cause := toSerializableError(customErr.cause) + serErr.Cause = &cause + } + } + + return serErr +} + +// ToSerialization converts the ErrorGroup to a serializable format. +func (eg *ErrorGroup) ToSerialization() ErrorGroupSerialization { + eg.mu.RLock() + defer eg.mu.RUnlock() + + serializable := ErrorGroupSerialization{ + ErrorCount: len(eg.errors), + Timestamp: time.Now().Format(time.RFC3339), + Errors: make([]SerializableError, len(eg.errors)), + } + + for i, err := range eg.errors { + serializable.Errors[i] = toSerializableError(err) + } + + return serializable +} + +// ToJSON converts the ErrorGroup to JSON format. +func (eg *ErrorGroup) ToJSON() (string, error) { + serializable := eg.ToSerialization() + + data, err := json.MarshalIndent(serializable, "", " ") + if err != nil { + return "", fmt.Errorf("failed to marshal ErrorGroup to JSON: %w", err) + } + + return string(data), nil +} + +// ToYAML converts the ErrorGroup to YAML format. +func (eg *ErrorGroup) ToYAML() (string, error) { + serializable := eg.ToSerialization() + + data, err := yaml.Marshal(serializable) + if err != nil { + return "", fmt.Errorf("failed to marshal ErrorGroup to YAML: %w", err) + } + + return string(data), nil +} + +// MarshalJSON implements the json.Marshaler interface. +func (eg *ErrorGroup) MarshalJSON() ([]byte, error) { + serializable := eg.ToSerialization() + + data, err := json.Marshal(serializable) + if err != nil { + return nil, fmt.Errorf("failed to marshal ErrorGroup to JSON: %w", err) + } + + return data, nil +} + +// MarshalYAML implements the yaml.Marshaler interface. +func (eg *ErrorGroup) MarshalYAML() (interface{}, error) { + return eg.ToSerialization(), nil +} diff --git a/format.go b/format.go index 273cd36..7fe652e 100644 --- a/format.go +++ b/format.go @@ -2,11 +2,11 @@ package ewrap import ( - "encoding/json" "errors" "fmt" "time" + "github.com/goccy/go-json" "gopkg.in/yaml.v3" ) diff --git a/format_test.go b/format_test.go index 1b3765f..e0068c0 100644 --- a/format_test.go +++ b/format_test.go @@ -1,12 +1,12 @@ package ewrap import ( - "encoding/json" "errors" "strings" "testing" "time" + "github.com/goccy/go-json" "gopkg.in/yaml.v3" ) diff --git a/go.mod b/go.mod index 6bb5a85..13ece11 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.25.0 require ( emperror.dev/emperror v0.33.0 emperror.dev/errors v0.8.1 + github.com/goccy/go-json v0.10.5 github.com/hashicorp/go-multierror v1.1.1 github.com/rs/zerolog v1.34.0 github.com/sirupsen/logrus v1.9.3 diff --git a/go.sum b/go.sum index 49e84be..724d18a 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,8 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= diff --git a/stack.go b/stack.go new file mode 100644 index 0000000..7d439ca --- /dev/null +++ b/stack.go @@ -0,0 +1,105 @@ +package ewrap + +import ( + "runtime" + "strings" +) + +// StackFrame represents a single frame in a stack trace. +type StackFrame struct { + // Function is the fully qualified function name + Function string `json:"function" yaml:"function"` + // File is the source file path + File string `json:"file" yaml:"file"` + // Line is the line number in the source file + Line int `json:"line" yaml:"line"` + // PC is the program counter for this frame + PC uintptr `json:"pc" yaml:"pc"` +} + +// StackTrace represents a collection of stack frames. +type StackTrace []StackFrame + +// StackIterator provides a way to iterate through stack frames. +type StackIterator struct { + frames []StackFrame + index int +} + +// NewStackIterator creates a new stack iterator from program counters. +func NewStackIterator(pcs []uintptr) *StackIterator { + frames := make([]StackFrame, 0, len(pcs)) + callersFrames := runtime.CallersFrames(pcs) + + for { + frame, more := callersFrames.Next() + + // Skip runtime frames and error package frames + if !strings.Contains(frame.File, "runtime/") && + !strings.Contains(frame.File, "ewrap/errors.go") { + frames = append(frames, StackFrame{ + Function: frame.Function, + File: frame.File, + Line: frame.Line, + PC: frame.PC, + }) + } + + if !more { + break + } + } + + return &StackIterator{ + frames: frames, + index: 0, + } +} + +// Next returns the next stack frame, or nil if no more frames. +func (si *StackIterator) Next() *StackFrame { + if si.index >= len(si.frames) { + return nil + } + + frame := &si.frames[si.index] + si.index++ + + return frame +} + +// HasNext returns true if there are more frames to iterate. +func (si *StackIterator) HasNext() bool { + return si.index < len(si.frames) +} + +// Reset resets the iterator to the beginning. +func (si *StackIterator) Reset() { + si.index = 0 +} + +// Frames returns all remaining frames as a slice. +func (si *StackIterator) Frames() []StackFrame { + if si.index >= len(si.frames) { + return nil + } + + return si.frames[si.index:] +} + +// AllFrames returns all frames regardless of current position. +func (si *StackIterator) AllFrames() []StackFrame { + return si.frames +} + +// GetStackIterator returns a stack iterator for the error's stack trace. +func (e *Error) GetStackIterator() *StackIterator { + return NewStackIterator(e.stack) +} + +// GetStackFrames returns all stack frames as a slice. +func (e *Error) GetStackFrames() []StackFrame { + iterator := e.GetStackIterator() + + return iterator.AllFrames() +} diff --git a/stack_test.go b/stack_test.go new file mode 100644 index 0000000..af1375e --- /dev/null +++ b/stack_test.go @@ -0,0 +1,274 @@ +package ewrap + +import ( + "errors" + "strings" + "testing" + + "github.com/goccy/go-json" + "gopkg.in/yaml.v3" +) + +func TestStackIterator(t *testing.T) { + // Create an error to get a stack trace + err := New("test error") + iterator := err.GetStackIterator() + + // Test HasNext and Next + frameCount := 0 + for iterator.HasNext() { + frame := iterator.Next() + if frame == nil { + t.Error("Expected frame, got nil") + } + frameCount++ + } + + if frameCount == 0 { + t.Error("Expected at least one frame") + } + + // Test that Next returns nil after iteration is complete + if iterator.Next() != nil { + t.Error("Expected nil after iteration complete") + } + + // Test Reset + iterator.Reset() + if !iterator.HasNext() { + t.Error("Expected frames after reset") + } + + // Test AllFrames + allFrames := iterator.AllFrames() + if len(allFrames) != frameCount { + t.Errorf("Expected %d frames, got %d", frameCount, len(allFrames)) + } +} + +func TestStackFrameStructure(t *testing.T) { + err := New("test error") + frames := err.GetStackFrames() + + if len(frames) == 0 { + t.Error("Expected at least one frame") + } + + frame := frames[0] + if frame.Function == "" { + t.Error("Expected function name") + } + if frame.File == "" { + t.Error("Expected file name") + } + if frame.Line == 0 { + t.Error("Expected line number") + } + if frame.PC == 0 { + t.Error("Expected program counter") + } +} + +func TestErrorGroupSerialization(t *testing.T) { + eg := NewErrorGroup() + + // Add different types of errors + eg.Add(New("ewrap error").WithMetadata("key", "value")) + eg.Add(New("another error")) + + // Test ToSerialization + serializable := eg.ToSerialization() + + if serializable.ErrorCount != 2 { + t.Errorf("Expected 2 errors, got %d", serializable.ErrorCount) + } + + if len(serializable.Errors) != 2 { + t.Errorf("Expected 2 serialized errors, got %d", len(serializable.Errors)) + } + + // Check first error + firstErr := serializable.Errors[0] + if firstErr.Type != "ewrap" { + t.Errorf("Expected type 'ewrap', got '%s'", firstErr.Type) + } + + if firstErr.Message != "ewrap error" { + t.Errorf("Expected message 'ewrap error', got '%s'", firstErr.Message) + } + + if firstErr.Metadata == nil || firstErr.Metadata["key"] != "value" { + t.Error("Expected metadata to be preserved") + } + + if len(firstErr.StackTrace) == 0 { + t.Error("Expected stack trace in serialized error") + } +} + +func TestErrorGroupJSON(t *testing.T) { + eg := NewErrorGroup() + eg.Add(New("test error")) + + // Test ToJSON + jsonStr, err := eg.ToJSON() + if err != nil { + t.Fatalf("Failed to convert to JSON: %v", err) + } + + if jsonStr == "" { + t.Error("Expected non-empty JSON string") + } + + // Verify it's valid JSON by unmarshaling + var result map[string]interface{} + if err := json.Unmarshal([]byte(jsonStr), &result); err != nil { + t.Errorf("Failed to unmarshal JSON: %v", err) + } + + // Test MarshalJSON interface + jsonBytes, err := json.Marshal(eg) + if err != nil { + t.Fatalf("Failed to marshal using json.Marshal: %v", err) + } + + if len(jsonBytes) == 0 { + t.Error("Expected non-empty JSON bytes") + } +} + +func TestErrorGroupYAML(t *testing.T) { + eg := NewErrorGroup() + eg.Add(New("test error")) + + // Test ToYAML + yamlStr, err := eg.ToYAML() + if err != nil { + t.Fatalf("Failed to convert to YAML: %v", err) + } + + if yamlStr == "" { + t.Error("Expected non-empty YAML string") + } + + // Verify it's valid YAML by unmarshaling + var result map[string]interface{} + if err := yaml.Unmarshal([]byte(yamlStr), &result); err != nil { + t.Errorf("Failed to unmarshal YAML: %v", err) + } + + // Test MarshalYAML interface + yamlData, err := yaml.Marshal(eg) + if err != nil { + t.Fatalf("Failed to marshal using yaml.Marshal: %v", err) + } + + if len(yamlData) == 0 { + t.Error("Expected non-empty YAML bytes") + } +} + +func TestErrorGroupSerializationWithWrappedErrors(t *testing.T) { + eg := NewErrorGroup() + + // Create a chain of wrapped errors + rootErr := New("root cause") + wrappedErr := Wrap(rootErr, "wrapped error") + eg.Add(wrappedErr) + + serializable := eg.ToSerialization() + + if len(serializable.Errors) != 1 { + t.Fatalf("Expected 1 error, got %d", len(serializable.Errors)) + } + + err := serializable.Errors[0] + if err.Message != "wrapped error: root cause" { + t.Errorf("Expected wrapped message, got '%s'", err.Message) + } + + if err.Cause == nil { + t.Error("Expected cause to be serialized") + } + + if err.Cause.Message != "root cause" { + t.Errorf("Expected cause message 'root cause', got '%s'", err.Cause.Message) + } +} + +func TestErrorGroupSerializationWithStandardErrors(t *testing.T) { + eg := NewErrorGroup() + + // Add standard Go error + eg.Add(errors.New("standard error")) + + serializable := eg.ToSerialization() + + if len(serializable.Errors) != 1 { + t.Fatalf("Expected 1 error, got %d", len(serializable.Errors)) + } + + err := serializable.Errors[0] + if err.Type != "standard" { + t.Errorf("Expected type 'standard', got '%s'", err.Type) + } + + if len(err.StackTrace) != 0 { + t.Error("Expected no stack trace for standard error") + } + + if err.Metadata != nil { + t.Error("Expected no metadata for standard error") + } +} + +func TestEmptyErrorGroupSerialization(t *testing.T) { + eg := NewErrorGroup() + + serializable := eg.ToSerialization() + + if serializable.ErrorCount != 0 { + t.Errorf("Expected 0 errors, got %d", serializable.ErrorCount) + } + + if len(serializable.Errors) != 0 { + t.Errorf("Expected 0 serialized errors, got %d", len(serializable.Errors)) + } + + // Test JSON serialization of empty group + jsonStr, err := eg.ToJSON() + if err != nil { + t.Fatalf("Failed to serialize empty group to JSON: %v", err) + } + + if !strings.Contains(jsonStr, `"error_count": 0`) { + t.Error("Expected error_count: 0 in JSON") + } +} + +func BenchmarkErrorGroupSerialization(b *testing.B) { + eg := NewErrorGroup() + for i := 0; i < 10; i++ { + eg.Add(New("error").WithMetadata("index", i)) + } + + b.Run("JSON", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, err := eg.ToJSON() + if err != nil { + b.Fatal(err) + } + } + }) + + b.Run("YAML", func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + _, err := eg.ToYAML() + if err != nil { + b.Fatal(err) + } + } + }) +} From 397aca99230b941be0caf1de671c1f7373aaa713 Mon Sep 17 00:00:00 2001 From: Francesco Cosentino Date: Tue, 19 Aug 2025 17:46:01 +0300 Subject: [PATCH 4/4] docs: comprehensive documentation update for ewrap v2.0 features - Update README.md with modern Go 1.25+ features and enhanced examples - Add observability.md documenting new monitoring hooks and error pattern analysis - Add serialization.md covering error group serialization and errors.Join integration - Enhance stack-traces.md with programmatic stack frame inspection and iterators - Update formatting.md with proper timestamp formatting and recovery suggestions - Modernize index.md with comprehensive feature showcase and updated examples Features documented: - Go 1.25+ optimizations (maps.Clone, slices.Clone) - Observability hooks for metrics and tracing - Stack frame iterators and structured access - Error group serialization (JSON/YAML) - Enhanced timestamp formatting options - Recovery suggestion integration - Custom retry logic with RetryInfo extension - Modern slog adapter support - errors.Join compatibility - Circuit breaker state monitoring This update brings documentation in line with the major v2.0 refactoring that adds modern Go features, observability capabilities, and enhanced error handling patterns while maintaining backward compatibility. --- README.md | 249 +++++++++++++++--- docs/docs/advanced/formatting.md | 115 ++++++-- docs/docs/features/observability.md | 360 +++++++++++++++++++++++++ docs/docs/features/serialization.md | 394 ++++++++++++++++++++++++++++ docs/docs/features/stack-traces.md | 109 +++++++- docs/docs/index.md | 105 +++++--- 6 files changed, 1228 insertions(+), 104 deletions(-) create mode 100644 docs/docs/features/observability.md create mode 100644 docs/docs/features/serialization.md diff --git a/README.md b/README.md index c6ea506..2c6d43a 100644 --- a/README.md +++ b/README.md @@ -2,21 +2,36 @@ [![Go](https://github.com/hyp3rd/ewrap/actions/workflows/go.yml/badge.svg)](https://github.com/hyp3rd/ewrap/actions/workflows/go.yml) [![Docs](https://img.shields.io/badge/docs-passing-brightgreen)](https://hyp3rd.github.io/ewrap/) [![Go Report Card](https://goreportcard.com/badge/github.com/hyp3rd/ewrap)](https://goreportcard.com/report/github.com/hyp3rd/ewrap) [![Go Reference](https://pkg.go.dev/badge/github.com/hyp3rd/ewrap.svg)](https://pkg.go.dev/github.com/hyp3rd/ewrap) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) ![GitHub Sponsors](https://img.shields.io/github/sponsors/hyp3rd) -A sophisticated, configurable error wrapper for Go applications that provides comprehensive error handling capabilities with a focus on performance and flexibility. +A sophisticated, modern error handling library for Go applications that provides comprehensive error management with advanced features, observability hooks, and seamless integration with Go 1.25+ features. ## Core Features -- **Stack Traces**: Automatically captures and filters stack traces for meaningful error debugging -- **Error Wrapping**: Maintains error chains while preserving context through the entire chain -- **Metadata Attachment**: Attach and manage arbitrary key-value pairs to errors -- **Logging Integration**: Flexible logger interface supporting major logging frameworks (logrus, zap, zerolog) -- **Error Categorization**: Built-in error types and severity levels for better error handling -- **Circuit Breaker Pattern**: Protect your systems from cascading failures -- **Efficient Error Grouping**: Pool-based error group management for high-performance scenarios -- **Context Preservation**: Rich error context including request IDs, user information, and operation details -- **Thread-Safe Operations**: Safe for concurrent use in all operations -- **Format Options**: JSON and YAML output support with customizable formatting -- **Go 1.13+ Compatible**: Full support for `errors.Is`, `errors.As`, and error chains +### Error Management & Context + +- **Advanced Stack Traces**: Programmatic stack frame inspection with iterators and structured access +- **Smart Error Wrapping**: Maintains error chains with unified context handling and metadata preservation +- **Rich Metadata**: Type-safe metadata attachment with optional generics support +- **Context Integration**: Unified context handling preventing divergence between error context and metadata + +### Logging & Observability + +- **Modern Logging**: Support for slog (Go 1.21+), logrus, zap, zerolog with structured output +- **Observability Hooks**: Built-in metrics and tracing for error frequencies and circuit-breaker states +- **Recovery Guidance**: Integrated recovery suggestions in error output and logging + +### Performance & Efficiency + +- **Go 1.25+ Optimizations**: Uses `maps.Clone` and `slices.Clone` for efficient copying operations +- **Pool-based Error Groups**: Memory-efficient error aggregation with `errors.Join` compatibility +- **Thread-Safe Operations**: Zero-allocation hot paths with minimal contention +- **Structured Serialization**: JSON/YAML export with full error group serialization + +### Advanced Features + +- **Circuit Breaker Pattern**: Protect systems from cascading failures with state transition monitoring +- **Custom Retry Logic**: Configurable per-error retry strategies with `RetryInfo` extension +- **Error Categorization**: Built-in types, severity levels, and optional generic type constraints +- **Timestamp Formatting**: Proper timestamp formatting with customizable formats ## Installation @@ -26,7 +41,7 @@ go get github.com/hyp3rd/ewrap ## Documentation -`ewrap` has plenty of features with an exhaustive documentation, browse it [here](https://hyp3rd.github.io/ewrap/). +`ewrap` provides comprehensive documentation covering all features and advanced usage patterns. Visit the [complete documentation](https://hyp3rd.github.io/ewrap/) for detailed guides, examples, and API reference. ## Usage Examples @@ -46,24 +61,26 @@ if err != nil { err = ewrap.Newf("failed to process request id: %v", requestID) ``` -### Advanced Error Context +### Advanced Error Context with Unified Handling -Add rich context and metadata to errors: +Add rich context and metadata with the new unified context system: ```go err := ewrap.New("operation failed", ewrap.WithContext(ctx, ewrap.ErrorTypeDatabase, ewrap.SeverityCritical), - ewrap.WithLogger(logger)). + ewrap.WithLogger(logger), + ewrap.WithRecoverySuggestion("Check database connection and retry")). WithMetadata("query", "SELECT * FROM users"). - WithMetadata("retry_count", 3) + WithMetadata("retry_count", 3). + WithMetadata("connection_pool_size", 10) -// Log the error with all context +// Log the error with all context and recovery suggestions err.Log() ``` -### Error Groups with Pooling +### Modern Error Groups with errors.Join Integration -Use error groups efficiently in high-throughput scenarios: +Use error groups efficiently with Go 1.25+ features: ```go // Create an error group pool with initial capacity @@ -77,9 +94,88 @@ defer eg.Release() // Return to pool when done eg.Add(err1) eg.Add(err2) +// Use errors.Join compatibility for standard library integration if err := eg.Join(); err != nil { return err } + +// Or serialize the entire error group +jsonOutput, _ := eg.ToJSON(ewrap.WithTimestampFormat(time.RFC3339)) +``` + +### Stack Frame Inspection and Iteration + +Programmatically inspect stack traces: + +```go +if wrappedErr, ok := err.(*ewrap.Error); ok { + // Get a stack iterator for programmatic access + iterator := wrappedErr.GetStackIterator() + + for iterator.HasNext() { + frame := iterator.Next() + fmt.Printf("Function: %s\n", frame.Function) + fmt.Printf("File: %s:%d\n", frame.File, frame.Line) + fmt.Printf("PC: %x\n", frame.PC) + } + + // Or get all frames at once + frames := wrappedErr.GetStackFrames() + for _, frame := range frames { + // Process each frame... + } +} +``` + +### Custom Retry Logic with Extended RetryInfo + +Configure per-error retry strategies: + +```go +// Define custom retry logic +shouldRetry := func(err error, attempt int) bool { + if attempt >= 5 { + return false + } + + // Custom logic based on error type + if wrappedErr, ok := err.(*ewrap.Error); ok { + return wrappedErr.ErrorType() == ewrap.ErrorTypeNetwork + } + return false +} + +// Create error with custom retry configuration +err := ewrap.New("network timeout", + ewrap.WithRetryInfo(3, time.Second*2, shouldRetry)) + +// Use the retry information +if retryInfo := err.GetRetryInfo(); retryInfo != nil { + if retryInfo.ShouldRetry(err, currentAttempt) { + // Perform retry logic + } +} +``` + +### Observability Hooks and Monitoring + +Monitor error patterns and circuit breaker states: + +```go +// Set up observability hooks +observer := &MyObserver{ + metricsClient: metricsClient, + tracer: tracer, +} + +// Create circuit breaker with observability +cb := ewrap.NewCircuitBreaker("payment-service", 5, time.Minute*2, + ewrap.WithObserver(observer)) + +// The observer will receive notifications for: +// - Error frequency changes +// - Circuit breaker state transitions +// - Recovery suggestions triggered ``` ### Circuit Breaker Pattern @@ -167,9 +263,16 @@ type Logger interface { } ``` -Built-in adapters are provided for popular logging frameworks: +Built-in adapters are provided for popular logging frameworks including modern slog support: ```go +// Slog logger (Go 1.21+) - Recommended for new projects +slogLogger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + Level: slog.LevelDebug, +})) +err := ewrap.New("error occurred", + ewrap.WithLogger(adapters.NewSlogAdapter(slogLogger))) + // Zap logger zapLogger, _ := zap.NewProduction() err := ewrap.New("error occurred", @@ -184,37 +287,115 @@ 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))) +### Recovery Suggestions in Logging + +Recovery suggestions are now automatically included in log output: + +```go +err := ewrap.New("database connection failed", + ewrap.WithRecoverySuggestion("Check database connectivity and connection pool settings")) + +// When logged, includes recovery guidance for operations teams +err.Log() // Outputs recovery suggestion in structured format ``` ## Error Formatting -Convert errors to structured formats: +Convert errors to structured formats with proper timestamp formatting: ```go -// Convert to JSON +// Convert to JSON with proper timestamp formatting jsonStr, _ := err.ToJSON( ewrap.WithTimestampFormat(time.RFC3339), - ewrap.WithStackTrace(true)) + ewrap.WithStackTrace(true), + ewrap.WithRecoverySuggestion(true)) -// Convert to YAML +// Convert to YAML with custom formatting yamlStr, _ := err.ToYAML( + ewrap.WithTimestampFormat("2006-01-02T15:04:05Z07:00"), ewrap.WithStackTrace(true)) + +// Serialize entire error groups +pool := ewrap.NewErrorGroupPool(4) +eg := pool.Get() +eg.Add(err1) +eg.Add(err2) + +// Export all errors in the group +groupJSON, _ := eg.ToJSON(ewrap.WithTimestampFormat(time.RFC3339)) +``` + +### Modern Go Features Integration + +Leverage Go 1.25+ features for efficient operations: + +```go +// Efficient metadata copying using maps.Clone +originalErr := ewrap.New("base error").WithMetadata("key1", "value1") +clonedErr := originalErr.Clone() // Uses maps.Clone internally + +// Error group integration with errors.Join +eg := pool.Get() +eg.Add(err1, err2, err3) +standardErr := eg.Join() // Returns standard errors.Join result + +// Use with standard library error handling +if errors.Is(standardErr, expectedErr) { + // Handle specific error type +} ``` ## Performance Considerations -The package is designed with performance in mind: +The package is designed with performance in mind and leverages modern Go features: + +### Go 1.25+ Optimizations + +- Uses `maps.Clone` and `slices.Clone` for efficient copying operations +- Zero-allocation paths for error creation and wrapping in hot paths +- Optimized stack trace capture with intelligent filtering + +### Memory Management + +- Error groups use `sync.Pool` for efficient memory reuse +- Stack frame iterators provide lazy evaluation +- Minimal allocations during error metadata operations + +### Concurrency & Safety + +- Thread-safe operations with low lock contention +- Atomic operations for circuit breaker state management +- Lock-free observability hook notifications -- Error groups use sync.Pool for efficient memory usage -- Minimal allocations in hot paths -- Thread-safe operations with low contention -- Pre-allocated buffers for string operations +### Structured Operations + +- Pre-allocated buffers for JSON/YAML serialization - Efficient stack trace capture and filtering +- Optimized metadata storage and retrieval + +## Observability Features + +### Built-in Monitoring + +- Error frequency tracking and reporting +- Circuit breaker state transition monitoring +- Recovery suggestion effectiveness metrics + +### Integration Points + +```go +// Implement the Observer interface for custom monitoring +type Observer interface { + OnErrorCreated(err *Error, context ErrorContext) + OnCircuitBreakerStateChange(name string, from, to CircuitState) + OnRecoverySuggestionTriggered(suggestion string, context ErrorContext) +} + +// Register observers for monitoring +ewrap.RegisterGlobalObserver(myObserver) +``` ## Development Setup diff --git a/docs/docs/advanced/formatting.md b/docs/docs/advanced/formatting.md index 5c85796..765f38a 100644 --- a/docs/docs/advanced/formatting.md +++ b/docs/docs/advanced/formatting.md @@ -1,29 +1,31 @@ # Error Formatting -Error formatting in ewrap provides flexible ways to present errors in different formats and contexts. This capability is crucial for logging, debugging, API responses, and system integration. Let's explore how to effectively format errors to meet various needs in your application. +Error formatting in ewrap provides flexible ways to present errors in different formats and contexts with proper timestamp formatting and enhanced serialization capabilities. This capability is crucial for logging, debugging, API responses, and system integration. Let's explore how to effectively format errors to meet various needs in your application. ## Understanding Error Formatting When an error occurs in your system, you might need to present it in different ways depending on the context: -- As JSON for API responses +- As JSON for API responses with proper timestamp formatting - As YAML for configuration-related errors -- As structured text for logging +- As structured text for logging with recovery suggestions - As user-friendly messages for end users +- As serialized error groups for monitoring systems -ewrap provides formatting options to handle all these cases while maintaining the rich context and metadata associated with your errors. +ewrap provides comprehensive formatting options to handle all these cases while maintaining the rich context and metadata associated with your errors. -## JSON Formatting +## Enhanced JSON Formatting -JSON formatting is particularly useful for API responses and structured logging. Here's how to work with JSON formatting in ewrap: +JSON formatting now includes proper timestamp formatting and recovery suggestions: ```go func handleAPIError(w http.ResponseWriter, err error) { if wrappedErr, ok := err.(*ewrap.Error); ok { - // Convert error to JSON with full context + // Convert error to JSON with enhanced formatting jsonOutput, err := wrappedErr.ToJSON( - ewrap.WithTimestampFormat(time.RFC3339), + ewrap.WithTimestampFormat(time.RFC3339), // Proper timestamp formatting ewrap.WithStackTrace(true), + ewrap.WithRecoverySuggestion(true), // Include recovery guidance ) if err != nil { @@ -39,38 +41,87 @@ func handleAPIError(w http.ResponseWriter, err error) { } ``` -The resulting JSON might look like this: +### Enhanced JSON Structure + +The resulting JSON now includes proper timestamp formatting and recovery suggestions: ```json { "message": "failed to process user order", "timestamp": "2024-03-15T14:30:00Z", "type": "database", - "severity": "error", - "stack": "main.processOrder:/app/main.go:42\nmain.handleRequest:/app/main.go:28", + "severity": "critical", + "stack_trace": [ + { + "function": "main.processOrder", + "file": "/app/main.go", + "line": 42, + "pc": "0x4567890" + } + ], "metadata": { "user_id": "12345", - "order_id": "ORD-789", - "attempt": 1 + "order_id": "ord_789", + "retry_count": 3 }, - "cause": { - "message": "database connection timeout", - "type": "database", - "severity": "critical" - } + "recovery_suggestion": "Check database connectivity and retry with exponential backoff" } ``` -## YAML Formatting +## Timestamp Formatting Options + +### Standard Formats + +ewrap supports various timestamp formats: + +```go +// RFC3339 format (recommended for APIs and JSON) +jsonOutput, _ := err.ToJSON(ewrap.WithTimestampFormat(time.RFC3339)) +// Result: "2024-03-15T14:30:00Z" + +// RFC3339Nano for high precision +jsonOutput, _ := err.ToJSON(ewrap.WithTimestampFormat(time.RFC3339Nano)) +// Result: "2024-03-15T14:30:00.123456789Z" + +// Kitchen format for human-readable logs +jsonOutput, _ := err.ToJSON(ewrap.WithTimestampFormat(time.Kitchen)) +// Result: "2:30PM" + +// Custom format for specific requirements +jsonOutput, _ := err.ToJSON(ewrap.WithTimestampFormat("2006-01-02 15:04:05")) +// Result: "2024-03-15 14:30:00" +``` + +### Unix Timestamp Support -YAML formatting can be particularly useful for configuration-related errors or when you need a more human-readable format: +For systems integration requiring Unix timestamps: + +```go +// Unix timestamp (seconds since epoch) +jsonOutput, _ := err.ToJSON(ewrap.WithTimestampFormat("unix")) +// Result: "1710507000" + +// Unix timestamp with milliseconds +jsonOutput, _ := err.ToJSON(ewrap.WithTimestampFormat("unix_milli")) +// Result: "1710507000123" + +// Unix timestamp with microseconds +jsonOutput, _ := err.ToJSON(ewrap.WithTimestampFormat("unix_micro")) +// Result: "1710507000123456" +``` + +## YAML Formatting with Recovery Suggestions + +YAML formatting now supports recovery suggestions and enhanced metadata: ```go func logConfigurationError(err error, logger Logger) { if wrappedErr, ok := err.(*ewrap.Error); ok { - // Convert error to YAML for logging + // Convert error to YAML with recovery suggestions yamlOutput, err := wrappedErr.ToYAML( + ewrap.WithTimestampFormat(time.RFC3339), ewrap.WithStackTrace(true), + ewrap.WithRecoverySuggestion(true), ) if err != nil { @@ -83,24 +134,30 @@ func logConfigurationError(err error, logger Logger) { } ``` -The formatted YAML might look like this: +### Enhanced YAML Structure + +The formatted YAML now includes recovery suggestions: ```yaml message: failed to load configuration -timestamp: 2024-03-15T14:30:00Z +timestamp: "2024-03-15T14:30:00Z" type: configuration severity: critical -stack: | - main.loadConfig:/app/config.go:25 - main.initialize:/app/main.go:15 +stack_trace: + - function: main.loadConfig + file: /app/config.go + line: 25 + pc: "0x4567890" + - function: main.initialize + file: /app/main.go + line: 15 + pc: "0x4567891" metadata: config_file: /etc/myapp/config.yaml invalid_fields: - database.host - database.port -cause: - message: invalid port number - type: validation +recovery_suggestion: "Check configuration file format and validate required fields" ``` ## Custom Formatting diff --git a/docs/docs/features/observability.md b/docs/docs/features/observability.md new file mode 100644 index 0000000..2aaf4ca --- /dev/null +++ b/docs/docs/features/observability.md @@ -0,0 +1,360 @@ +# Observability and Monitoring + +ewrap provides comprehensive observability features that allow you to monitor error patterns, track system health, and gain insights into application behavior. These features are designed to integrate seamlessly with modern monitoring and alerting systems. + +## Observer Interface + +The Observer interface allows you to receive notifications about various error-related events in your application: + +```go +type Observer interface { + OnErrorCreated(err *Error, context ErrorContext) + OnCircuitBreakerStateChange(name string, from, to CircuitState) + OnRecoverySuggestionTriggered(suggestion string, context ErrorContext) +} +``` + +## Setting Up Observability + +### Global Observer Registration + +Register observers globally to monitor all error activity: + +```go +// Create a custom observer +type MetricsObserver struct { + metricsClient *prometheus.Client + logger *slog.Logger +} + +func (m *MetricsObserver) OnErrorCreated(err *Error, context ErrorContext) { + // Track error frequency by type + m.metricsClient.Counter("errors_total"). + WithLabelValues(string(err.ErrorType()), string(err.Severity())). + Inc() + + // Log structured error information + m.logger.Error("error created", + "type", err.ErrorType(), + "severity", err.Severity(), + "message", err.Error(), + "context", context) +} + +func (m *MetricsObserver) OnCircuitBreakerStateChange(name string, from, to CircuitState) { + // Track circuit breaker state transitions + m.metricsClient.Counter("circuit_breaker_state_changes"). + WithLabelValues(name, string(from), string(to)). + Inc() + + // Alert on circuit breaker openings + if to == CircuitStateOpen { + m.logger.Warn("circuit breaker opened", + "name", name, + "previous_state", from) + } +} + +func (m *MetricsObserver) OnRecoverySuggestionTriggered(suggestion string, context ErrorContext) { + // Track recovery suggestion effectiveness + m.metricsClient.Counter("recovery_suggestions_total"). + WithLabelValues(suggestion). + Inc() +} + +// Register the observer globally +ewrap.RegisterGlobalObserver(&MetricsObserver{ + metricsClient: prometheusClient, + logger: slogLogger, +}) +``` + +### Component-Specific Observers + +Attach observers to specific components: + +```go +// Create circuit breaker with observer +cb := ewrap.NewCircuitBreaker("payment-service", 5, time.Minute*2, + ewrap.WithObserver(&PaymentServiceObserver{ + alertManager: alertMgr, + metrics: metrics, + })) + +// Observer for specific circuit breaker +type PaymentServiceObserver struct { + alertManager *AlertManager + metrics *Metrics +} + +func (p *PaymentServiceObserver) OnCircuitBreakerStateChange(name string, from, to CircuitState) { + if to == CircuitStateOpen { + p.alertManager.SendAlert(&Alert{ + Severity: "critical", + Summary: fmt.Sprintf("Payment service circuit breaker opened"), + Details: map[string]string{ + "service": name, + "previous_state": string(from), + "current_state": string(to), + "timestamp": time.Now().Format(time.RFC3339), + }, + }) + } +} +``` + +## Error Pattern Analysis + +### Frequency Tracking + +Monitor error patterns to identify systemic issues: + +```go +type ErrorAnalyzer struct { + errorCounts map[string]int + errorWindows map[string][]time.Time + mutex sync.RWMutex +} + +func (ea *ErrorAnalyzer) OnErrorCreated(err *Error, context ErrorContext) { + ea.mutex.Lock() + defer ea.mutex.Unlock() + + errorKey := fmt.Sprintf("%s:%s", err.ErrorType(), err.Severity()) + + // Track error counts + ea.errorCounts[errorKey]++ + + // Track error timing for rate analysis + now := time.Now() + ea.errorWindows[errorKey] = append(ea.errorWindows[errorKey], now) + + // Clean old entries (keep last hour) + cutoff := now.Add(-time.Hour) + filtered := ea.errorWindows[errorKey][:0] + for _, timestamp := range ea.errorWindows[errorKey] { + if timestamp.After(cutoff) { + filtered = append(filtered, timestamp) + } + } + ea.errorWindows[errorKey] = filtered + + // Check for error spikes + if len(ea.errorWindows[errorKey]) > 100 { // More than 100 errors in last hour + ea.triggerErrorSpike(errorKey, len(ea.errorWindows[errorKey])) + } +} + +func (ea *ErrorAnalyzer) triggerErrorSpike(errorKey string, count int) { + log.Printf("ERROR SPIKE DETECTED: %s - %d errors in last hour", errorKey, count) +} +``` + +## Circuit Breaker Monitoring + +### State Transition Tracking + +Monitor circuit breaker health and performance: + +```go +type CircuitBreakerMonitor struct { + stateHistory map[string][]StateTransition + healthMetrics map[string]*HealthMetrics + mutex sync.RWMutex +} + +type StateTransition struct { + From CircuitState + To CircuitState + Timestamp time.Time +} + +type HealthMetrics struct { + TotalRequests int64 + SuccessfulReqs int64 + FailedRequests int64 + OpenDuration time.Duration + LastStateChange time.Time +} + +func (cbm *CircuitBreakerMonitor) OnCircuitBreakerStateChange(name string, from, to CircuitState) { + cbm.mutex.Lock() + defer cbm.mutex.Unlock() + + // Record state transition + transition := StateTransition{ + From: from, + To: to, + Timestamp: time.Now(), + } + + cbm.stateHistory[name] = append(cbm.stateHistory[name], transition) + + // Update health metrics + if cbm.healthMetrics[name] == nil { + cbm.healthMetrics[name] = &HealthMetrics{} + } + + metrics := cbm.healthMetrics[name] + metrics.LastStateChange = transition.Timestamp + + // Track open duration + if from == CircuitStateOpen && to == CircuitStateHalfOpen { + // Find when it opened + for i := len(cbm.stateHistory[name]) - 2; i >= 0; i-- { + if cbm.stateHistory[name][i].To == CircuitStateOpen { + openDuration := transition.Timestamp.Sub(cbm.stateHistory[name][i].Timestamp) + metrics.OpenDuration += openDuration + break + } + } + } + + // Generate health report + cbm.generateHealthReport(name, metrics) +} + +func (cbm *CircuitBreakerMonitor) generateHealthReport(name string, metrics *HealthMetrics) { + if metrics.TotalRequests > 0 { + successRate := float64(metrics.SuccessfulReqs) / float64(metrics.TotalRequests) * 100 + + log.Printf("Circuit Breaker Health Report - %s: Success Rate: %.2f%%, Open Duration: %v", + name, successRate, metrics.OpenDuration) + } +} +``` + +## Recovery Suggestion Tracking + +Monitor the effectiveness of recovery suggestions: + +```go +type RecoveryTracker struct { + suggestions map[string]*SuggestionMetrics + mutex sync.RWMutex +} + +type SuggestionMetrics struct { + Count int + FirstSeen time.Time + LastSeen time.Time + Contexts []ErrorContext +} + +func (rt *RecoveryTracker) OnRecoverySuggestionTriggered(suggestion string, context ErrorContext) { + rt.mutex.Lock() + defer rt.mutex.Unlock() + + if rt.suggestions[suggestion] == nil { + rt.suggestions[suggestion] = &SuggestionMetrics{ + FirstSeen: time.Now(), + Contexts: make([]ErrorContext, 0), + } + } + + metrics := rt.suggestions[suggestion] + metrics.Count++ + metrics.LastSeen = time.Now() + metrics.Contexts = append(metrics.Contexts, context) + + // Generate actionable insights + if metrics.Count > 10 { + rt.generateRecoveryInsights(suggestion, metrics) + } +} + +func (rt *RecoveryTracker) generateRecoveryInsights(suggestion string, metrics *SuggestionMetrics) { + frequency := float64(metrics.Count) / time.Since(metrics.FirstSeen).Hours() + + log.Printf("Recovery Suggestion Analysis - '%s': Count: %d, Frequency: %.2f/hour, Duration: %v", + suggestion, metrics.Count, frequency, time.Since(metrics.FirstSeen)) + + // Analyze context patterns + contextTypes := make(map[string]int) + for _, ctx := range metrics.Contexts { + contextTypes[string(ctx.ErrorType)]++ + } + + for contextType, count := range contextTypes { + percentage := float64(count) / float64(len(metrics.Contexts)) * 100 + log.Printf(" Context Type '%s': %d occurrences (%.1f%%)", contextType, count, percentage) + } +} +``` + +## Integration with Monitoring Systems + +### Prometheus Integration + +```go +import "github.com/prometheus/client_golang/prometheus" + +type PrometheusObserver struct { + errorCounter *prometheus.CounterVec + circuitBreakerGauge *prometheus.GaugeVec + recoveryCounter *prometheus.CounterVec +} + +func NewPrometheusObserver() *PrometheusObserver { + return &PrometheusObserver{ + errorCounter: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "ewrap_errors_total", + Help: "Total number of errors created", + }, + []string{"type", "severity"}, + ), + circuitBreakerGauge: prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "ewrap_circuit_breaker_state", + Help: "Current circuit breaker state (0=closed, 1=open, 2=half-open)", + }, + []string{"name"}, + ), + recoveryCounter: prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "ewrap_recovery_suggestions_total", + Help: "Total number of recovery suggestions triggered", + }, + []string{"suggestion_type"}, + ), + } +} + +func (p *PrometheusObserver) OnCircuitBreakerStateChange(name string, from, to CircuitState) { + var stateValue float64 + switch to { + case CircuitStateClosed: + stateValue = 0 + case CircuitStateOpen: + stateValue = 1 + case CircuitStateHalfOpen: + stateValue = 2 + } + + p.circuitBreakerGauge.WithLabelValues(name).Set(stateValue) +} +``` + +## Best Practices + +### Performance Considerations + +1. **Lightweight Observers**: Keep observer implementations fast to avoid impacting error handling performance +2. **Async Processing**: Use goroutines for expensive operations in observers +3. **Buffered Channels**: Use buffered channels for high-throughput scenarios + +### Monitoring Strategy + +1. **Error Rate Monitoring**: Track error rates by type and severity +2. **Circuit Breaker Health**: Monitor state transitions and success rates +3. **Recovery Effectiveness**: Analyze which recovery suggestions are most common +4. **Performance Impact**: Monitor the overhead of observability features + +### Alert Configuration + +1. **Error Spikes**: Alert on sudden increases in error rates +2. **Circuit Breaker Openings**: Immediate alerts when services become unavailable +3. **Recovery Pattern Changes**: Notify when new types of errors appear frequently + +The observability features in ewrap provide deep insights into your application's error patterns and system health, enabling proactive monitoring and faster incident response. diff --git a/docs/docs/features/serialization.md b/docs/docs/features/serialization.md new file mode 100644 index 0000000..0ff966d --- /dev/null +++ b/docs/docs/features/serialization.md @@ -0,0 +1,394 @@ +# Serialization and Error Groups + +ewrap provides comprehensive serialization capabilities for both individual errors and error groups, enabling structured export for monitoring systems, APIs, and debugging purposes. The serialization system supports multiple formats and is optimized for performance. + +## Error Group Serialization + +### Basic Serialization + +Export entire error groups to structured formats: + +```go +// Create an error group with multiple errors +pool := ewrap.NewErrorGroupPool(4) +eg := pool.Get() +defer eg.Release() + +// Add various errors +eg.Add(ewrap.New("database connection failed", + ewrap.WithErrorType(ewrap.ErrorTypeDatabase), + ewrap.WithSeverity(ewrap.SeverityCritical))) + +eg.Add(ewrap.New("validation error", + ewrap.WithErrorType(ewrap.ErrorTypeValidation), + ewrap.WithSeverity(ewrap.SeverityError))) + +// Serialize to JSON +jsonOutput, err := eg.ToJSON( + ewrap.WithTimestampFormat(time.RFC3339), + ewrap.WithStackTrace(true), + ewrap.WithRecoverySuggestion(true)) + +if err != nil { + log.Printf("Serialization failed: %v", err) + return +} + +fmt.Println(jsonOutput) +``` + +### JSON Output Structure + +The JSON serialization produces a structured format: + +```json +{ + "error_group": { + "timestamp": "2024-03-15T14:30:00Z", + "total_errors": 2, + "errors": [ + { + "message": "database connection failed", + "type": "database", + "severity": "critical", + "timestamp": "2024-03-15T14:30:00Z", + "metadata": {}, + "stack_trace": [ + { + "function": "main.connectDatabase", + "file": "/app/main.go", + "line": 42, + "pc": "0x4567890" + } + ], + "recovery_suggestion": "Check database connectivity and connection pool settings" + }, + { + "message": "validation error", + "type": "validation", + "severity": "error", + "timestamp": "2024-03-15T14:30:00Z", + "metadata": {}, + "stack_trace": [] + } + ] + } +} +``` + +### YAML Serialization + +Export error groups to YAML format: + +```go +yamlOutput, err := eg.ToYAML( + ewrap.WithTimestampFormat("2006-01-02T15:04:05Z07:00"), + ewrap.WithStackTrace(false)) // Exclude stack traces for cleaner output + +if err != nil { + log.Printf("YAML serialization failed: %v", err) + return +} + +fmt.Println(yamlOutput) +``` + +### YAML Output Structure + +```yaml +error_group: + timestamp: "2024-03-15T14:30:00Z" + total_errors: 2 + errors: + - message: "database connection failed" + type: "database" + severity: "critical" + timestamp: "2024-03-15T14:30:00Z" + metadata: {} + recovery_suggestion: "Check database connectivity and connection pool settings" + - message: "validation error" + type: "validation" + severity: "error" + timestamp: "2024-03-15T14:30:00Z" + metadata: {} +``` + +## Individual Error Serialization + +### Enhanced JSON Serialization + +Individual errors can be serialized with full context: + +```go +err := ewrap.New("payment processing failed", + ewrap.WithErrorType(ewrap.ErrorTypeExternal), + ewrap.WithSeverity(ewrap.SeverityCritical), + ewrap.WithRecoverySuggestion("Retry with exponential backoff or contact payment provider")). + WithMetadata("payment_id", "pay_12345"). + WithMetadata("amount", 99.99). + WithMetadata("currency", "USD") + +jsonOutput, serErr := err.ToJSON( + ewrap.WithTimestampFormat(time.RFC3339), + ewrap.WithStackTrace(true), + ewrap.WithRecoverySuggestion(true)) + +if serErr != nil { + log.Printf("Error serialization failed: %v", serErr) + return +} +``` + +### Custom Serialization Options + +#### Timestamp Formatting + +Configure timestamp formats for different use cases: + +```go +// RFC3339 format (recommended for APIs) +jsonOutput, _ := err.ToJSON(ewrap.WithTimestampFormat(time.RFC3339)) + +// Custom format for logs +jsonOutput, _ := err.ToJSON(ewrap.WithTimestampFormat("2006-01-02 15:04:05")) + +// Unix timestamp for systems integration +jsonOutput, _ := err.ToJSON(ewrap.WithTimestampFormat("unix")) +``` + +#### Stack Trace Control + +Control stack trace inclusion in serialized output: + +```go +// Include full stack traces (for debugging) +jsonOutput, _ := err.ToJSON(ewrap.WithStackTrace(true)) + +// Exclude stack traces (for production logs) +jsonOutput, _ := err.ToJSON(ewrap.WithStackTrace(false)) + +// Include only application frames +jsonOutput, _ := err.ToJSON( + ewrap.WithStackTrace(true), + ewrap.WithStackFilter(func(frame StackFrame) bool { + return strings.Contains(frame.File, "/myapp/") && + !strings.Contains(frame.File, "/vendor/") + })) +``` + +#### Recovery Suggestions + +Control recovery suggestion inclusion: + +```go +// Include recovery suggestions (for operational use) +jsonOutput, _ := err.ToJSON(ewrap.WithRecoverySuggestion(true)) + +// Exclude recovery suggestions (for end-user APIs) +jsonOutput, _ := err.ToJSON(ewrap.WithRecoverySuggestion(false)) +``` + +## Integration with errors.Join + +### Standard Library Compatibility + +ewrap error groups integrate seamlessly with Go's standard `errors.Join`: + +```go +// Create error group +eg := pool.Get() +eg.Add(err1) +eg.Add(err2) +eg.Add(err3) + +// Get standard errors.Join result +standardErr := eg.Join() + +// Use with standard library functions +if errors.Is(standardErr, expectedErr) { + // Handle specific error +} + +var targetErr *MyCustomError +if errors.As(standardErr, &targetErr) { + // Handle custom error type +} + +// The joined error maintains ewrap capabilities +if ewrapGroup, ok := standardErr.(*ewrap.ErrorGroup); ok { + // Can still serialize the group + jsonOutput, _ := ewrapGroup.ToJSON(ewrap.WithTimestampFormat(time.RFC3339)) +} +``` + +### Preserving ewrap Features + +When using `errors.Join`, ewrap features are preserved: + +```go +eg := pool.Get() +eg.Add(ewrap.New("error 1", ewrap.WithErrorType(ewrap.ErrorTypeDatabase))) +eg.Add(ewrap.New("error 2", ewrap.WithErrorType(ewrap.ErrorTypeNetwork))) + +// Join preserves individual error metadata +joinedErr := eg.Join() + +// Individual errors maintain their ewrap features +fmt.Printf("Joined error: %v\n", joinedErr) + +// Can still access individual errors +for _, err := range eg.Errors() { + if ewrapErr, ok := err.(*ewrap.Error); ok { + fmt.Printf("Error type: %s, Severity: %s\n", + ewrapErr.ErrorType(), ewrapErr.Severity()) + } +} +``` + +## Performance Optimizations + +### Go 1.25+ Features + +ewrap leverages modern Go features for efficient serialization: + +```go +// Uses maps.Clone for efficient metadata copying +func (err *Error) Clone() *Error { + cloned := &Error{ + message: err.message, + errorType: err.errorType, + severity: err.severity, + timestamp: err.timestamp, + stack: err.stack, + metadata: maps.Clone(err.metadata), // Efficient copying + } + return cloned +} + +// Uses slices.Clone for error group copying +func (eg *ErrorGroup) Clone() *ErrorGroup { + return &ErrorGroup{ + errors: slices.Clone(eg.errors), // Efficient slice copying + mutex: sync.RWMutex{}, + } +} +``` + +### Memory Management + +Serialization is optimized for memory efficiency: + +```go +// Pre-allocated buffers for JSON marshaling +type SerializationBuffer struct { + jsonBuffer bytes.Buffer + yamlBuffer bytes.Buffer +} + +// Reuse buffers across serialization operations +func (sb *SerializationBuffer) SerializeToJSON(err *Error) (string, error) { + sb.jsonBuffer.Reset() // Reuse existing buffer + + encoder := json.NewEncoder(&sb.jsonBuffer) + if err := encoder.Encode(err); err != nil { + return "", err + } + + return sb.jsonBuffer.String(), nil +} +``` + +## API Integration Examples + +### REST API Error Responses + +```go +func handleAPIError(w http.ResponseWriter, r *http.Request, err error) { + if ewrapErr, ok := err.(*ewrap.Error); ok { + jsonResponse, serErr := ewrapErr.ToJSON( + ewrap.WithTimestampFormat(time.RFC3339), + ewrap.WithStackTrace(false), // Don't expose stack traces in API + ewrap.WithRecoverySuggestion(false)) // Keep suggestions internal + + if serErr != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + + // Set appropriate status code based on error type + statusCode := getStatusCodeForError(ewrapErr) + w.WriteHeader(statusCode) + + w.Write([]byte(jsonResponse)) + return + } + + // Fallback for non-ewrap errors + http.Error(w, err.Error(), http.StatusInternalServerError) +} + +func getStatusCodeForError(err *ewrap.Error) int { + switch err.ErrorType() { + case ewrap.ErrorTypeValidation: + return http.StatusBadRequest + case ewrap.ErrorTypeNotFound: + return http.StatusNotFound + case ewrap.ErrorTypePermission: + return http.StatusForbidden + case ewrap.ErrorTypeDatabase, ewrap.ErrorTypeNetwork: + return http.StatusInternalServerError + default: + return http.StatusInternalServerError + } +} +``` + +### Structured Logging Integration + +```go +func logErrorGroup(logger *slog.Logger, eg *ewrap.ErrorGroup) { + jsonOutput, err := eg.ToJSON( + ewrap.WithTimestampFormat(time.RFC3339), + ewrap.WithStackTrace(true), + ewrap.WithRecoverySuggestion(true)) + + if err != nil { + logger.Error("failed to serialize error group", "error", err) + return + } + + logger.Error("error group occurred", + "error_count", len(eg.Errors()), + "errors", jsonOutput) +} +``` + +### Monitoring System Integration + +```go +func sendToMonitoring(eg *ewrap.ErrorGroup, metricsClient *prometheus.Client) { + for _, err := range eg.Errors() { + if ewrapErr, ok := err.(*ewrap.Error); ok { + // Send metrics + metricsClient.Counter("errors_total"). + WithLabelValues( + string(ewrapErr.ErrorType()), + string(ewrapErr.Severity())). + Inc() + + // Send structured data to monitoring + jsonData, serErr := ewrapErr.ToJSON( + ewrap.WithTimestampFormat(time.RFC3339), + ewrap.WithStackTrace(false)) + + if serErr == nil { + metricsClient.SendCustomMetric("error_details", jsonData) + } + } + } +} +``` + +The serialization features in ewrap provide comprehensive support for structured error export, enabling seamless integration with monitoring systems, APIs, and debugging workflows while maintaining excellent performance characteristics. diff --git a/docs/docs/features/stack-traces.md b/docs/docs/features/stack-traces.md index 2181393..244f74b 100644 --- a/docs/docs/features/stack-traces.md +++ b/docs/docs/features/stack-traces.md @@ -1,6 +1,6 @@ # Stack Traces -Stack traces are crucial for understanding where and why errors occur in your application. In ewrap, stack traces are automatically captured and enhanced to provide meaningful debugging information while maintaining performance. +Stack traces are crucial for understanding where and why errors occur in your application. In ewrap, stack traces are automatically captured and enhanced to provide meaningful debugging information while maintaining performance. The latest version includes programmatic stack frame inspection through iterators and structured access. ## Understanding Stack Traces @@ -25,27 +25,118 @@ The captured stack trace includes: - Function names - File names - Line numbers -- Package information +- Program counter (PC) values However, ewrap goes beyond simple capture by: 1. Filtering out runtime implementation details 2. Maintaining stack traces through error wrapping 3. Providing formatted output options +4. Offering programmatic access through iterators -## Accessing Stack Traces +## Programmatic Stack Frame Access -You can access the stack trace of an error in several ways: +### Using Stack Iterators + +The new stack iterator provides efficient, lazy access to stack frames: + +```go +func analyzeError(err error) { + if wrappedErr, ok := err.(*ewrap.Error); ok { + iterator := wrappedErr.GetStackIterator() + + for iterator.HasNext() { + frame := iterator.Next() + + fmt.Printf("Function: %s\n", frame.Function) + fmt.Printf("File: %s:%d\n", frame.File, frame.Line) + fmt.Printf("PC: %x\n", frame.PC) + + // Custom logic based on frame information + if strings.Contains(frame.Function, "database") { + handleDatabaseFrame(frame) + } + } + } +} +``` + +### Accessing All Frames + +Get all stack frames at once for batch processing: + +```go +func generateErrorReport(err error) ErrorReport { + if wrappedErr, ok := err.(*ewrap.Error); ok { + frames := wrappedErr.GetStackFrames() + + return ErrorReport{ + Message: wrappedErr.Error(), + StackFrames: frames, + Timestamp: time.Now(), + } + } + return ErrorReport{} +} +``` + +### Stack Frame Structure + +Each stack frame provides detailed information: + +```go +type StackFrame struct { + Function string `json:"function" yaml:"function"` // Fully qualified function name + File string `json:"file" yaml:"file"` // Source file path + Line int `json:"line" yaml:"line"` // Line number + PC uintptr `json:"pc" yaml:"pc"` // Program counter +} +``` + +## Iterator Operations + +### Navigation and Control + +```go +iterator := wrappedErr.GetStackIterator() + +// Check if more frames are available +if iterator.HasNext() { + frame := iterator.Next() + // Process frame +} + +// Reset iterator to beginning +iterator.Reset() + +// Get remaining frames from current position +remainingFrames := iterator.Frames() + +// Get all frames regardless of current position +allFrames := iterator.AllFrames() +``` + +### Filtering and Processing ```go -func handleError(err error) { +func findApplicationFrames(err error) []StackFrame { + var appFrames []StackFrame + if wrappedErr, ok := err.(*ewrap.Error); ok { - // Get the full stack trace as a string - stackTrace := wrappedErr.Stack() + iterator := wrappedErr.GetStackIterator() + + for iterator.HasNext() { + frame := iterator.Next() - fmt.Printf("Error occurred: %v\n", err) - fmt.Printf("Stack trace:\n%s", stackTrace) + // Filter for application-specific frames + if strings.Contains(frame.File, "/myapp/") && + !strings.Contains(frame.File, "/vendor/") { + appFrames = append(appFrames, *frame) + } + } } + + return appFrames } ``` diff --git a/docs/docs/index.md b/docs/docs/index.md index fcda658..d7e73dc 100644 --- a/docs/docs/index.md +++ b/docs/docs/index.md @@ -1,59 +1,87 @@ # ewrap Documentation -Welcome to the documentation for `ewrap`, a sophisticated error handling package for Go applications that provides comprehensive error management capabilities with a focus on performance and developer experience. +Welcome to the documentation for `ewrap`, a sophisticated, modern error handling library for Go applications that provides comprehensive error management with advanced features, observability hooks, and seamless integration with Go 1.25+ features. ## Overview -ewrap is designed to make error handling in Go applications more robust, informative, and maintainable. It provides a rich set of features while maintaining excellent performance characteristics through careful optimization and efficient memory management. +ewrap is designed to make error handling in Go applications more robust, informative, and maintainable. It provides a rich set of features while maintaining excellent performance characteristics through careful optimization, efficient memory management, and modern Go language features. ### Key Features -- **Stack Traces**: Automatically capture and filter stack traces for meaningful error debugging -- **Error Wrapping**: Maintain error chains while preserving context -- **Metadata Attachment**: Attach and manage arbitrary key-value pairs to errors -- **Logging Integration**: Flexible logger interface supporting major logging frameworks -- **Error Categorization**: Built-in error types and severity levels -- **Circuit Breaker Pattern**: Protect your systems from cascading failures -- **Efficient Error Grouping**: Pool-based error group management -- **Context Preservation**: Rich error context preservation -- **Thread-Safe Operations**: Safe for concurrent use -- **Format Options**: JSON and YAML output support +- **Advanced Stack Traces**: Programmatic stack frame inspection with iterators and structured access +- **Smart Error Wrapping**: Maintains error chains with unified context handling and metadata preservation +- **Modern Logging Integration**: Support for slog (Go 1.21+), logrus, zap, zerolog with structured output +- **Observability Hooks**: Built-in metrics and tracing for error frequencies and circuit-breaker states +- **Go 1.25+ Optimizations**: Uses `maps.Clone` and `slices.Clone` for efficient copying operations +- **Pool-based Error Groups**: Memory-efficient error aggregation with `errors.Join` compatibility +- **Circuit Breaker Pattern**: Protect systems from cascading failures with state transition monitoring +- **Custom Retry Logic**: Configurable per-error retry strategies with `RetryInfo` extension +- **Recovery Guidance**: Integrated recovery suggestions in error output and logging +- **Structured Serialization**: JSON/YAML export with full error group serialization +- **Thread-Safe Operations**: Zero-allocation hot paths with minimal contention +- **Type-Safe Metadata**: Optional generics support for strongly typed error contexts ## Quick Example -Here's a quick example of how ewrap can be used in your application: +Here's a comprehensive example showcasing the modern features of ewrap: ```go func processOrder(ctx context.Context, orderID string) error { - // Get an error group from the pool + // Set up observability + observer := &MyObserver{metricsClient: metrics, tracer: trace} + + // Get an error group from the pool with errors.Join support pool := ewrap.NewErrorGroupPool(4) eg := pool.Get() defer eg.Release() - // Create a circuit breaker - cb := ewrap.NewCircuitBreaker("database", 3, time.Minute) + // Create a circuit breaker with observability hooks + cb := ewrap.NewCircuitBreaker("payment-service", 5, time.Minute*2, + ewrap.WithObserver(observer)) - // Add validation errors to the group + // Add validation errors with recovery suggestions if err := validateOrder(orderID); err != nil { eg.Add(ewrap.Wrap(err, "invalid order", - ewrap.WithContext(ctx), - ewrap.WithErrorType(ewrap.ErrorTypeValidation), - ewrap.WithLogger(logger))) + ewrap.WithContext(ctx, ewrap.ErrorTypeValidation, ewrap.SeverityError), + ewrap.WithRecoverySuggestion("Validate order format and required fields"), + ewrap.WithLogger(slogLogger))) + } + + // Handle database operations with custom retry logic + shouldRetry := func(err error, attempt int) bool { + return attempt < 3 && ewrap.IsType(err, ewrap.ErrorTypeNetwork) } - // Handle database operations with circuit breaker if !eg.HasErrors() && cb.CanExecute() { if err := saveToDatabase(orderID); err != nil { cb.RecordFailure() - return ewrap.Wrap(err, "database operation failed", - ewrap.WithContext(ctx), - ewrap.WithErrorType(ewrap.ErrorTypeDatabase), - ewrap.WithRetry(3, time.Second*5)) + dbErr := ewrap.Wrap(err, "database operation failed", + ewrap.WithContext(ctx, ewrap.ErrorTypeDatabase, ewrap.SeverityCritical), + ewrap.WithRetryInfo(3, time.Second*5, shouldRetry), + ewrap.WithRecoverySuggestion("Check database connectivity and connection pool")) + + // Inspect stack frames programmatically + if iterator := dbErr.GetStackIterator(); iterator.HasNext() { + frame := iterator.Next() + // Custom handling based on stack frame information + handleCriticalFrame(frame) + } + + return dbErr } cb.RecordSuccess() } - return eg.Error() + // Use errors.Join compatibility for standard library integration + if err := eg.Join(); err != nil { + // Serialize the entire error group for structured logging + if jsonOutput, serErr := eg.ToJSON(ewrap.WithTimestampFormat(time.RFC3339)); serErr == nil { + structuredLogger.Error("order processing failed", "errors", jsonOutput) + } + return err + } + + return nil } ``` @@ -63,13 +91,26 @@ To start using ewrap in your project, visit the [Installation](getting-started/i ## Why ewrap? -ewrap was created to address common challenges in error handling: +ewrap was created to address common challenges in modern Go error handling: + +### Traditional Challenges Solved -1. **Lack of Context**: Traditional error handling often loses important context -2. **Performance Overhead**: Many error handling libraries introduce significant overhead -3. **Memory Management**: Poor memory management in error handling can lead to increased GC pressure -4. **Inconsistent Logging**: Different parts of an application often handle error logging differently -5. **Missing Stack Traces**: Getting meaningful stack traces can be challenging +1. **Context Loss**: Traditional error handling often loses important context during error propagation +2. **Performance Overhead**: Many error handling libraries introduce significant memory and CPU overhead +3. **Memory Management**: Poor memory management in error handling leads to increased GC pressure +4. **Inconsistent Logging**: Different parts of applications handle error logging differently +5. **Missing Stack Traces**: Getting meaningful, filterable stack traces is challenging 6. **Circuit Breaking**: Protecting systems from cascading failures requires complex implementation +### Modern Go Challenges Addressed + +1. **Go 1.25+ Feature Integration**: Lack of libraries leveraging modern Go performance features +2. **Observability Gaps**: Missing built-in support for metrics and tracing in error handling +3. **Recovery Guidance**: Errors without actionable remediation suggestions +4. **Type Safety**: Metadata handling without compile-time guarantees +5. **Standard Library Integration**: Poor integration with `errors.Join` and modern error patterns +6. **Serialization Complexity**: Difficulty in structured error export for monitoring systems + +ewrap provides solutions to all these challenges while maintaining backward compatibility and excellent performance characteristics. + ewrap solves these challenges while maintaining excellent performance characteristics and providing a clean, intuitive API.