mirror of
https://github.com/bitechdev/ResolveSpec.git
synced 2026-02-14 20:36:08 +00:00
Tracking provider
This commit is contained in:
150
pkg/errortracking/README.md
Normal file
150
pkg/errortracking/README.md
Normal file
@@ -0,0 +1,150 @@
|
||||
# Error Tracking
|
||||
|
||||
This package provides error tracking integration for ResolveSpec, with built-in support for Sentry.
|
||||
|
||||
## Features
|
||||
|
||||
- **Provider Interface**: Flexible design supporting multiple error tracking backends
|
||||
- **Sentry Integration**: Full-featured Sentry support with automatic error, warning, and panic tracking
|
||||
- **Automatic Logger Integration**: All `logger.Error()` and `logger.Warn()` calls are automatically sent to the error tracker
|
||||
- **Panic Tracking**: Automatic panic capture with stack traces
|
||||
- **NoOp Provider**: Zero-overhead when error tracking is disabled
|
||||
|
||||
## Configuration
|
||||
|
||||
Add error tracking configuration to your config file:
|
||||
|
||||
```yaml
|
||||
error_tracking:
|
||||
enabled: true
|
||||
provider: "sentry" # Currently supports: "sentry" or "noop"
|
||||
dsn: "https://your-sentry-dsn@sentry.io/project-id"
|
||||
environment: "production" # e.g., production, staging, development
|
||||
release: "v1.0.0" # Your application version
|
||||
debug: false
|
||||
sample_rate: 1.0 # Error sample rate (0.0-1.0)
|
||||
traces_sample_rate: 0.1 # Traces sample rate (0.0-1.0)
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Initialization
|
||||
|
||||
Initialize error tracking in your application startup:
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/bitechdev/ResolveSpec/pkg/config"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/errortracking"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Load your configuration
|
||||
cfg := config.Config{
|
||||
ErrorTracking: config.ErrorTrackingConfig{
|
||||
Enabled: true,
|
||||
Provider: "sentry",
|
||||
DSN: "https://your-sentry-dsn@sentry.io/project-id",
|
||||
Environment: "production",
|
||||
Release: "v1.0.0",
|
||||
SampleRate: 1.0,
|
||||
},
|
||||
}
|
||||
|
||||
// Initialize logger
|
||||
logger.Init(false)
|
||||
|
||||
// Initialize error tracking
|
||||
provider, err := errortracking.NewProviderFromConfig(cfg.ErrorTracking)
|
||||
if err != nil {
|
||||
logger.Error("Failed to initialize error tracking: %v", err)
|
||||
} else {
|
||||
logger.InitErrorTracking(provider)
|
||||
}
|
||||
|
||||
// Your application code...
|
||||
|
||||
// Cleanup on shutdown
|
||||
defer logger.CloseErrorTracking()
|
||||
}
|
||||
```
|
||||
|
||||
### Automatic Tracking
|
||||
|
||||
Once initialized, all logger errors and warnings are automatically sent to the error tracker:
|
||||
|
||||
```go
|
||||
// This will be logged AND sent to Sentry
|
||||
logger.Error("Database connection failed: %v", err)
|
||||
|
||||
// This will also be logged AND sent to Sentry
|
||||
logger.Warn("Cache miss for key: %s", key)
|
||||
```
|
||||
|
||||
### Panic Tracking
|
||||
|
||||
Panics are automatically captured when using the logger's panic handlers:
|
||||
|
||||
```go
|
||||
// Using CatchPanic
|
||||
defer logger.CatchPanic("MyFunction")
|
||||
|
||||
// Using CatchPanicCallback
|
||||
defer logger.CatchPanicCallback("MyFunction", func(err any) {
|
||||
// Custom cleanup
|
||||
})
|
||||
|
||||
// Using HandlePanic
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
err = logger.HandlePanic("MyMethod", r)
|
||||
}
|
||||
}()
|
||||
```
|
||||
|
||||
### Manual Tracking
|
||||
|
||||
You can also use the provider directly for custom error tracking:
|
||||
|
||||
```go
|
||||
import (
|
||||
"context"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/errortracking"
|
||||
"github.com/bitechdev/ResolveSpec/pkg/logger"
|
||||
)
|
||||
|
||||
func someFunction() {
|
||||
tracker := logger.GetErrorTracker()
|
||||
if tracker != nil {
|
||||
// Capture an error
|
||||
tracker.CaptureError(context.Background(), err, errortracking.SeverityError, map[string]interface{}{
|
||||
"user_id": userID,
|
||||
"request_id": requestID,
|
||||
})
|
||||
|
||||
// Capture a message
|
||||
tracker.CaptureMessage(context.Background(), "Important event occurred", errortracking.SeverityInfo, map[string]interface{}{
|
||||
"event_type": "user_signup",
|
||||
})
|
||||
|
||||
// Capture a panic
|
||||
tracker.CapturePanic(context.Background(), recovered, stackTrace, map[string]interface{}{
|
||||
"context": "background_job",
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Severity Levels
|
||||
|
||||
The package supports the following severity levels:
|
||||
|
||||
- `SeverityError`: For errors that should be tracked and investigated
|
||||
- `SeverityWarning`: For warnings that may indicate potential issues
|
||||
- `SeverityInfo`: For informational messages
|
||||
- `SeverityDebug`: For debug-level information
|
||||
|
||||
```
|
||||
67
pkg/errortracking/errortracking_test.go
Normal file
67
pkg/errortracking/errortracking_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package errortracking
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNoOpProvider(t *testing.T) {
|
||||
provider := NewNoOpProvider()
|
||||
|
||||
// Test that all methods can be called without panicking
|
||||
t.Run("CaptureError", func(t *testing.T) {
|
||||
provider.CaptureError(context.Background(), errors.New("test error"), SeverityError, nil)
|
||||
})
|
||||
|
||||
t.Run("CaptureMessage", func(t *testing.T) {
|
||||
provider.CaptureMessage(context.Background(), "test message", SeverityWarning, nil)
|
||||
})
|
||||
|
||||
t.Run("CapturePanic", func(t *testing.T) {
|
||||
provider.CapturePanic(context.Background(), "panic!", []byte("stack trace"), nil)
|
||||
})
|
||||
|
||||
t.Run("Flush", func(t *testing.T) {
|
||||
result := provider.Flush(5)
|
||||
if !result {
|
||||
t.Error("Expected Flush to return true")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Close", func(t *testing.T) {
|
||||
err := provider.Close()
|
||||
if err != nil {
|
||||
t.Errorf("Expected Close to return nil, got %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestSeverityLevels(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
severity Severity
|
||||
expected string
|
||||
}{
|
||||
{"Error", SeverityError, "error"},
|
||||
{"Warning", SeverityWarning, "warning"},
|
||||
{"Info", SeverityInfo, "info"},
|
||||
{"Debug", SeverityDebug, "debug"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if string(tt.severity) != tt.expected {
|
||||
t.Errorf("Expected %s, got %s", tt.expected, string(tt.severity))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProviderInterface(t *testing.T) {
|
||||
// Test that NoOpProvider implements Provider interface
|
||||
var _ Provider = (*NoOpProvider)(nil)
|
||||
|
||||
// Test that SentryProvider implements Provider interface
|
||||
var _ Provider = (*SentryProvider)(nil)
|
||||
}
|
||||
33
pkg/errortracking/factory.go
Normal file
33
pkg/errortracking/factory.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package errortracking
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/bitechdev/ResolveSpec/pkg/config"
|
||||
)
|
||||
|
||||
// NewProviderFromConfig creates an error tracking provider based on the configuration
|
||||
func NewProviderFromConfig(cfg config.ErrorTrackingConfig) (Provider, error) {
|
||||
if !cfg.Enabled {
|
||||
return NewNoOpProvider(), nil
|
||||
}
|
||||
|
||||
switch cfg.Provider {
|
||||
case "sentry":
|
||||
if cfg.DSN == "" {
|
||||
return nil, fmt.Errorf("sentry DSN is required when error tracking is enabled")
|
||||
}
|
||||
return NewSentryProvider(SentryConfig{
|
||||
DSN: cfg.DSN,
|
||||
Environment: cfg.Environment,
|
||||
Release: cfg.Release,
|
||||
Debug: cfg.Debug,
|
||||
SampleRate: cfg.SampleRate,
|
||||
TracesSampleRate: cfg.TracesSampleRate,
|
||||
})
|
||||
case "noop", "":
|
||||
return NewNoOpProvider(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown error tracking provider: %s", cfg.Provider)
|
||||
}
|
||||
}
|
||||
33
pkg/errortracking/interfaces.go
Normal file
33
pkg/errortracking/interfaces.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package errortracking
|
||||
|
||||
import (
|
||||
"context"
|
||||
)
|
||||
|
||||
// Severity represents the severity level of an error
|
||||
type Severity string
|
||||
|
||||
const (
|
||||
SeverityError Severity = "error"
|
||||
SeverityWarning Severity = "warning"
|
||||
SeverityInfo Severity = "info"
|
||||
SeverityDebug Severity = "debug"
|
||||
)
|
||||
|
||||
// Provider defines the interface for error tracking providers
|
||||
type Provider interface {
|
||||
// CaptureError captures an error with the given severity and additional context
|
||||
CaptureError(ctx context.Context, err error, severity Severity, extra map[string]interface{})
|
||||
|
||||
// CaptureMessage captures a message with the given severity and additional context
|
||||
CaptureMessage(ctx context.Context, message string, severity Severity, extra map[string]interface{})
|
||||
|
||||
// CapturePanic captures a panic with stack trace
|
||||
CapturePanic(ctx context.Context, recovered interface{}, stackTrace []byte, extra map[string]interface{})
|
||||
|
||||
// Flush waits for all events to be sent (useful for graceful shutdown)
|
||||
Flush(timeout int) bool
|
||||
|
||||
// Close closes the provider and releases resources
|
||||
Close() error
|
||||
}
|
||||
37
pkg/errortracking/noop.go
Normal file
37
pkg/errortracking/noop.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package errortracking
|
||||
|
||||
import "context"
|
||||
|
||||
// NoOpProvider is a no-op implementation of the Provider interface
|
||||
// Used when error tracking is disabled
|
||||
type NoOpProvider struct{}
|
||||
|
||||
// NewNoOpProvider creates a new NoOp provider
|
||||
func NewNoOpProvider() *NoOpProvider {
|
||||
return &NoOpProvider{}
|
||||
}
|
||||
|
||||
// CaptureError does nothing
|
||||
func (n *NoOpProvider) CaptureError(ctx context.Context, err error, severity Severity, extra map[string]interface{}) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
// CaptureMessage does nothing
|
||||
func (n *NoOpProvider) CaptureMessage(ctx context.Context, message string, severity Severity, extra map[string]interface{}) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
// CapturePanic does nothing
|
||||
func (n *NoOpProvider) CapturePanic(ctx context.Context, recovered interface{}, stackTrace []byte, extra map[string]interface{}) {
|
||||
// No-op
|
||||
}
|
||||
|
||||
// Flush does nothing and returns true
|
||||
func (n *NoOpProvider) Flush(timeout int) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Close does nothing
|
||||
func (n *NoOpProvider) Close() error {
|
||||
return nil
|
||||
}
|
||||
154
pkg/errortracking/sentry.go
Normal file
154
pkg/errortracking/sentry.go
Normal file
@@ -0,0 +1,154 @@
|
||||
package errortracking
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/getsentry/sentry-go"
|
||||
)
|
||||
|
||||
// SentryProvider implements the Provider interface using Sentry
|
||||
type SentryProvider struct {
|
||||
hub *sentry.Hub
|
||||
}
|
||||
|
||||
// SentryConfig holds the configuration for Sentry
|
||||
type SentryConfig struct {
|
||||
DSN string
|
||||
Environment string
|
||||
Release string
|
||||
Debug bool
|
||||
SampleRate float64
|
||||
TracesSampleRate float64
|
||||
}
|
||||
|
||||
// NewSentryProvider creates a new Sentry provider
|
||||
func NewSentryProvider(config SentryConfig) (*SentryProvider, error) {
|
||||
err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: config.DSN,
|
||||
Environment: config.Environment,
|
||||
Release: config.Release,
|
||||
Debug: config.Debug,
|
||||
AttachStacktrace: true,
|
||||
SampleRate: config.SampleRate,
|
||||
TracesSampleRate: config.TracesSampleRate,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to initialize Sentry: %w", err)
|
||||
}
|
||||
|
||||
return &SentryProvider{
|
||||
hub: sentry.CurrentHub(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CaptureError captures an error with the given severity and additional context
|
||||
func (s *SentryProvider) CaptureError(ctx context.Context, err error, severity Severity, extra map[string]interface{}) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
|
||||
hub := sentry.GetHubFromContext(ctx)
|
||||
if hub == nil {
|
||||
hub = s.hub
|
||||
}
|
||||
|
||||
event := sentry.NewEvent()
|
||||
event.Level = s.convertSeverity(severity)
|
||||
event.Message = err.Error()
|
||||
event.Exception = []sentry.Exception{
|
||||
{
|
||||
Value: err.Error(),
|
||||
Type: fmt.Sprintf("%T", err),
|
||||
Stacktrace: sentry.ExtractStacktrace(err),
|
||||
},
|
||||
}
|
||||
|
||||
if extra != nil {
|
||||
event.Extra = extra
|
||||
}
|
||||
|
||||
hub.CaptureEvent(event)
|
||||
}
|
||||
|
||||
// CaptureMessage captures a message with the given severity and additional context
|
||||
func (s *SentryProvider) CaptureMessage(ctx context.Context, message string, severity Severity, extra map[string]interface{}) {
|
||||
if message == "" {
|
||||
return
|
||||
}
|
||||
|
||||
hub := sentry.GetHubFromContext(ctx)
|
||||
if hub == nil {
|
||||
hub = s.hub
|
||||
}
|
||||
|
||||
event := sentry.NewEvent()
|
||||
event.Level = s.convertSeverity(severity)
|
||||
event.Message = message
|
||||
|
||||
if extra != nil {
|
||||
event.Extra = extra
|
||||
}
|
||||
|
||||
hub.CaptureEvent(event)
|
||||
}
|
||||
|
||||
// CapturePanic captures a panic with stack trace
|
||||
func (s *SentryProvider) CapturePanic(ctx context.Context, recovered interface{}, stackTrace []byte, extra map[string]interface{}) {
|
||||
if recovered == nil {
|
||||
return
|
||||
}
|
||||
|
||||
hub := sentry.GetHubFromContext(ctx)
|
||||
if hub == nil {
|
||||
hub = s.hub
|
||||
}
|
||||
|
||||
event := sentry.NewEvent()
|
||||
event.Level = sentry.LevelError
|
||||
event.Message = fmt.Sprintf("Panic: %v", recovered)
|
||||
event.Exception = []sentry.Exception{
|
||||
{
|
||||
Value: fmt.Sprintf("%v", recovered),
|
||||
Type: "panic",
|
||||
},
|
||||
}
|
||||
|
||||
if extra != nil {
|
||||
event.Extra = extra
|
||||
}
|
||||
|
||||
if stackTrace != nil {
|
||||
event.Extra["stack_trace"] = string(stackTrace)
|
||||
}
|
||||
|
||||
hub.CaptureEvent(event)
|
||||
}
|
||||
|
||||
// Flush waits for all events to be sent (useful for graceful shutdown)
|
||||
func (s *SentryProvider) Flush(timeout int) bool {
|
||||
return sentry.Flush(time.Duration(timeout) * time.Second)
|
||||
}
|
||||
|
||||
// Close closes the provider and releases resources
|
||||
func (s *SentryProvider) Close() error {
|
||||
sentry.Flush(2 * time.Second)
|
||||
return nil
|
||||
}
|
||||
|
||||
// convertSeverity converts our Severity to Sentry's Level
|
||||
func (s *SentryProvider) convertSeverity(severity Severity) sentry.Level {
|
||||
switch severity {
|
||||
case SeverityError:
|
||||
return sentry.LevelError
|
||||
case SeverityWarning:
|
||||
return sentry.LevelWarning
|
||||
case SeverityInfo:
|
||||
return sentry.LevelInfo
|
||||
case SeverityDebug:
|
||||
return sentry.LevelDebug
|
||||
default:
|
||||
return sentry.LevelError
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user