Skip to content

Interfaces

The ewrap package is built around several key interfaces that provide flexibility and extensibility. Understanding these interfaces is crucial for effectively integrating ewrap into your application and extending its functionality to meet your specific needs.

Core Interfaces

Logger Interface

The Logger interface is fundamental to ewrap's logging capabilities. It provides a standardized way to log error information at different severity levels:

type Logger interface {
    Error(msg string, keysAndValues ...interface{})
    Debug(msg string, keysAndValues ...interface{})
    Info(msg string, keysAndValues ...interface{})
}

This interface is intentionally simple yet powerful. Let's explore how to implement and use it effectively:

// Example implementation using standard log package
type StandardLogger struct {
    logger *log.Logger
}

func (l *StandardLogger) Error(msg string, keysAndValues ...interface{}) {
    l.logger.Printf("ERROR: %s %v", msg, formatKeyValues(keysAndValues...))
}

func (l *StandardLogger) Debug(msg string, keysAndValues ...interface{}) {
    l.logger.Printf("DEBUG: %s %v", msg, formatKeyValues(keysAndValues...))
}

func (l *StandardLogger) Info(msg string, keysAndValues ...interface{}) {
    l.logger.Printf("INFO: %s %v", msg, formatKeyValues(keysAndValues...))
}

// Helper function to format key-value pairs
func formatKeyValues(keysAndValues ...interface{}) string {
    var pairs []string
    for i := 0; i < len(keysAndValues); i += 2 {
        if i+1 < len(keysAndValues) {
            pairs = append(pairs, fmt.Sprintf("%v=%v",
                keysAndValues[i], keysAndValues[i+1]))
        }
    }
    return strings.Join(pairs, " ")
}

The Logger interface is designed to:

  • Support structured logging through key-value pairs
  • Provide different log levels for appropriate error handling
  • Be easily implemented by existing logging frameworks
  • Allow for context-aware logging

Error Interface

While ewrap's Error type implements Go's standard error interface, it extends it with additional capabilities:

type error interface {
    Error() string
}

// ewrap.Error implements these additional methods:
type Error struct {
    // Cause returns the underlying cause of the error
    Cause() error

    // Stack returns the error's stack trace
    Stack() string

    // GetMetadata retrieves metadata associated with the error
    GetMetadata(key string) (interface{}, bool)

    // WithMetadata adds metadata to the error
    WithMetadata(key string, value interface{}) *Error

    // Is reports whether target matches err in the error chain
    Is(target error) bool

    // Unwrap provides compatibility with Go 1.13 error chains
    Unwrap() error
}

Understanding these interfaces helps when working with errors:

func processError(err error) {
    // Type assert to access ewrap.Error functionality
    if wrappedErr, ok := err.(*ewrap.Error); ok {
        // Access stack trace
        fmt.Printf("Stack Trace:\n%s\n", wrappedErr.Stack())

        // Access metadata
        if requestID, ok := wrappedErr.GetMetadata("request_id"); ok {
            fmt.Printf("Request ID: %v\n", requestID)
        }

        // Access cause chain
        for cause := wrappedErr; cause != nil; cause = cause.Unwrap() {
            fmt.Printf("Error: %s\n", cause.Error())
        }
    }
}

Interface Integration

Implementing Custom Loggers

Creating custom loggers for different environments or logging systems:

// Production logger with structured output
type ProductionLogger struct {
    output io.Writer
}

func (l *ProductionLogger) Error(msg string, keysAndValues ...interface{}) {
    entry := LogEntry{
        Level:     "ERROR",
        Message:   msg,
        Timestamp: time.Now().UTC(),
        Data:      makeMap(keysAndValues...),
    }
    json.NewEncoder(l.output).Encode(entry)
}

// Test logger for capturing logs in tests
type TestLogger struct {
    Logs []LogEntry
    mu   sync.Mutex
}

func (l *TestLogger) Error(msg string, keysAndValues ...interface{}) {
    l.mu.Lock()
    defer l.mu.Unlock()

    l.Logs = append(l.Logs, LogEntry{
        Level:     "ERROR",
        Message:   msg,
        Timestamp: time.Now(),
        Data:      makeMap(keysAndValues...),
    })
}

Interface Composition

Combining interfaces for enhanced functionality:

// MetricsLogger combines logging with metrics collection
type MetricsLogger struct {
    logger  Logger
    metrics MetricsCollector
}

func (l *MetricsLogger) Error(msg string, keysAndValues ...interface{}) {
    // Log the error
    l.logger.Error(msg, keysAndValues...)

    // Collect metrics
    l.metrics.IncrementCounter("errors_total", 1)
    l.metrics.Record("error_occurred", keysAndValues...)
}

Best Practices

Logger Implementation

When implementing the Logger interface, consider these guidelines:

// Structured logger with configurable output
type StructuredLogger struct {
    output    io.Writer
    minLevel  LogLevel
    formatter LogFormatter
    mu        sync.Mutex
}

func (l *StructuredLogger) Error(msg string, keysAndValues ...interface{}) {
    if l.minLevel <= ErrorLevel {
        l.log(ErrorLevel, msg, keysAndValues...)
    }
}

func (l *StructuredLogger) log(level LogLevel, msg string, keysAndValues ...interface{}) {
    l.mu.Lock()
    defer l.mu.Unlock()

    entry := l.formatter.Format(level, msg, keysAndValues...)
    l.output.Write(entry)
}

Interface Testing

Writing tests for interface implementations:

func TestLogger(t *testing.T) {
    // Create a buffer to capture log output
    var buf bytes.Buffer
    logger := NewStructuredLogger(&buf)

    // Test error logging
    logger.Error("test error",
        "key1", "value1",
        "key2", 42)

    // Verify log output
    output := buf.String()
    if !strings.Contains(output, "test error") {
        t.Error("Log message not found in output")
    }

    // Verify structured data
    var logEntry LogEntry
    if err := json.NewDecoder(&buf).Decode(&logEntry); err != nil {
        t.Fatal(err)
    }

    if logEntry.Level != "ERROR" {
        t.Errorf("Expected level ERROR, got %s", logEntry.Level)
    }
}

Interface Extensions

Creating specialized interfaces for specific use cases:

// ContextualLogger adds context awareness to the basic Logger interface
type ContextualLogger interface {
    Logger
    WithContext(ctx context.Context) Logger
    WithFields(fields map[string]interface{}) Logger
}

// Implementation example
type contextualLogger struct {
    Logger
    ctx    context.Context
    fields map[string]interface{}
}

func (l *contextualLogger) WithContext(ctx context.Context) Logger {
    return &contextualLogger{
        Logger: l.Logger,
        ctx:    ctx,
        fields: l.fields,
    }
}

func (l *contextualLogger) Error(msg string, keysAndValues ...interface{}) {
    // Combine context values, fields, and provided key-values
    allKeyValues := l.mergeContextAndFields(keysAndValues...)
    l.Logger.Error(msg, allKeyValues...)
}