Error Metadata¶
Metadata is additional information attached to errors that provides crucial context for debugging and error handling. Think of metadata as tags or labels that give you deeper insight into what was happening when an error occurred. In ewrap, metadata is implemented as a flexible key-value store that travels with the error through your application.
Understanding Error Metadata¶
When an error occurs, the error message alone often doesn't tell the complete story. For example, if a database query fails, you might want to know:
- What query was being executed?
- How long did it take before failing?
- What parameters were used?
- How many retries were attempted?
Metadata allows you to capture all this contextual information in a structured way.
Basic Metadata Usage¶
Let's start with the fundamentals of adding and retrieving metadata:
func processUserOrder(userID string, orderID string) error {
err := processOrder(orderID)
if err != nil {
return ewrap.Wrap(err, "failed to process order").
WithMetadata("user_id", userID).
WithMetadata("order_id", orderID).
WithMetadata("timestamp", time.Now()).
WithMetadata("attempt", 1)
}
return nil
}
Retrieving metadata is just as straightforward:
func handleError(err error) {
if wrappedErr, ok := err.(*ewrap.Error); ok {
// Get specific metadata values
if userID, exists := wrappedErr.GetMetadata("user_id"); exists {
fmt.Printf("Error occurred for user: %v\n", userID)
}
// Log all metadata for debugging
if timestamp, exists := wrappedErr.GetMetadata("timestamp"); exists {
fmt.Printf("Error occurred at: %v\n", timestamp)
}
}
}
Structured Metadata Patterns¶
While metadata values can be of any type, it's often helpful to use structured data for complex information:
type QueryMetadata struct {
SQL string
Parameters []interface{}
Duration time.Duration
Table string
}
func executeQuery(query string, params ...interface{}) error {
start := time.Now()
result, err := db.Exec(query, params...)
if err != nil {
queryMeta := QueryMetadata{
SQL: query,
Parameters: params,
Duration: time.Since(start),
Table: extractTableName(query),
}
return ewrap.Wrap(err, "database query failed").
WithMetadata("query_info", queryMeta).
WithMetadata("query_attempt", 1)
}
return nil
}
Dynamic Metadata Collection¶
Sometimes you need to build metadata progressively as an operation proceeds:
func processComplexOperation(ctx context.Context, data []byte) error {
// Create error with initial metadata
err := ewrap.New("starting complex operation",
ewrap.WithContext(ctx, ewrap.ErrorTypeInternal, ewrap.SeverityInfo)).
WithMetadata("start_time", time.Now()).
WithMetadata("data_size", len(data))
// Process stages and collect metadata
stages := []string{"validation", "transformation", "storage"}
metrics := make(map[string]time.Duration)
for _, stage := range stages {
stageStart := time.Now()
if err := processStage(stage, data); err != nil {
// Add stage-specific metadata to error
return ewrap.Wrap(err, fmt.Sprintf("%s stage failed", stage)).
WithMetadata("failed_stage", stage).
WithMetadata("stage_metrics", metrics).
WithMetadata("stage_duration", time.Since(stageStart))
}
metrics[stage] = time.Since(stageStart)
}
return nil
}
Metadata for Debugging and Monitoring¶
Metadata is particularly valuable for debugging and monitoring. Here's a pattern that combines metadata with logging:
type OperationTracker struct {
StartTime time.Time
Steps []string
Metrics map[string]interface{}
Attributes map[string]string
}
func NewOperationTracker() *OperationTracker {
return &OperationTracker{
StartTime: time.Now(),
Steps: make([]string, 0),
Metrics: make(map[string]interface{}),
Attributes: make(map[string]string),
}
}
func (ot *OperationTracker) AddStep(step string) {
ot.Steps = append(ot.Steps, step)
}
func (ot *OperationTracker) AddMetric(key string, value interface{}) {
ot.Metrics[key] = value
}
func processWithTracking(ctx context.Context, data []byte) error {
tracker := NewOperationTracker()
// Track operation progress
err := func() error {
tracker.AddStep("initialization")
if err := validate(data); err != nil {
return ewrap.Wrap(err, "validation failed").
WithMetadata("tracker", tracker)
}
tracker.AddStep("validation")
if err := transform(data); err != nil {
return ewrap.Wrap(err, "transformation failed").
WithMetadata("tracker", tracker)
}
tracker.AddStep("transformation")
tracker.AddMetric("processing_time", time.Since(tracker.StartTime))
return nil
}()
if err != nil {
return ewrap.Wrap(err, "operation failed").
WithMetadata("final_state", tracker)
}
return nil
}
Metadata Best Practices¶
1. Keep Metadata Serializable¶
Ensure your metadata can be properly serialized when needed:
// Good - uses simple types
err = ewrap.New("processing failed").
WithMetadata("count", 42).
WithMetadata("status", "incomplete")
// Better - uses structured data that can be serialized
type ProcessMetadata struct {
Count int `json:"count"`
Status string `json:"status"`
Duration string `json:"duration"`
}
meta := ProcessMetadata{
Count: 42,
Status: "incomplete",
Duration: time.Since(start).String(),
}
err = ewrap.New("processing failed").
WithMetadata("process_info", meta)
2. Use Consistent Keys¶
Maintain consistent metadata keys across your application:
// Define common metadata keys as constants
const (
MetaKeyUserID = "user_id"
MetaKeyRequestID = "request_id"
MetaKeyDuration = "duration"
MetaKeyRetryCount = "retry_count"
)
func processRequest(ctx context.Context, userID string) error {
requestID := ctx.Value("request_id").(string)
start := time.Now()
err := performOperation()
if err != nil {
return ewrap.Wrap(err, "operation failed").
WithMetadata(MetaKeyUserID, userID).
WithMetadata(MetaKeyRequestID, requestID).
WithMetadata(MetaKeyDuration, time.Since(start))
}
return nil
}
3. Structure Complex Data¶
For complex metadata, use structured types:
type HTTPRequestMetadata struct {
Method string
URL string
StatusCode int
Duration time.Duration
Headers map[string][]string
}
func makeAPICall(ctx context.Context, req *http.Request) error {
start := time.Now()
resp, err := http.DefaultClient.Do(req)
requestMeta := HTTPRequestMetadata{
Method: req.Method,
URL: req.URL.String(),
Duration: time.Since(start),
}
if err != nil {
return ewrap.Wrap(err, "API call failed").
WithMetadata("request_details", requestMeta)
}
requestMeta.StatusCode = resp.StatusCode
requestMeta.Headers = resp.Header
if resp.StatusCode >= 400 {
return ewrap.New("API returned error status").
WithMetadata("request_details", requestMeta)
}
return nil
}