From 33c90cf02ded5d9a92d2734e16f33ba770232879 Mon Sep 17 00:00:00 2001 From: Rahugg <@a.faizolla> Date: Fri, 22 Nov 2024 17:44:57 +0500 Subject: [PATCH] Initial setup --- .github/workflows/go.yml | 49 ++++++++++ .idea/.gitignore | 10 ++ Readme.md | 115 ++++++++++++++++++++++ errorhandler/colors.go | 35 +++++++ errorhandler/errorhandler.go | 157 ++++++++++++++++++++++++++++++ errorhandler/errorhandler_test.go | 126 ++++++++++++++++++++++++ example/main.go | 20 ++++ go.mod | 3 + 8 files changed, 515 insertions(+) create mode 100644 .github/workflows/go.yml create mode 100644 .idea/.gitignore create mode 100644 Readme.md create mode 100644 errorhandler/colors.go create mode 100644 errorhandler/errorhandler.go create mode 100644 errorhandler/errorhandler_test.go create mode 100644 example/main.go create mode 100644 go.mod diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..970fc61 --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,49 @@ +name: Go CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + build: + name: Build and Test + runs-on: ubuntu-latest + strategy: + matrix: + go-version: [ '1.22.x' ] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Go ${{ matrix.go-version }} + uses: actions/setup-go@v4 + with: + go-version: ${{ matrix.go-version }} + cache: true + + - name: Install dependencies + run: go mod download + + - name: Verify dependencies + run: go mod verify + + - name: Run go vet + run: go vet ./... + + - name: Run tests with coverage + run: go test ./... -v -race -coverprofile=coverage.txt -covermode=atomic + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + + - name: Build + run: go build -v ./... + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v3 + with: + version: latest \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..a9d7db9 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,10 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml +# GitHub Copilot persisted chat sessions +/copilot/chatSessions diff --git a/Readme.md b/Readme.md new file mode 100644 index 0000000..378c411 --- /dev/null +++ b/Readme.md @@ -0,0 +1,115 @@ +# go-stacktrace + +`go-stacktrace` is a robust Go package for enhanced error handling, providing stack traces, error wrapping, and user-friendly error messages with optional color formatting. + +## Features + +- ๐Ÿ” Detailed stack traces for improved debugging +- ๐Ÿ“ Custom user messages and payload support +- ๐ŸŽจ Colorized error output (configurable) +- ๐Ÿ”„ Error wrapping with context preservation +- ๐Ÿชถ Lightweight with zero external dependencies +- ๐Ÿงช Comprehensive test coverage + +## Installation + +```bash +go get github.com/Rahugg/go-stacktrace +``` + +## Usage + +### Basic Error Wrapping + +```go +import "github.com/Rahugg/go-stacktrace/errorhandler" + +func main() { + err := someFunction() + if err != nil { + wrappedErr := errorhandler.WrapError( + err, + "operation-failed", // payload + "Failed to process request" // user message + ) + fmt.Println(errorhandler.FailOnError(wrappedErr)) + } +} +``` + +### Controlling Color Output + +```go +// Disable colored output +errorhandler.SetEnableColors(false) + +// Enable colored output (default) +errorhandler.SetEnableColors(true) +``` + +### Error Information + +The wrapped error includes: +- Original error message +- User-friendly message +- Custom payload (optional) +- Stack trace +- Color-coded output (optional) + +## Example Output + +``` +Payload: operation-failed +Error Details +Original Error: file not found +User Message: Failed to process request + +Stack Trace +main.someFunction + /path/to/file.go:25 +main.main + /path/to/main.go:12 +``` + +## Features in Detail + +### TracedError Structure +```go +type TracedError struct { + OriginalError error + UserMessage string + Payload string + StackTrace []uintptr +} +``` + +### Available Colors +- Red: Error messages +- Green: User messages +- Yellow: Payload information +- Blue: Function names in stack trace +- Magenta: Stack trace headers +- Cyan: Section headers + +## Contributing + +Contributions are welcome! Please feel free to submit a Pull Request. +Or DM me. + +## License + +This project is licensed under the MIT License - see the LICENSE file for details. + +## Project Structure +``` +. +โ”œโ”€โ”€ errorhandler +โ”‚ โ”œโ”€โ”€ colors.go # Color constants +โ”‚ โ”œโ”€โ”€ errorhandler.go # Core functionality +โ”‚ โ””โ”€โ”€ errorhandler_test.go # Test suite +โ”œโ”€โ”€ example +โ”‚ โ””โ”€โ”€ main.go # Usage examples +โ”œโ”€โ”€ go.mod +โ”œโ”€โ”€ go.yml +โ””โ”€โ”€ README.md +``` \ No newline at end of file diff --git a/errorhandler/colors.go b/errorhandler/colors.go new file mode 100644 index 0000000..c8fd0dc --- /dev/null +++ b/errorhandler/colors.go @@ -0,0 +1,35 @@ +package errorhandler + +const ( + // Reset sequence + Reset = "\033[0m" + + // Black Regular colors + Black = "\033[30m" + Red = "\033[31m" + Green = "\033[32m" + Yellow = "\033[33m" + Blue = "\033[34m" + Magenta = "\033[35m" + Cyan = "\033[36m" + White = "\033[37m" + + // BrightBlack Bright colors for better visibility + BrightBlack = "\033[90m" + BrightRed = "\033[91m" + BrightGreen = "\033[92m" + BrightYellow = "\033[93m" + BrightBlue = "\033[94m" + BrightMagenta = "\033[95m" + BrightCyan = "\033[96m" + BrightWhite = "\033[97m" + + // BgRed Background colors (if needed) + BgRed = "\033[41m" + BgGreen = "\033[42m" + BgYellow = "\033[43m" + BgBlue = "\033[44m" + BgMagenta = "\033[45m" + BgCyan = "\033[46m" + BgWhite = "\033[47m" +) diff --git a/errorhandler/errorhandler.go b/errorhandler/errorhandler.go new file mode 100644 index 0000000..36383d9 --- /dev/null +++ b/errorhandler/errorhandler.go @@ -0,0 +1,157 @@ +package errorhandler + +import ( + "errors" + "fmt" + "runtime" + "strings" +) + +// EnableColors determines whether colored output is enabled. +var EnableColors = true + +// TracedError represents an error with additional context, user-friendly messages, and stack trace information. +type TracedError struct { + OriginalError error + UserMessage string + Payload string + StackTrace []uintptr +} + +// Error implements the error interface for TracedError, returning a formatted error message. +func (te *TracedError) Error() string { + return fmt.Sprintf("%s\nUser Message: %s", te.OriginalError.Error(), te.UserMessage) +} + +// FailOnError formats the error message for any given error. +// If the error is a TracedError, it includes additional context. +func FailOnError(err error) string { + if err == nil { + return "" + } + + var tracedErr *TracedError + if errors.As(err, &tracedErr) { + return tracedErr.formatError() + } + + return err.Error() +} + +// captureCallers retrieves the call stack up to a certain depth. +func captureCallers() []uintptr { + const stackDepth = 64 + var pcs [stackDepth]uintptr + n := runtime.Callers(3, pcs[:]) + return pcs[:n] +} + +func formatWithColor(color, text string) string { + if EnableColors { + return fmt.Sprintf("%s%s%s", color, text, Reset) + } + return text +} + +// formatError formats the TracedError for detailed display, including payload, user message, and stack trace. +// optionally using colors for better readability. +func (te *TracedError) formatError() string { + var sb strings.Builder + + // Apply payload if available + if te.Payload != "" { + // Using BrightYellow for better visibility of metadata + sb.WriteString(fmt.Sprintf("%s: %s\n", formatWithColor(BrightYellow, "Payload"), te.Payload)) + } + + // Add error details header (using white for headers) + sb.WriteString(fmt.Sprintf("%s\n", formatWithColor(BrightWhite, "Error Details"))) + + // Original error in bright red for high visibility + sb.WriteString( + fmt.Sprintf( + "%s: %s\n", + formatWithColor(BrightRed, "Error"), + formatWithColor(Red, te.OriginalError.Error()), + ), + ) + + // Add user message if available (using cyan for info) + if te.UserMessage != "" { + sb.WriteString( + fmt.Sprintf( + "%s: %s\n", + formatWithColor(BrightCyan, "User Message"), + formatWithColor(Cyan, te.UserMessage), + ), + ) + } + + // Add stack trace header + sb.WriteString(fmt.Sprintf("\n%s\n", formatWithColor(BrightWhite, "Stack Trace"))) + frames := runtime.CallersFrames(te.StackTrace) + + for { + frame, more := frames.Next() + if !more { + break + } + + // Skip runtime-related functions + if strings.Contains(frame.Function, "runtime.") { + continue + } + + // Function name in bright blue for better visibility + sb.WriteString(fmt.Sprintf("%s\n", formatWithColor(BrightBlue, frame.Function))) + // File and line in dimmed white for less emphasis + sb.WriteString( + fmt.Sprintf( + "\t%s:%d\n", + formatWithColor(White, frame.File), + frame.Line, + ), + ) + } + + return sb.String() +} + +// WrapError wraps an existing error with additional context, including payload and a user-friendly message. +func WrapError(err error, payload, message string) error { + if err == nil { + return nil + } + + var existingTracedErr *TracedError + if errors.As(err, &existingTracedErr) { + // Merge existing TracedError context + if payload == "" { + payload = existingTracedErr.Payload + } + if message != "" && existingTracedErr.UserMessage != "" { + message = fmt.Sprintf("%s\n%s", existingTracedErr.UserMessage, message) + } else if message == "" { + message = existingTracedErr.UserMessage + } + return &TracedError{ + OriginalError: existingTracedErr.OriginalError, + UserMessage: message, + Payload: payload, + StackTrace: captureCallers(), + } + } + + // Create a new TracedError + return &TracedError{ + OriginalError: err, + UserMessage: message, + Payload: payload, + StackTrace: captureCallers(), + } +} + +// SetEnableColors sets the global EnableColors flag for controlling colored output. +func SetEnableColors(enable bool) { + EnableColors = enable +} diff --git a/errorhandler/errorhandler_test.go b/errorhandler/errorhandler_test.go new file mode 100644 index 0000000..6073de7 --- /dev/null +++ b/errorhandler/errorhandler_test.go @@ -0,0 +1,126 @@ +package errorhandler + +import ( + "errors" + "strings" + "testing" +) + +func TestWrapError(t *testing.T) { + err := errors.New("base error") + wrappedErr := WrapError(err, "payload", "additional message") + + if wrappedErr == nil { + t.Fatalf("expected non-nil error, got nil") + } + + var te *TracedError + ok := errors.As(wrappedErr, &te) + if !ok { + t.Fatalf("expected *TracedError, got %T", wrappedErr) + } + + if te.Payload != "payload" { + t.Errorf("expected payload to be 'payload', got %s", te.Payload) + } + + if te.UserMessage != "additional message" { + t.Errorf("expected user message to be 'additional message', got %s", te.UserMessage) + } + + if te.OriginalError.Error() != "base error" { + t.Errorf("expected original error message to be 'base error', got %s", te.OriginalError.Error()) + } +} + +func TestFailOnError(t *testing.T) { + // Test simple error + err := errors.New("simple error") + result := FailOnError(err) + if result != "simple error" { + t.Errorf("unexpected result: %s", result) + } + + // Test nil error + result = FailOnError(nil) + if result != "" { + t.Errorf("expected empty result for nil error, got %s", result) + } + + // Test TracedError + tracedErr := WrapError(err, "test payload", "test message") + result = FailOnError(tracedErr) + if !strings.Contains(result, "test message") { + t.Errorf("expected result to contain 'test message', got %s", result) + } +} + +func TestEnableColors(t *testing.T) { + // Save original state + originalState := EnableColors + + // Test with colors enabled + SetEnableColors(true) + if !EnableColors { + t.Fatalf("expected EnableColors to be true, got false") + } + + // Test with colors disabled + SetEnableColors(false) + if EnableColors { + t.Fatalf("expected EnableColors to be false, got true") + } + + // Restore original state + SetEnableColors(originalState) +} + +func TestFormatWithColor(t *testing.T) { + // Test with colors enabled + SetEnableColors(true) + coloredText := formatWithColor(Red, "test") + expected := "\033[31mtest\033[0m" + if coloredText != expected { + t.Errorf("expected colored text '%s', got '%s'", expected, coloredText) + } + + // Test with colors disabled + SetEnableColors(false) + coloredText = formatWithColor(Red, "test") + expected = "test" + if coloredText != expected { + t.Errorf("expected plain text 'test', got '%s'", coloredText) + } +} + +func TestTracedErrorStackTrace(t *testing.T) { + err := errors.New("stack trace error") + wrappedErr := WrapError(err, "", "") + var te *TracedError + if !errors.As(wrappedErr, &te) { + t.Fatalf("expected *TracedError, got %T", wrappedErr) + } + + if len(te.StackTrace) == 0 { + t.Error("expected non-empty stack trace, got empty stack trace") + } +} + +func TestWrapErrorWithExistingTracedError(t *testing.T) { + originalErr := errors.New("original error") + wrappedErr := WrapError(originalErr, "payload", "message") + doubleWrappedErr := WrapError(wrappedErr, "new payload", "new message") + + var te *TracedError + if !errors.As(doubleWrappedErr, &te) { + t.Fatalf("expected *TracedError, got %T", doubleWrappedErr) + } + + if te.Payload != "new payload" { + t.Errorf("expected payload to be 'new payload', got %s", te.Payload) + } + + if !strings.Contains(te.UserMessage, "new message") { + t.Errorf("expected user message to contain 'new message', got %s", te.UserMessage) + } +} diff --git a/example/main.go b/example/main.go new file mode 100644 index 0000000..f882ed3 --- /dev/null +++ b/example/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "errors" + "fmt" + + "github.com/Rahugg/go-stacktrace/errorhandler" +) + +func main() { + // Example usage + err := errors.New("an example error") + wrappedErr := errorhandler.WrapError(err, "some payload", "A user-friendly message") + fmt.Println(errorhandler.FailOnError(wrappedErr)) + + // Disable colors + errorhandler.SetEnableColors(false) + fmt.Println("With colors disabled:") + fmt.Println(errorhandler.FailOnError(wrappedErr)) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..4cf34af --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/Rahugg/go-stacktrace + +go 1.22.0