Logging Integration¶
ewrap defines a tiny three-method Logger interface and ships a single adapter (for stdlib log/slog) in a sibling subpackage. Adapters for zap, zerolog, logrus, glog, etc. are intentionally not bundled: the interface is so small that a working adapter is well under ten lines of your own code.
The interface¶
type Logger interface {
Error(msg string, keysAndValues ...any)
Debug(msg string, keysAndValues ...any)
Info(msg string, keysAndValues ...any)
}
keysAndValues is the standard structured-logging convention: alternating key/value pairs after the message. Implementations must be goroutine-safe because (*Error).Log calls them synchronously from the calling goroutine.
Attaching a logger¶
err := ewrap.New("payment failed", ewrap.WithLogger(logger))
err.Log() // emits an "error occurred" record with all attached fields
(*Error).Log emits a single record containing:
error— the messagecause—e.cause.Error()if the chain has onestack— formatted stack trace- every key/value from the metadata map
recovery_message,recovery_actions,recovery_documentationifWithRecoverySuggestionwas used
The logger reference is also inherited by Wrap when the inner error is already a *Error, so a single WithLogger near the root propagates out.
Slog adapter¶
Stdlib log/slog is the recommended target for new projects. The adapter is in ewrap/slog:
import (
"log/slog"
"os"
"github.com/hyp3rd/ewrap"
ewrapslog "github.com/hyp3rd/ewrap/slog"
)
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})
logger := ewrapslog.New(slog.New(handler))
err := ewrap.New("payment failed", ewrap.WithLogger(logger))
err.Log()
If you'd rather log the error directly via slog, you don't need an adapter at all — *Error implements slog.LogValuer:
slog.Error("payment failed", "err", err)
// emits structured fields: message, type, severity, request_id, cause,
// metadata, recovery — all without the adapter
See fmt.Formatter & slog for the LogValuer details.
Writing an adapter for another logger¶
The whole adapter is three methods. Here's zap:
import "go.uber.org/zap"
type ZapAdapter struct{ l *zap.SugaredLogger }
func NewZap(l *zap.Logger) *ZapAdapter { return &ZapAdapter{l: l.Sugar()} }
func (a *ZapAdapter) Error(msg string, kv ...any) { a.l.Errorw(msg, kv...) }
func (a *ZapAdapter) Debug(msg string, kv ...any) { a.l.Debugw(msg, kv...) }
func (a *ZapAdapter) Info(msg string, kv ...any) { a.l.Infow(msg, kv...) }
logrus:
import "github.com/sirupsen/logrus"
type LogrusAdapter struct{ l *logrus.Logger }
func NewLogrus(l *logrus.Logger) *LogrusAdapter { return &LogrusAdapter{l: l} }
func (a *LogrusAdapter) emit(level logrus.Level, msg string, kv []any) {
fields := logrus.Fields{}
for i := 0; i+1 < len(kv); i += 2 {
if k, ok := kv[i].(string); ok {
fields[k] = kv[i+1]
}
}
a.l.WithFields(fields).Log(level, msg)
}
func (a *LogrusAdapter) Error(msg string, kv ...any) { a.emit(logrus.ErrorLevel, msg, kv) }
func (a *LogrusAdapter) Debug(msg string, kv ...any) { a.emit(logrus.DebugLevel, msg, kv) }
func (a *LogrusAdapter) Info(msg string, kv ...any) { a.emit(logrus.InfoLevel, msg, kv) }
zerolog:
import "github.com/rs/zerolog"
type ZerologAdapter struct{ l zerolog.Logger }
func NewZerolog(l zerolog.Logger) *ZerologAdapter { return &ZerologAdapter{l: l} }
func (a *ZerologAdapter) emit(ev *zerolog.Event, msg string, kv []any) {
for i := 0; i+1 < len(kv); i += 2 {
if k, ok := kv[i].(string); ok {
ev = ev.Interface(k, kv[i+1])
}
}
ev.Msg(msg)
}
func (a *ZerologAdapter) Error(msg string, kv ...any) { a.emit(a.l.Error(), msg, kv) }
func (a *ZerologAdapter) Debug(msg string, kv ...any) { a.emit(a.l.Debug(), msg, kv) }
func (a *ZerologAdapter) Info(msg string, kv ...any) { a.emit(a.l.Info(), msg, kv) }
Drop one of these into your codebase, pass an instance to WithLogger, and you're done. ewrap stays free of those dependencies.
Recovery suggestions in log output¶
When you attach a RecoverySuggestion, (*Error).Log automatically expands it into structured fields:
err := ewrap.New("DB unreachable",
ewrap.WithLogger(logger),
ewrap.WithRecoverySuggestion(&ewrap.RecoverySuggestion{
Message: "Verify pool sizing and credentials.",
Actions: []string{"reset pool", "rotate creds"},
Documentation: "https://runbooks.example.com/db",
}))
err.Log()
// Fields emitted: error, stack, recovery_message, recovery_actions,
// recovery_documentation
Best practices¶
- Set the logger near the root so wraps inherit it, instead of threading
WithLoggerthrough every layer. - Don't log inside libraries — return the error and let the caller decide.
WithLoggeris for application-layer code. - Use slog directly for new projects unless you've already standardised on another logger.
LogValuergives you fully structured output with no adapter at all. - Keep adapters in a single internal package in your own repo so all of your services share the same logger choice without ewrap having to pick one.