diff --git a/README.md b/README.md index 34746d5..f4f187b 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,9 @@ import ( var ErrUserNotFound errific.Err = "user not found" func main() { + // Configure pretty JSON output for readability + errific.Configure(errific.OutputJSONPretty) + // Return an error with context err := GetUser("user-123") fmt.Println(err) @@ -36,15 +39,23 @@ func GetUser(userID string) error { ``` **Output:** -``` -user not found [main.go:20.GetUser] +```json +{ + "error": "user not found", + "code": "USER_404", + "caller": "main.go:27.GetUser", + "context": { + "source": "database", + "user_id": "user-123" + } +} ``` The error includes: - ✅ Automatic caller information (`main.go:20.GetUser`) -- ✅ Error code (`USER_404`) -- ✅ Structured context (user_id, source) -- ✅ JSON serializable for logging +- ✅ Error code (`USER_404`) visible in output +- ✅ Structured context (user_id, source) visible in output +- ✅ JSON output by default for structured logging ## ✨ Features @@ -133,6 +144,78 @@ jsonBytes, _ := json.Marshal(err) log.Info(string(jsonBytes)) ``` +### 🎨 Output Formats & Verbosity + +Errific supports multiple output formats and verbosity levels. **By default, errors output as JSON with all metadata visible**. + +```go +// Default: JSON format with full verbosity (shows all metadata) +errific.Configure() // or Configure(OutputJSON, VerbosityFull) + +err := ErrUserNotFound. + WithCode("USER_404"). + WithContext(errific.Context{"user_id": "user-123"}) + +fmt.Println(err) +// Output: {"error":"user not found","code":"USER_404","caller":"main.go:20","context":{"user_id":"user-123"}} + +// JSON Pretty format (indented JSON for docs/debugging) +errific.Configure(OutputJSONPretty) +fmt.Println(err) +// Output: +// { +// "error": "user not found", +// "code": "USER_404", +// "caller": "main.go:20", +// "context": { +// "user_id": "user-123" +// } +// } + +// Pretty format (multi-line, human-readable text) +errific.Configure(OutputPretty) +fmt.Println(err) +// Output: +// user not found [main.go:20.GetUser] +// code: USER_404 +// context: map[user_id:user-123] + +// Compact format (single-line key=value) +errific.Configure(OutputCompact) +fmt.Println(err) +// Output: user not found [main.go:20] code=USER_404 user_id=user-123 + +// Minimal verbosity (only message + caller, useful for simple logging) +errific.Configure(VerbosityMinimal) +fmt.Println(err) +// Output (JSON): {"error":"user not found","caller":"main.go:20"} + +// Standard verbosity (message + caller + code + category + context) +errific.Configure(VerbosityStandard) +fmt.Println(err) +// Output (JSON): {"error":"user not found","code":"USER_404","caller":"main.go:20","context":{"user_id":"user-123"}} + +// Custom verbosity (show only specific fields) +errific.Configure(VerbosityFull, HideContext, HideMCPData) +fmt.Println(err) +// Output (JSON): {"error":"user not found","code":"USER_404","caller":"main.go:20","http_status":404} +``` + +**Available output formats:** +- `OutputJSON` (default) - Compact JSON for structured logging +- `OutputJSONPretty` - Indented JSON for documentation and debugging +- `OutputPretty` - Multi-line, human-readable text +- `OutputCompact` - Single-line key=value pairs + +**Available verbosity levels:** +- `VerbosityFull` (default) - Show all non-empty fields +- `VerbosityStandard` - Show code, category, context +- `VerbosityMinimal` - Show only message and caller +- `VerbosityCustom` - Use with `Show*`/`Hide*` flags for granular control + +**Granular field control:** +`HideCode`, `HideCategory`, `HideContext`, `HideHTTPStatus`, `HideRetryMetadata`, `HideMCPData`, `HideTags`, `HideLabels`, `HideTimestamps` + ### JSON Output ```json diff --git a/conf.go b/conf.go index 24f6132..01e6120 100644 --- a/conf.go +++ b/conf.go @@ -21,6 +21,19 @@ func Configure(opts ...Option) { c.withStack = false c.trimPrefixes = nil c.trimCWD = false + c.outputFormat = OutputJSON + c.verbosity = VerbosityFull + + // Default field visibility (used when verbosity is VerbosityFull or VerbosityCustom) + c.showCode = true + c.showCategory = true + c.showContext = true + c.showHTTPStatus = true + c.showRetryMetadata = true + c.showMCPData = true + c.showTags = true + c.showLabels = true + c.showTimestamps = true for _, opt := range opts { switch o := opt.(type) { @@ -38,6 +51,74 @@ func Configure(opts ...Option) { case trimCWDOption: c.trimCWD = o + + case outputFormatOption: + c.outputFormat = o + + case verbosityOption: + c.verbosity = o + // Set field visibility based on verbosity level + switch o { + case VerbosityMinimal: + c.showCode = false + c.showCategory = false + c.showContext = false + c.showHTTPStatus = false + c.showRetryMetadata = false + c.showMCPData = false + c.showTags = false + c.showLabels = false + c.showTimestamps = false + + case VerbosityStandard: + c.showCode = true + c.showCategory = true + c.showContext = true + c.showHTTPStatus = false + c.showRetryMetadata = false + c.showMCPData = false + c.showTags = false + c.showLabels = false + c.showTimestamps = false + + case VerbosityFull: + c.showCode = true + c.showCategory = true + c.showContext = true + c.showHTTPStatus = true + c.showRetryMetadata = true + c.showMCPData = true + c.showTags = true + c.showLabels = true + c.showTimestamps = true + } + + case fieldVisibilityOption: + // When using field visibility options, automatically switch to VerbosityCustom + if c.verbosity != VerbosityCustom { + c.verbosity = VerbosityCustom + } + // Apply the specific field visibility setting + switch o.field { + case "code": + c.showCode = o.show + case "category": + c.showCategory = o.show + case "context": + c.showContext = o.show + case "http_status": + c.showHTTPStatus = o.show + case "retry_metadata": + c.showRetryMetadata = o.show + case "mcp_data": + c.showMCPData = o.show + case "tags": + c.showTags = o.show + case "labels": + c.showLabels = o.show + case "timestamps": + c.showTimestamps = o.show + } } } @@ -70,6 +151,22 @@ var ( // TrimCWD will trim the current working directory from filenames. // Default is false. trimCWD trimCWDOption + // Output format: Pretty, JSON, or Compact. + // Default is Pretty. + outputFormat outputFormatOption + // Verbosity controls which fields are shown in Error() output. + // Default is VerbosityFull (show all non-empty fields). + verbosity verbosityOption + // Field visibility flags (used when verbosity is VerbosityCustom) + showCode bool + showCategory bool + showContext bool + showHTTPStatus bool + showRetryMetadata bool + showMCPData bool + showTags bool + showLabels bool + showTimestamps bool } cMu sync.RWMutex ) @@ -139,6 +236,144 @@ type Option interface { ErrificOption() } +// outputFormatOption controls the format of error string output. +type outputFormatOption int + +func (outputFormatOption) ErrificOption() {} + +const ( + // OutputPretty formats errors as human-readable multi-line text with all metadata. + // + // Example: + // user not found [main.go:20.GetUser] + // code: USER_404 + // context: {user_id: user-123, source: database} + // http_status: 400 + OutputPretty outputFormatOption = iota + + // OutputJSON formats errors as compact JSON. + // This is the default. + // Useful for structured logging and machine processing. + // + // Example: + // {"error":"user not found","caller":"main.go:20","code":"USER_404",...} + OutputJSON + + // OutputJSONPretty formats errors as indented JSON. + // Useful for documentation, debugging, and human-readable JSON output. + // + // Example: + // { + // "error": "user not found", + // "code": "USER_404", + // "caller": "main.go:20" + // } + OutputJSONPretty + + // OutputCompact formats errors as single-line text with key=value pairs. + // Useful for log aggregation systems. + // + // Example: + // user not found [main.go:20] code=USER_404 user_id=user-123 http_status=400 + OutputCompact +) + +// verbosityOption controls which fields are included in Error() output. +type verbosityOption int + +func (verbosityOption) ErrificOption() {} + +const ( + // VerbosityMinimal shows only the error message and caller. + // + // Example: + // user not found [main.go:20.GetUser] + VerbosityMinimal verbosityOption = iota + + // VerbosityStandard shows message, caller, code, category, and context. + // Good balance for most applications. + // + // Example: + // user not found [main.go:20.GetUser] + // code: USER_404 + // category: validation + // context: {user_id: user-123} + VerbosityStandard + + // VerbosityFull shows all non-empty fields (default). + // Recommended for debugging and development. + // + // Example: + // user not found [main.go:20.GetUser] + // code: USER_404 + // category: validation + // context: {user_id: user-123, source: database} + // http_status: 400 + // retryable: true + // correlation_id: trace-123 + // help: Check if user exists + VerbosityFull + + // VerbosityCustom allows fine-grained control via individual field flags. + // Use with Show* and Hide* options. + VerbosityCustom +) + +// Field visibility options for VerbosityCustom. +type fieldVisibilityOption struct { + field string + show bool +} + +func (fieldVisibilityOption) ErrificOption() {} + +var ( + // ShowCode includes error code in output. + ShowCode = fieldVisibilityOption{field: "code", show: true} + // HideCode excludes error code from output. + HideCode = fieldVisibilityOption{field: "code", show: false} + + // ShowCategory includes error category in output. + ShowCategory = fieldVisibilityOption{field: "category", show: true} + // HideCategory excludes error category from output. + HideCategory = fieldVisibilityOption{field: "category", show: false} + + // ShowContext includes structured context in output. + ShowContext = fieldVisibilityOption{field: "context", show: true} + // HideContext excludes structured context from output. + HideContext = fieldVisibilityOption{field: "context", show: false} + + // ShowHTTPStatus includes HTTP status code in output. + ShowHTTPStatus = fieldVisibilityOption{field: "http_status", show: true} + // HideHTTPStatus excludes HTTP status code from output. + HideHTTPStatus = fieldVisibilityOption{field: "http_status", show: false} + + // ShowRetryMetadata includes retry information (retryable, retry_after, max_retries) in output. + ShowRetryMetadata = fieldVisibilityOption{field: "retry_metadata", show: true} + // HideRetryMetadata excludes retry information from output. + HideRetryMetadata = fieldVisibilityOption{field: "retry_metadata", show: false} + + // ShowMCPData includes MCP-related fields (correlation_id, help, suggestion, etc.) in output. + ShowMCPData = fieldVisibilityOption{field: "mcp_data", show: true} + // HideMCPData excludes MCP-related fields from output. + HideMCPData = fieldVisibilityOption{field: "mcp_data", show: false} + + // ShowTags includes semantic tags in output. + ShowTags = fieldVisibilityOption{field: "tags", show: true} + // HideTags excludes semantic tags from output. + HideTags = fieldVisibilityOption{field: "tags", show: false} + + // ShowLabels includes key-value labels in output. + ShowLabels = fieldVisibilityOption{field: "labels", show: true} + // HideLabels excludes key-value labels from output. + HideLabels = fieldVisibilityOption{field: "labels", show: false} + + // ShowTimestamps includes timestamp and duration in output. + ShowTimestamps = fieldVisibilityOption{field: "timestamps", show: true} + // HideTimestamps excludes timestamp and duration from output. + HideTimestamps = fieldVisibilityOption{field: "timestamps", show: false} +) + var root string var goroot string diff --git a/errific_test.go b/errific_test.go index 2e8c597..6752cbe 100644 --- a/errific_test.go +++ b/errific_test.go @@ -11,7 +11,7 @@ import ( ) func TestErrNew(t *testing.T) { - Configure() + Configure(OutputPretty) t.Run("basic error", func(t *testing.T) { var ErrTest Err = "test error" @@ -62,7 +62,7 @@ func TestErrNew(t *testing.T) { } func TestErrErrorf(t *testing.T) { - Configure() + Configure(OutputPretty) t.Run("formatted error", func(t *testing.T) { var ErrTest Err = "test error: %s %d" @@ -92,7 +92,7 @@ func TestErrErrorf(t *testing.T) { } func TestErrWithf(t *testing.T) { - Configure() + Configure(OutputPretty) t.Run("basic withf", func(t *testing.T) { var ErrTest Err = "test error" @@ -123,7 +123,7 @@ func TestErrWithf(t *testing.T) { } func TestErrWrapf(t *testing.T) { - Configure() + Configure(OutputPretty) t.Run("basic wrapf", func(t *testing.T) { var ErrTest Err = "test error" @@ -154,7 +154,7 @@ func TestErrWrapf(t *testing.T) { } func TestErrificJoin(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" err := ErrTest.New().Join(io.EOF, io.ErrUnexpectedEOF) @@ -170,7 +170,7 @@ func TestErrificJoin(t *testing.T) { func TestConfigureCallerOption(t *testing.T) { t.Run("suffix", func(t *testing.T) { - Configure(Suffix) + Configure(OutputPretty, Suffix) var ErrTest Err = "test" err := ErrTest.New() @@ -187,7 +187,7 @@ func TestConfigureCallerOption(t *testing.T) { }) t.Run("prefix", func(t *testing.T) { - Configure(Prefix) + Configure(OutputPretty, Prefix) var ErrTest Err = "test" err := ErrTest.New() @@ -200,7 +200,7 @@ func TestConfigureCallerOption(t *testing.T) { }) t.Run("disabled", func(t *testing.T) { - Configure(Disabled) + Configure(OutputPretty, Disabled) var ErrTest Err = "test" err := ErrTest.New() @@ -215,7 +215,7 @@ func TestConfigureCallerOption(t *testing.T) { func TestConfigureLayoutOption(t *testing.T) { t.Run("newline", func(t *testing.T) { - Configure(Newline) + Configure(OutputPretty, Newline) var ErrTest Err = "test" err := ErrTest.New(io.EOF, io.ErrUnexpectedEOF) @@ -228,7 +228,7 @@ func TestConfigureLayoutOption(t *testing.T) { }) t.Run("inline", func(t *testing.T) { - Configure(Inline) + Configure(OutputPretty, Inline) var ErrTest Err = "test" err := ErrTest.New(io.EOF, io.ErrUnexpectedEOF) @@ -269,7 +269,7 @@ func TestConfigureWithStack(t *testing.T) { }) t.Run("without stack", func(t *testing.T) { - Configure() // Default is without stack + Configure(OutputPretty) // Default is without stack var ErrTest Err = "test" err := ErrTest.New() @@ -287,7 +287,7 @@ func TestConfigureWithStack(t *testing.T) { } func TestWithStackContents(t *testing.T) { - Configure(WithStack) + Configure(OutputPretty, WithStack) t.Run("stack contains expected file and function", func(t *testing.T) { var ErrTest Err = "test error" @@ -453,7 +453,7 @@ func TestConcurrentConfigure(t *testing.T) { } func TestConcurrentErrorCreation(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test" var wg sync.WaitGroup @@ -476,7 +476,7 @@ func TestConcurrentErrorCreation(t *testing.T) { } func TestUnwrap(t *testing.T) { - Configure() + Configure(OutputPretty) var ( Err1 Err = "error 1" @@ -501,7 +501,7 @@ func TestUnwrap(t *testing.T) { } func TestCircularReferenceFixed(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test" err := ErrTest.Withf("detail %d", 1) @@ -522,7 +522,7 @@ func TestCircularReferenceFixed(t *testing.T) { } func BenchmarkErrNew(b *testing.B) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" b.ResetTimer() @@ -532,7 +532,7 @@ func BenchmarkErrNew(b *testing.B) { } func BenchmarkErrNewWithWrap(b *testing.B) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" b.ResetTimer() @@ -542,7 +542,7 @@ func BenchmarkErrNewWithWrap(b *testing.B) { } func BenchmarkErrError(b *testing.B) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" err := ErrTest.New(io.EOF) @@ -567,7 +567,7 @@ func BenchmarkErrWithStack(b *testing.B) { // ============================================================================ func TestWithContext(t *testing.T) { - Configure() + Configure(OutputPretty) t.Run("basic context", func(t *testing.T) { var ErrTest Err = "test error" @@ -616,7 +616,7 @@ func TestWithContext(t *testing.T) { } func TestWithCode(t *testing.T) { - Configure() + Configure(OutputPretty) t.Run("basic code", func(t *testing.T) { var ErrTest Err = "test error" @@ -638,7 +638,7 @@ func TestWithCode(t *testing.T) { } func TestWithCategory(t *testing.T) { - Configure() + Configure(OutputPretty) t.Run("basic category", func(t *testing.T) { var ErrTest Err = "test error" @@ -672,7 +672,7 @@ func TestWithCategory(t *testing.T) { } func TestRetryMetadata(t *testing.T) { - Configure() + Configure(OutputPretty) t.Run("retryable", func(t *testing.T) { var ErrTest Err = "test error" @@ -732,7 +732,7 @@ func TestRetryMetadata(t *testing.T) { } func TestWithHTTPStatus(t *testing.T) { - Configure() + Configure(OutputPretty) t.Run("basic http status", func(t *testing.T) { var ErrTest Err = "test error" @@ -768,7 +768,7 @@ func TestWithHTTPStatus(t *testing.T) { } func TestJSONSerialization(t *testing.T) { - Configure() + Configure(OutputPretty) t.Run("basic json", func(t *testing.T) { var ErrTest Err = "test error" @@ -864,7 +864,7 @@ func TestJSONSerialization(t *testing.T) { } func TestAIAgentScenario(t *testing.T) { - Configure() + Configure(OutputPretty) t.Run("database timeout scenario", func(t *testing.T) { // Simulate a database timeout error with full AI-agent metadata @@ -950,7 +950,7 @@ func TestAIAgentScenario(t *testing.T) { // Phase 2A: MCP & RAG Integration Tests func TestPhase2A_CorrelationID(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("with correlation ID", func(t *testing.T) { @@ -980,7 +980,7 @@ func TestPhase2A_CorrelationID(t *testing.T) { } func TestPhase2A_RequestID(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" err := ErrTest.New().WithRequestID("req-67890") @@ -992,7 +992,7 @@ func TestPhase2A_RequestID(t *testing.T) { } func TestPhase2A_UserID(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" err := ErrTest.New().WithUserID("user-123") @@ -1004,7 +1004,7 @@ func TestPhase2A_UserID(t *testing.T) { } func TestPhase2A_SessionID(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" err := ErrTest.New().WithSessionID("sess-456") @@ -1016,7 +1016,7 @@ func TestPhase2A_SessionID(t *testing.T) { } func TestPhase2A_Help(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" helpText := "Check your configuration and try again" @@ -1029,7 +1029,7 @@ func TestPhase2A_Help(t *testing.T) { } func TestPhase2A_Suggestion(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" suggestion := "Increase timeout to 30 seconds" @@ -1042,7 +1042,7 @@ func TestPhase2A_Suggestion(t *testing.T) { } func TestPhase2A_Docs(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" docsURL := "https://docs.example.com/errors/timeout" @@ -1055,7 +1055,7 @@ func TestPhase2A_Docs(t *testing.T) { } func TestPhase2A_Tags(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("with tags", func(t *testing.T) { @@ -1085,7 +1085,7 @@ func TestPhase2A_Tags(t *testing.T) { } func TestPhase2A_Labels(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("with labels map", func(t *testing.T) { @@ -1135,7 +1135,7 @@ func TestPhase2A_Labels(t *testing.T) { } func TestPhase2A_Timestamp(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" now := time.Now() @@ -1148,7 +1148,7 @@ func TestPhase2A_Timestamp(t *testing.T) { } func TestPhase2A_Duration(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" duration := 5 * time.Second @@ -1161,7 +1161,7 @@ func TestPhase2A_Duration(t *testing.T) { } func TestPhase2A_MCPCode(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("with MCP code", func(t *testing.T) { @@ -1191,7 +1191,7 @@ func TestPhase2A_MCPCode(t *testing.T) { } func TestPhase2A_ToMCPError(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("with explicit MCP code", func(t *testing.T) { @@ -1273,7 +1273,7 @@ func TestPhase2A_MCPErrorType(t *testing.T) { } func TestPhase2A_JSONSerialization(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("Phase 2A fields in JSON", func(t *testing.T) { @@ -1351,7 +1351,7 @@ func TestPhase2A_JSONSerialization(t *testing.T) { } func TestPhase2A_MCPIntegration(t *testing.T) { - Configure() + Configure(OutputPretty) t.Run("MCP tool error scenario", func(t *testing.T) { var ErrToolExecution Err = "tool execution failed" @@ -1466,7 +1466,7 @@ func TestMCPErrorCodeConstants(t *testing.T) { } func TestToMCPError_EdgeCases(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("nil error returns zero MCPError", func(t *testing.T) { @@ -1537,7 +1537,7 @@ func TestMCPError_ErrorFormat(t *testing.T) { } func TestPhase2A_LabelMerging(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("WithLabels then WithLabel merges", func(t *testing.T) { @@ -1574,7 +1574,7 @@ func TestPhase2A_LabelMerging(t *testing.T) { } func TestPhase2A_WithLabelsNil(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" err := ErrTest.New().WithLabels(nil) @@ -1587,7 +1587,7 @@ func TestPhase2A_WithLabelsNil(t *testing.T) { } func TestPhase2A_EmptyTags(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" err := ErrTest.New().WithTags() @@ -1645,7 +1645,7 @@ func TestPhase2A_HelpersWithStdlibErrors(t *testing.T) { } func TestPhase2A_TimestampEdgeCases(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("no timestamp returns zero time", func(t *testing.T) { @@ -1679,7 +1679,7 @@ func TestPhase2A_TimestampEdgeCases(t *testing.T) { } func TestPhase2A_DurationEdgeCases(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("no duration returns zero", func(t *testing.T) { @@ -1712,7 +1712,7 @@ func TestPhase2A_DurationEdgeCases(t *testing.T) { } func TestPhase2A_SpecialCharacters(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" err := ErrTest.New(). @@ -1745,7 +1745,7 @@ func TestPhase2A_SpecialCharacters(t *testing.T) { } func TestPhase2A_JSONZeroValues(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" // Create error with no Phase 2A fields set @@ -1771,7 +1771,7 @@ func TestPhase2A_JSONZeroValues(t *testing.T) { } func TestPhase2A_ChainAllMethods(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" now := time.Now() @@ -1832,7 +1832,7 @@ func TestPhase2A_ChainAllMethods(t *testing.T) { } func TestPhase2A_LabelKeyEdgeCases(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" err := ErrTest.New(). @@ -1862,7 +1862,7 @@ func TestPhase2A_LabelKeyEdgeCases(t *testing.T) { } func BenchmarkWithContext(b *testing.B) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test" ctx := Context{"key": "value"} @@ -1873,7 +1873,7 @@ func BenchmarkWithContext(b *testing.B) { } func BenchmarkJSONMarshal(b *testing.B) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test" err := ErrTest.New(). WithCode("ERR_001"). diff --git a/error.go b/error.go index 558e99b..c722cf6 100644 --- a/error.go +++ b/error.go @@ -56,15 +56,13 @@ func (e Err) New(errs ...error) errific { a[i] = errs[i] } - caller, stack, cfgCaller, cfgLayout, cfgWithStack := callstack(a) + caller, stack, cfg := callstack(a) return errific{ - err: e, - errs: errs, - caller: caller, - stack: stack, - cfgCaller: cfgCaller, - cfgLayout: cfgLayout, - cfgWithStack: cfgWithStack, + err: e, + errs: errs, + caller: caller, + stack: stack, + cfg: cfg, } } @@ -75,15 +73,13 @@ func (e Err) New(errs ...error) errific { // // return ErrProcessThing.Errorf("abc") func (e Err) Errorf(a ...any) errific { - caller, stack, cfgCaller, cfgLayout, cfgWithStack := callstack(a) + caller, stack, cfg := callstack(a) return errific{ - err: fmt.Errorf(e.Error(), a...), - caller: caller, - unwrap: []error{e}, - stack: stack, - cfgCaller: cfgCaller, - cfgLayout: cfgLayout, - cfgWithStack: cfgWithStack, + err: fmt.Errorf(e.Error(), a...), + caller: caller, + unwrap: []error{e}, + stack: stack, + cfg: cfg, } } @@ -93,16 +89,14 @@ func (e Err) Errorf(a ...any) errific { // // return ErrProcessThing.Withf("id: '%s'", "abc") func (e Err) Withf(format string, a ...any) errific { - caller, stack, cfgCaller, cfgLayout, cfgWithStack := callstack(a) + caller, stack, cfg := callstack(a) format = e.Error() + ": " + format return errific{ - err: fmt.Errorf(format, a...), - caller: caller, - unwrap: []error{e}, - stack: stack, - cfgCaller: cfgCaller, - cfgLayout: cfgLayout, - cfgWithStack: cfgWithStack, + err: fmt.Errorf(format, a...), + caller: caller, + unwrap: []error{e}, + stack: stack, + cfg: cfg, } } @@ -113,15 +107,13 @@ func (e Err) Withf(format string, a ...any) errific { // // return ErrProcessThing.Wrapf("cause: %w", err) func (e Err) Wrapf(format string, a ...any) errific { - caller, stack, cfgCaller, cfgLayout, cfgWithStack := callstack(a) + caller, stack, cfg := callstack(a) return errific{ - err: e, - errs: []error{fmt.Errorf(format, a...)}, - caller: caller, - stack: stack, - cfgCaller: cfgCaller, - cfgLayout: cfgLayout, - cfgWithStack: cfgWithStack, + err: e, + errs: []error{fmt.Errorf(format, a...)}, + caller: caller, + stack: stack, + cfg: cfg, } } @@ -283,6 +275,25 @@ func (m MCPError) Error() string { return fmt.Sprintf("MCP error %d: %s", m.Code, m.Message) } +// configSnapshot captures configuration at error creation time. +// This prevents race conditions and ensures consistent formatting. +type configSnapshot struct { + caller callerOption + layout layoutOption + withStack bool + outputFormat outputFormatOption + verbosity verbosityOption + showCode bool + showCategory bool + showContext bool + showHTTPStatus bool + showRetryMeta bool + showMCPData bool + showTags bool + showLabels bool + showTimestamps bool +} + type errific struct { err error // primary error. errs []error // errors used in string output, and satisfy errors.Is. @@ -310,39 +321,47 @@ type errific struct { timestamp time.Time // when the error occurred. duration time.Duration // operation duration before error. // Configuration snapshot at error creation time - cfgCaller callerOption // caller config when error was created. - cfgLayout layoutOption // layout config when error was created. - cfgWithStack bool // withStack config when error was created. + cfg configSnapshot } -func (e errific) Error() (msg string) { +func (e errific) Error() string { // Use configuration snapshot from error creation time // This prevents race conditions and ensures consistent formatting - caller := e.cfgCaller - layout := e.cfgLayout - withStack := e.cfgWithStack + switch e.cfg.outputFormat { + case OutputJSON: + return e.formatJSON() + case OutputJSONPretty: + return e.formatJSONPretty() + case OutputCompact: + return e.formatCompact() + default: // OutputPretty + return e.formatPretty() + } +} + +// formatPretty formats the error as human-readable multi-line text. +func (e errific) formatPretty() string { + var msg string - switch caller { + // Build the base message with caller + switch e.cfg.caller { case Disabled: - // Include error message without caller information msg = e.err.Error() - case Prefix: msg = fmt.Sprintf("[%s] %s", e.caller, e.err.Error()) - - default: + default: // Suffix msg = fmt.Sprintf("%s [%s]", e.err.Error(), e.caller) } - switch layout { + // Add wrapped errors + switch e.cfg.layout { case Inline: for i := range e.errs { if e.errs[i] != nil { msg = fmt.Sprintf("%s ↩ %s", msg, e.errs[i].Error()) } } - - default: + default: // Newline for i := range e.errs { if e.errs[i] != nil { msg = fmt.Sprintf("%s\n%s", msg, e.errs[i].Error()) @@ -350,17 +369,188 @@ func (e errific) Error() (msg string) { } } - if withStack && len(e.stack) > 0 { - // Append stack trace at the end - // Note: If wrapping another errific error with a stack, both stacks may appear. - // This is intentional - each error in the chain shows its creation point. - // To avoid duplicate stacks, the stack from wrapped errors is reused when possible. + // Add metadata fields based on verbosity + var fields []string + + if e.cfg.showCode && e.code != "" { + fields = append(fields, fmt.Sprintf(" code: %s", e.code)) + } + + if e.cfg.showCategory && e.category != "" { + fields = append(fields, fmt.Sprintf(" category: %s", e.category)) + } + + if e.cfg.showContext && len(e.context) > 0 { + fields = append(fields, fmt.Sprintf(" context: %v", e.context)) + } + + if e.cfg.showHTTPStatus && e.httpStatus != 0 { + fields = append(fields, fmt.Sprintf(" http_status: %d", e.httpStatus)) + } + + if e.cfg.showRetryMeta { + if e.retryable { + fields = append(fields, " retryable: true") + } + if e.retryAfter > 0 { + fields = append(fields, fmt.Sprintf(" retry_after: %s", e.retryAfter)) + } + if e.maxRetries > 0 { + fields = append(fields, fmt.Sprintf(" max_retries: %d", e.maxRetries)) + } + } + + if e.cfg.showMCPData { + if e.mcpCode != 0 { + fields = append(fields, fmt.Sprintf(" mcp_code: %d", e.mcpCode)) + } + if e.correlationID != "" { + fields = append(fields, fmt.Sprintf(" correlation_id: %s", e.correlationID)) + } + if e.requestID != "" { + fields = append(fields, fmt.Sprintf(" request_id: %s", e.requestID)) + } + if e.userID != "" { + fields = append(fields, fmt.Sprintf(" user_id: %s", e.userID)) + } + if e.sessionID != "" { + fields = append(fields, fmt.Sprintf(" session_id: %s", e.sessionID)) + } + if e.help != "" { + fields = append(fields, fmt.Sprintf(" help: %s", e.help)) + } + if e.suggestion != "" { + fields = append(fields, fmt.Sprintf(" suggestion: %s", e.suggestion)) + } + if e.docsURL != "" { + fields = append(fields, fmt.Sprintf(" docs: %s", e.docsURL)) + } + } + + if e.cfg.showTags && len(e.tags) > 0 { + fields = append(fields, fmt.Sprintf(" tags: %v", e.tags)) + } + + if e.cfg.showLabels && len(e.labels) > 0 { + fields = append(fields, fmt.Sprintf(" labels: %v", e.labels)) + } + + if e.cfg.showTimestamps { + if !e.timestamp.IsZero() { + fields = append(fields, fmt.Sprintf(" timestamp: %s", e.timestamp.Format(time.RFC3339))) + } + if e.duration > 0 { + fields = append(fields, fmt.Sprintf(" duration: %s", e.duration)) + } + } + + // Append all fields + if len(fields) > 0 { + msg += "\n" + strings.Join(fields, "\n") + } + + // Add stack trace if configured + if e.cfg.withStack && len(e.stack) > 0 { msg += string(e.stack) } return msg } +// formatJSON formats the error as compact JSON. +func (e errific) formatJSON() string { + data, err := json.Marshal(e) + if err != nil { + // Fallback to simple error message if marshaling fails + return fmt.Sprintf(`{"error":"%s"}`, e.err.Error()) + } + return string(data) +} + +// formatJSONPretty formats the error as indented JSON. +func (e errific) formatJSONPretty() string { + data, err := json.MarshalIndent(e, "", " ") + if err != nil { + // Fallback to simple error message if marshaling fails + return fmt.Sprintf(`{\n "error": "%s"\n}`, e.err.Error()) + } + return string(data) +} + +// formatCompact formats the error as single-line text with key=value pairs. +func (e errific) formatCompact() string { + var parts []string + + // Base message with caller + switch e.cfg.caller { + case Disabled: + parts = append(parts, e.err.Error()) + case Prefix: + parts = append(parts, fmt.Sprintf("[%s] %s", e.caller, e.err.Error())) + default: // Suffix + parts = append(parts, fmt.Sprintf("%s [%s]", e.err.Error(), e.caller)) + } + + // Add wrapped errors inline + for i := range e.errs { + if e.errs[i] != nil { + parts = append(parts, "↩", e.errs[i].Error()) + } + } + + // Add metadata as key=value pairs + if e.cfg.showCode && e.code != "" { + parts = append(parts, fmt.Sprintf("code=%s", e.code)) + } + + if e.cfg.showCategory && e.category != "" { + parts = append(parts, fmt.Sprintf("category=%s", e.category)) + } + + if e.cfg.showContext && len(e.context) > 0 { + for k, v := range e.context { + parts = append(parts, fmt.Sprintf("%s=%v", k, v)) + } + } + + if e.cfg.showHTTPStatus && e.httpStatus != 0 { + parts = append(parts, fmt.Sprintf("http_status=%d", e.httpStatus)) + } + + if e.cfg.showRetryMeta { + if e.retryable { + parts = append(parts, "retryable=true") + } + if e.retryAfter > 0 { + parts = append(parts, fmt.Sprintf("retry_after=%s", e.retryAfter)) + } + if e.maxRetries > 0 { + parts = append(parts, fmt.Sprintf("max_retries=%d", e.maxRetries)) + } + } + + if e.cfg.showMCPData { + if e.correlationID != "" { + parts = append(parts, fmt.Sprintf("correlation_id=%s", e.correlationID)) + } + if e.requestID != "" { + parts = append(parts, fmt.Sprintf("request_id=%s", e.requestID)) + } + } + + if e.cfg.showTags && len(e.tags) > 0 { + parts = append(parts, fmt.Sprintf("tags=%v", e.tags)) + } + + if e.cfg.showLabels && len(e.labels) > 0 { + for k, v := range e.labels { + parts = append(parts, fmt.Sprintf("label_%s=%s", k, v)) + } + } + + return strings.Join(parts, " ") +} + func (e errific) Join(errs ...error) error { e.errs = append(e.errs, errs...) return e @@ -766,17 +956,36 @@ func unwrapStack(errs []any) []byte { return nil } -func callstack(errs []any) (caller string, stack []byte, cfgCaller callerOption, cfgLayout layoutOption, cfgWithStack bool) { +// captureConfig captures the current configuration as a snapshot. +// This must be called with cMu held (either RLock or Lock). +func captureConfig() configSnapshot { + return configSnapshot{ + caller: c.caller, + layout: c.layout, + withStack: bool(c.withStack), + outputFormat: c.outputFormat, + verbosity: c.verbosity, + showCode: c.showCode, + showCategory: c.showCategory, + showContext: c.showContext, + showHTTPStatus: c.showHTTPStatus, + showRetryMeta: c.showRetryMetadata, + showMCPData: c.showMCPData, + showTags: c.showTags, + showLabels: c.showLabels, + showTimestamps: c.showTimestamps, + } +} + +func callstack(errs []any) (caller string, stack []byte, cfg configSnapshot) { pc := make([]uintptr, 32) n := runtime.Callers(3, pc) if n == 0 { // Capture config snapshot even if no caller info cMu.RLock() - cfgCaller = c.caller - cfgLayout = c.layout - cfgWithStack = bool(c.withStack) + cfg = captureConfig() cMu.RUnlock() - return "", stack, cfgCaller, cfgLayout, cfgWithStack + return "", stack, cfg } frames := runtime.CallersFrames(pc) @@ -785,23 +994,21 @@ func callstack(errs []any) (caller string, stack []byte, cfgCaller callerOption, // Capture configuration snapshot once at error creation time cMu.RLock() - cfgCaller = c.caller - cfgLayout = c.layout - cfgWithStack = bool(c.withStack) + cfg = captureConfig() cMu.RUnlock() - if !cfgWithStack { - return caller, stack, cfgCaller, cfgLayout, cfgWithStack + if !cfg.withStack { + return caller, stack, cfg } stack = unwrapStack(errs) if len(stack) > 0 { - return caller, stack, cfgCaller, cfgLayout, cfgWithStack + return caller, stack, cfg } if !more { - return caller, stack, cfgCaller, cfgLayout, cfgWithStack + return caller, stack, cfg } for { @@ -816,7 +1023,7 @@ func callstack(errs []any) (caller string, stack []byte, cfgCaller callerOption, } } - return caller, stack, cfgCaller, cfgLayout, cfgWithStack + return caller, stack, cfg } func parseFrame(frame runtime.Frame) string { diff --git a/examples/example_errorf_test.go b/examples/example_errorf_test.go index bd970fb..9a9b1d6 100644 --- a/examples/example_errorf_test.go +++ b/examples/example_errorf_test.go @@ -9,7 +9,7 @@ import ( ) func ExampleErrorf() { - Configure() // default configuration + Configure(OutputPretty) // default configuration // format an error with parameters. var ErrExample Err = "formatted error: %s %w" err := ErrExample.Errorf("io error", io.EOF) diff --git a/examples/example_new_test.go b/examples/example_new_test.go index a462f86..e054d75 100644 --- a/examples/example_new_test.go +++ b/examples/example_new_test.go @@ -9,7 +9,7 @@ import ( ) func ExampleNew() { - Configure() // default configuration + Configure(OutputPretty) // default configuration // w/out wrapping errors. var ErrExample Err = "example error" err := ErrExample.New() @@ -22,7 +22,7 @@ func ExampleNew() { } func Example_newWrapError() { - Configure() // default configuration + Configure(OutputPretty) // default configuration // wrap an error. var ErrExample Err = "example error" err := ErrExample.New(io.EOF) @@ -38,7 +38,7 @@ func Example_newWrapError() { } func Example_newWrapErrors() { - Configure() // default configuration + Configure(OutputPretty) // default configuration // wrap multiple errors. var ErrExample Err = "example error" err := ErrExample.New(io.ErrUnexpectedEOF, io.EOF) @@ -57,7 +57,7 @@ func Example_newWrapErrors() { } func Example_newNest() { - Configure() // default configuration + Configure(OutputPretty) // default configuration // wrapped errific error chain. var ( Err1 Err = "error 1" diff --git a/examples/example_output_formats_test.go b/examples/example_output_formats_test.go new file mode 100644 index 0000000..19fc3db --- /dev/null +++ b/examples/example_output_formats_test.go @@ -0,0 +1,54 @@ +package examples + +import ( + "fmt" + "github.com/leefernandes/errific" +) + +// Example_outputFormats demonstrates the different output formats available. +func Example_outputFormats() { + var ErrUserNotFound errific.Err = "user not found" + + // Pretty format (default) - shows all metadata + errific.Configure(errific.OutputPretty, errific.VerbosityFull) + err := ErrUserNotFound. + WithCode("USER_404"). + WithCategory(errific.CategoryNotFound). + WithContext(errific.Context{ + "user_id": "user-123", + "source": "database", + }). + WithHTTPStatus(404) + + fmt.Println("Pretty format:") + fmt.Println(err) + + // Minimal verbosity + errific.Configure(errific.VerbosityMinimal) + err2 := ErrUserNotFound. + WithCode("USER_404"). + WithContext(errific.Context{"user_id": "user-123"}) + + fmt.Println("\nMinimal verbosity:") + fmt.Println(err2) + + // JSON format + errific.Configure(errific.OutputJSON) + err3 := ErrUserNotFound. + WithCode("USER_404"). + WithContext(errific.Context{"user_id": "user-123"}) + + fmt.Println("\nJSON format:") + fmt.Println(err3) + + // Compact format + errific.Configure(errific.OutputCompact) + err4 := ErrUserNotFound. + WithCode("USER_404"). + WithContext(errific.Context{"user_id": "user-123"}) + + fmt.Println("\nCompact format:") + fmt.Println(err4) + + // Output varies based on file paths and caller info +} diff --git a/examples/example_phase1_test.go b/examples/example_phase1_test.go index 92f0feb..a72153e 100644 --- a/examples/example_phase1_test.go +++ b/examples/example_phase1_test.go @@ -11,7 +11,7 @@ import ( ) func ExampleContext() { - Configure() + Configure(OutputPretty, VerbosityMinimal) // Add structured context to errors for better debugging var ErrDatabaseQuery Err = "database query failed" err := ErrDatabaseQuery.New(io.EOF).WithContext(Context{ @@ -34,7 +34,7 @@ func ExampleContext() { } func Example_errorCode() { - Configure() + Configure(OutputPretty) // Use error codes for machine-readable identification var ErrAPITimeout Err = "API request timeout" err := ErrAPITimeout.New(). @@ -55,7 +55,7 @@ func Example_errorCode() { } func Example_retryable() { - Configure() + Configure(OutputPretty) // Mark errors as retryable with suggested retry strategy var ErrRateLimit Err = "rate limit exceeded" err := ErrRateLimit.New(). @@ -79,7 +79,7 @@ func Example_retryable() { } func Example_httpStatus() { - Configure() + Configure(OutputPretty) // Set HTTP status codes for automatic response handling var ErrValidation Err = "validation failed" err := ErrValidation.New(). @@ -97,7 +97,7 @@ func Example_httpStatus() { } func Example_json() { - Configure() + Configure(OutputPretty) // Serialize errors to JSON for logging and APIs var ErrDatabase Err = "database connection failed" err := ErrDatabase.New(io.EOF). @@ -133,7 +133,7 @@ func Example_json() { } func Example_aiAgentScenario() { - Configure() + Configure(OutputPretty) // Complete example for AI agent automated error handling var ErrServiceCall Err = "external service call failed" err := ErrServiceCall.New(). @@ -170,7 +170,7 @@ func Example_aiAgentScenario() { } func Example_chainedMethods() { - Configure() + Configure(OutputPretty) // Chain all Phase 1 methods together var ErrProcessing Err = "processing failed" err := ErrProcessing.New(io.EOF). diff --git a/examples/example_phase2a_test.go b/examples/example_phase2a_test.go index dc1aa10..1348186 100644 --- a/examples/example_phase2a_test.go +++ b/examples/example_phase2a_test.go @@ -16,7 +16,7 @@ var ( // Example_mcpToolError demonstrates MCP tool error handling with correlation tracking, // recovery suggestions, and semantic tags for RAG systems. func Example_mcpToolError() { - errific.Configure() + errific.Configure(errific.OutputPretty, errific.VerbosityMinimal) // MCP tool error with full metadata for AI agents err := ErrMCPToolExecution.New(). @@ -45,7 +45,7 @@ func Example_mcpToolError() { // Example_mcpErrorFormat demonstrates converting an errific error to MCP JSON-RPC 2.0 format // for use in MCP server error responses. func Example_mcpErrorFormat() { - errific.Configure() + errific.Configure(errific.OutputPretty) err := ErrMCPToolExecution.New(). WithMCPCode(errific.MCPInvalidParams). @@ -66,7 +66,7 @@ func Example_mcpErrorFormat() { // Example_correlationTracking demonstrates using correlation IDs to track errors // across distributed MCP tool calls. func Example_correlationTracking() { - errific.Configure() + errific.Configure(errific.OutputPretty) correlationID := "trace-12345" @@ -101,7 +101,7 @@ func Example_correlationTracking() { // Example_recoverySuggestions demonstrates providing recovery guidance for AI agents // to automatically resolve errors. func Example_recoverySuggestions() { - errific.Configure() + errific.Configure(errific.OutputPretty) var ErrDatabaseTimeout errific.Err = "database query timeout" @@ -128,7 +128,7 @@ func Example_recoverySuggestions() { // Example_semanticTags demonstrates using semantic tags for RAG systems // to categorize and search errors. func Example_semanticTags() { - errific.Configure() + errific.Configure(errific.OutputPretty) var ErrNetworkTimeout errific.Err = "network timeout" @@ -149,7 +149,7 @@ func Example_semanticTags() { // Example_labelsForFiltering demonstrates using key-value labels // to filter and group errors for monitoring and alerting. func Example_labelsForFiltering() { - errific.Configure() + errific.Configure(errific.OutputPretty) var ErrServiceDegraded errific.Err = "service degraded" @@ -174,7 +174,7 @@ func Example_labelsForFiltering() { // Example_timestampAndDuration demonstrates tracking when an error occurred // and how long the operation took before failing. func Example_timestampAndDuration() { - errific.Configure() + errific.Configure(errific.OutputPretty) var ErrSlowQuery errific.Err = "slow database query" @@ -200,7 +200,7 @@ func Example_timestampAndDuration() { // Example_phase2aJSONSerialization demonstrates JSON serialization of all Phase 2A fields // for structured logging and monitoring systems. func Example_phase2aJSONSerialization() { - errific.Configure() + errific.Configure(errific.OutputPretty) var ErrCompleteExample errific.Err = "complete Phase 2A example" @@ -232,7 +232,7 @@ func Example_phase2aJSONSerialization() { // Example_mcpInvalidParams demonstrates handling MCP invalid parameter errors // with detailed validation context. func Example_mcpInvalidParams() { - errific.Configure() + errific.Configure(errific.OutputPretty) var ErrInvalidToolParams errific.Err = "invalid tool parameters" @@ -262,7 +262,7 @@ func Example_mcpInvalidParams() { // Example_aiAgentWorkflow demonstrates a complete AI agent error handling workflow // using Phase 2A features for automated decision-making. func Example_aiAgentWorkflow() { - errific.Configure() + errific.Configure(errific.OutputPretty) var ErrToolFailed errific.Err = "tool execution failed" diff --git a/examples/example_trimprefixes_test.go b/examples/example_trimprefixes_test.go index 338d562..f543c4c 100644 --- a/examples/example_trimprefixes_test.go +++ b/examples/example_trimprefixes_test.go @@ -13,7 +13,7 @@ func ExampleTrimPrefixes() { if err != nil { panic(err) } - Configure(TrimPrefixes(wd + "/")) + Configure(OutputPretty, TrimPrefixes(wd + "/")) var ErrExample Err = "example error" err = ErrExample.New() fmt.Println(err) @@ -25,7 +25,7 @@ func ExampleTrimPrefixes() { } func ExampleTrimCWD() { - Configure(TrimCWD) + Configure(OutputPretty, TrimCWD) var ErrExample Err = "example error" err := ErrExample.New() fmt.Println(err) diff --git a/examples/example_withf_test.go b/examples/example_withf_test.go index 540eeb2..8bd4a8c 100644 --- a/examples/example_withf_test.go +++ b/examples/example_withf_test.go @@ -9,7 +9,7 @@ import ( ) func Example_withf() { - Configure() // default configuration + Configure(OutputPretty) // default configuration var ErrExample Err = "example error" err := ErrExample.Withf("int (%d) string (%s): %w", 123, "yarn", io.EOF) fmt.Println(err) @@ -23,7 +23,7 @@ func Example_withf() { } func Example_withfNest() { - Configure() // default configuration + Configure(OutputPretty) // default configuration var ( Err1 Err = "error 1" Err2 Err = "error 2" @@ -46,7 +46,7 @@ func Example_withfNest() { } func Example_withfChain() { - Configure() // default configuration + Configure(OutputPretty) // default configuration var ErrExample Err = "example error" err := ErrExample. diff --git a/examples/example_wrapf_test.go b/examples/example_wrapf_test.go index 704a021..a24842f 100644 --- a/examples/example_wrapf_test.go +++ b/examples/example_wrapf_test.go @@ -9,7 +9,7 @@ import ( ) func Example_wrapf() { - Configure() // default configuration + Configure(OutputPretty) // default configuration // wrap a formatted error. var ErrExample Err = "example error" err := ErrExample.Wrapf("formatted %d: %w", 1, io.EOF) @@ -25,7 +25,7 @@ func Example_wrapf() { } func Example_wrapfNest() { - Configure() // default configuration + Configure(OutputPretty) // default configuration // wrapped & formatted errific error chain. var ( Err1 Err = "error 1" @@ -49,7 +49,7 @@ func Example_wrapfNest() { } func Example_wrapfChain() { - Configure() // default configuration + Configure(OutputPretty) // default configuration var ErrExample Err = "example error" err := ErrExample. diff --git a/tests/concurrency_test.go b/tests/concurrency_test.go index be79a8e..7d1ee2b 100644 --- a/tests/concurrency_test.go +++ b/tests/concurrency_test.go @@ -14,7 +14,7 @@ import ( // ============================================================================ func TestConcurrent_Getters(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "concurrent test" err := ErrTest.New(). @@ -55,16 +55,16 @@ func TestConcurrent_ConfigureAndCreate(t *testing.T) { go func(idx int) { defer wg.Done() if idx%2 == 0 { - Configure(Suffix, Newline) + Configure(OutputPretty, Suffix, Newline) } else { - Configure(Prefix, Inline) + Configure(OutputPretty, Prefix, Inline) } _ = ErrTest.New() }(i) } wg.Wait() - Configure() // Reset + Configure(OutputPretty) // Reset } // ============================================================================ @@ -76,11 +76,11 @@ func TestRaceCondition_ConfigurationSnapshot(t *testing.T) { t.Run("error formatting consistent after Configure", func(t *testing.T) { // Create error with Suffix config - Configure(Suffix) + Configure(OutputPretty, Suffix) err := ErrTest.New() // Change configuration - Configure(Prefix) + Configure(OutputPretty, Prefix) // Error should still use Suffix (snapshot at creation time) msg := err.Error() @@ -94,7 +94,7 @@ func TestRaceCondition_ConfigurationSnapshot(t *testing.T) { t.Run("concurrent Configure and Error calls", func(t *testing.T) { // This test should pass race detector - Configure(Suffix, Newline) + Configure(OutputPretty, Suffix, Newline) var wg sync.WaitGroup errors := make([]error, 100) @@ -114,9 +114,9 @@ func TestRaceCondition_ConfigurationSnapshot(t *testing.T) { go func(n int) { defer wg.Done() if n%2 == 0 { - Configure(Prefix, Inline) + Configure(OutputPretty, Prefix, Inline) } else { - Configure(Suffix, Newline) + Configure(OutputPretty, Suffix, Newline) } }(i) } @@ -137,15 +137,15 @@ func TestRaceCondition_ConfigurationSnapshot(t *testing.T) { t.Run("stack config snapshot works", func(t *testing.T) { // Create error without stack - Configure() + Configure(OutputPretty) err1 := ErrTest.New() // Enable stack - Configure(WithStack) + Configure(OutputPretty, WithStack) err2 := ErrTest.New() // Disable stack again - Configure() + Configure(OutputPretty) err3 := ErrTest.New() // Each error should use its creation-time config @@ -171,11 +171,11 @@ func TestRaceCondition_ConfigurationSnapshot(t *testing.T) { t.Run("layout config snapshot works", func(t *testing.T) { // Create error with Newline layout - Configure(Newline) + Configure(OutputPretty, Newline) err1 := ErrTest.New(errors.New("wrapped1"), errors.New("wrapped2")) // Change to Inline - Configure(Inline) + Configure(OutputPretty, Inline) err2 := ErrTest.New(errors.New("wrapped1"), errors.New("wrapped2")) // err1 should use newlines @@ -200,7 +200,7 @@ func TestRaceCondition_ConfigurationSnapshot(t *testing.T) { // ============================================================================ func TestImmutability_NoMutation(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test" err1 := ErrTest.New().WithCode("CODE1") @@ -217,15 +217,15 @@ func TestImmutability_NoMutation(t *testing.T) { func TestImmutability_MultipleConfigureCalls(t *testing.T) { // Test that errors capture config at creation time - Configure(Suffix, Newline) + Configure(OutputPretty, Suffix, Newline) var ErrTest Err = "test" err1 := ErrTest.New() - Configure(Prefix, Inline) + Configure(OutputPretty, Prefix, Inline) err2 := ErrTest.New() - Configure(Disabled) + Configure(OutputPretty, Disabled) err3 := ErrTest.New() // Each error should use its creation-time config diff --git a/tests/forwarding_test.go b/tests/forwarding_test.go index 040d89c..fd29f31 100644 --- a/tests/forwarding_test.go +++ b/tests/forwarding_test.go @@ -10,7 +10,7 @@ import ( // TestForwardingMethods tests that With___ methods can be called directly on Err func TestForwardingMethods(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("WithCode without explicit New", func(t *testing.T) { @@ -160,7 +160,7 @@ func TestForwardingMethods(t *testing.T) { // TestForwardingMethodChaining tests that chaining works efficiently func TestForwardingMethodChaining(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("chained calls work", func(t *testing.T) { @@ -223,7 +223,7 @@ func TestForwardingMethodChaining(t *testing.T) { // TestForwardingBackwardsCompatibility tests that old style still works func TestForwardingBackwardsCompatibility(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("explicit New still works", func(t *testing.T) { @@ -263,7 +263,7 @@ func TestForwardingBackwardsCompatibility(t *testing.T) { // TestForwardingNewCalledOnce tests that New() is only called once func TestForwardingNewCalledOnce(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("caller info shows forwarding method", func(t *testing.T) { @@ -322,7 +322,7 @@ func TestForwardingNewCalledOnce(t *testing.T) { // TestForwardingWithWrappedErrors tests forwarding with wrapped errors func TestForwardingWithWrappedErrors(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" var ErrOther Err = "other error" @@ -345,7 +345,7 @@ func TestForwardingWithWrappedErrors(t *testing.T) { // TestForwardingValidationStillWorks tests that validation still applies func TestForwardingValidationStillWorks(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("invalid MCP code panics with forwarding", func(t *testing.T) { diff --git a/tests/integration_test.go b/tests/integration_test.go index c45e9c7..9459ba6 100644 --- a/tests/integration_test.go +++ b/tests/integration_test.go @@ -14,7 +14,7 @@ import ( // TestIntegration_WebAPIWithFullErrorHandling tests a complete web API scenario func TestIntegration_WebAPIWithFullErrorHandling(t *testing.T) { - Configure() + Configure(OutputPretty) var ( ErrInvalidInput = Err("invalid input") @@ -149,7 +149,7 @@ func TestIntegration_WebAPIWithFullErrorHandling(t *testing.T) { // TestIntegration_MCPToolServer tests MCP tool server scenario func TestIntegration_MCPToolServer(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrToolExecution = Err("tool execution failed") @@ -262,7 +262,7 @@ func TestIntegration_MCPToolServer(t *testing.T) { // TestIntegration_DistributedTracing tests distributed tracing scenario func TestIntegration_DistributedTracing(t *testing.T) { - Configure() + Configure(OutputPretty) var ( ErrServiceA = Err("service A failed") @@ -366,7 +366,7 @@ func containsAny(s string, substrs ...string) bool { // TestIntegration_AIAgentWithSelfHealing tests AI agent self-healing scenario func TestIntegration_AIAgentWithSelfHealing(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrAPICall = Err("API call failed") @@ -426,7 +426,7 @@ func TestIntegration_AIAgentWithSelfHealing(t *testing.T) { // TestIntegration_RAGErrorCategorization tests RAG system error categorization func TestIntegration_RAGErrorCategorization(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrEmbedding = Err("embedding generation failed") diff --git a/tests/serialization_test.go b/tests/serialization_test.go index c3d5773..183592d 100644 --- a/tests/serialization_test.go +++ b/tests/serialization_test.go @@ -16,7 +16,7 @@ import ( // ============================================================================ func TestToMCPError_WithNilError(t *testing.T) { - Configure() + Configure(OutputPretty) mcpErr := ToMCPError(nil) // ToMCPError returns zero MCPError for nil @@ -29,7 +29,7 @@ func TestToMCPError_WithNilError(t *testing.T) { } func TestToMCPError_WithStandardError(t *testing.T) { - Configure() + Configure(OutputPretty) stdErr := errors.New("standard error") mcpErr := ToMCPError(stdErr) @@ -43,7 +43,7 @@ func TestToMCPError_WithStandardError(t *testing.T) { } func TestMCPError_AllCodes(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" codes := []int{ @@ -77,7 +77,7 @@ func TestMCPError_AllCodes(t *testing.T) { } func TestMCPCode_Validation(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("valid MCP codes accepted", func(t *testing.T) { @@ -161,7 +161,7 @@ func TestMCPCode_Validation(t *testing.T) { // ============================================================================ func TestMarshalJSON_WithAllFields(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "complete error" err := ErrTest.New(). @@ -211,7 +211,7 @@ func TestMarshalJSON_WithAllFields(t *testing.T) { } func TestMarshalJSON_WithSpecialCharacters(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" // Test with special characters that need escaping diff --git a/tests/validation_test.go b/tests/validation_test.go index 4846bca..a4b9d16 100644 --- a/tests/validation_test.go +++ b/tests/validation_test.go @@ -14,7 +14,7 @@ import ( // ============================================================================ func TestHTTPStatus_Validation(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("valid HTTP status codes accepted", func(t *testing.T) { @@ -105,7 +105,7 @@ func TestHTTPStatus_Validation(t *testing.T) { // ============================================================================ func TestMaxRetries_Validation(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("non-negative values accepted", func(t *testing.T) { @@ -148,7 +148,7 @@ func TestMaxRetries_Validation(t *testing.T) { } func TestRetryAfter_Validation(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("non-negative durations accepted", func(t *testing.T) { @@ -200,7 +200,7 @@ func TestRetryAfter_Validation(t *testing.T) { // ============================================================================ func TestEmptyString_Validation(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("empty code ignored", func(t *testing.T) { @@ -302,7 +302,7 @@ func TestEmptyString_Validation(t *testing.T) { // ============================================================================ func TestChainedMethods_LastWins(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("multiple WithMCPCode calls - last wins", func(t *testing.T) { @@ -341,7 +341,7 @@ func TestChainedMethods_LastWins(t *testing.T) { // ============================================================================ func TestBoundaryValues_Extremes(t *testing.T) { - Configure() + Configure(OutputPretty) var ErrTest Err = "test error" t.Run("MCP code boundaries", func(t *testing.T) {