This guide will help you migrate from v0.4.1 to v0.5.0, which introduces native log/slog integration and removes the custom field system.
Version 0.5.0 is a breaking change release that replaces the custom field system with Go's native log/slog attributes:
- ❌ Removed:
FieldLogger,Loggerinterfaces - ❌ Removed: All field types (
BoolField,IntField, etc.) - ❌ Removed:
errors.Log(err, logger)function - ❌ Removed:
logging/logrusadapterpackage - ✅ Added: Native
slog.Attrsupport - ✅ Added:
slog.LogValuerimplementation - ✅ Added: Grouped attributes via
slog.Group - ✅ Added:
errors.Attrs(err)to extract attributes - ✅ Added:
errors.Log(ctx, logger, err)anderrors.LogLevel(ctx, logger, level, err)for slog logging - 📦 Minimum Go version: 1.21 (for
log/slogsupport)
- Update Go version to 1.21 or higher
- Remove imports of
errors/logging/logrusadapter - Replace old
errors.Log(err, logger)calls with newerrors.Log(ctx, logger, err)orerrors.LogLevel(ctx, logger, level, err)or direct slog usage - Update custom error types implementing
LoggableError - Update mock loggers in tests to use
errorstest.Logger - Consider using grouped attributes for better structure
// These interfaces no longer exist
type FieldLogger interface {
SetBool(key string, value bool)
SetInt(key string, value int)
// ... 9 more methods
}
type Logger interface {
FieldLogger
Log(message string)
}
type Field interface {
Set(logger FieldLogger)
}
// Field types like BoolField, IntField, etc. are removed// Use slog.Attr directly
import "log/slog"
// LoggableError now returns slog.Attr
type LoggableError interface {
Attrs() []slog.Attr
}import "github.com/muonsoft/errors/logging/logrusadapter"
err := errors.Wrap(dbErr, errors.String("table", "users"))
// Log with logrus adapter
logger := logrus.New()
logrusadapter.Log(err, logger)
// Or with custom logger
errors.Log(err, myLogger)import "log/slog"
err := errors.Wrap(dbErr, errors.String("table", "users"))
// Option 1: Use Log convenience function
errors.Log(ctx, slog.Default(), slog.LevelError, err)
// Option 2: Extract attributes and log manually
attrs := errors.Attrs(err)
slog.ErrorContext(ctx, err.Error(), attrsToAny(attrs)...)
// Option 3: Use LogValuer (error logs its attributes automatically)
slog.Error("database error", "error", err)type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return e.Message
}
func (e *ValidationError) LogFields(logger errors.FieldLogger) {
logger.SetString("field", e.Field)
logger.SetString("validation_error", e.Message)
}import "log/slog"
type ValidationError struct {
Field string
Message string
}
func (e *ValidationError) Error() string {
return e.Message
}
func (e *ValidationError) Attrs() []slog.Attr {
return []slog.Attr{
slog.String("field", e.Field),
slog.String("validation_error", e.Message),
}
}func TestMyError(t *testing.T) {
err := errors.Wrap(baseErr, errors.String("key", "value"))
loggable, ok := errors.As[errors.LoggableError](err)
require.True(t, ok)
logger := errorstest.NewLogger()
loggable.LogFields(logger)
logger.AssertField(t, "key", "value")
}func TestMyError(t *testing.T) {
err := errors.Wrap(baseErr, errors.String("key", "value"))
attrs := errors.Attrs(err)
require.NotEmpty(t, attrs)
// Option 1: Check attrs directly
require.Equal(t, "key", attrs[0].Key)
require.Equal(t, "value", attrs[0].Value.String())
// Option 2: Use mock logger
logger := errorstest.NewLogger()
logger.Attrs = attrs
logger.AssertField(t, "key", "value")
}All slog attribute types are now supported with dedicated functions:
// New in v0.5.0
errors.Int64(key string, value int64)
errors.Uint64(key string, value uint64)
errors.Float64(key string, value float64)
errors.Any(key string, value interface{}) // Replaces Value
// Deprecated
errors.Value(key string, value interface{}) // Use Any insteadExample:
err := errors.Wrap(
dbErr,
errors.Int64("timestamp", time.Now().Unix()),
errors.Uint64("bytes_processed", uint64(1024*1024)),
errors.Float64("cpu_usage", 0.75),
errors.Any("metadata", map[string]string{"region": "us-west"}),
)Group related attributes together for better structure:
err := errors.Wrap(
dbErr,
errors.Group("database",
slog.String("host", "localhost"),
slog.Int("port", 5432),
slog.String("name", "mydb"),
),
errors.Group("query",
slog.String("sql", "SELECT * FROM users"),
slog.Duration("duration", 150*time.Millisecond),
),
)
// JSON output:
// {
// "error": "...",
// "database": {
// "host": "localhost",
// "port": 5432,
// "name": "mydb"
// },
// "query": {
// "sql": "SELECT * FROM users",
// "duration": "150ms"
// }
// }
// %+v output:
// error message
// database.host: localhost
// database.port: 5432
// database.name: mydb
// query.sql: SELECT * FROM users
// query.duration: 150msv0.5.0 allows passing slog.Attr directly without wrapping in errors.Attr():
// You can pass slog.Attr directly (NEW!)
err := errors.Wrap(
err,
slog.Int64("timestamp", time.Now().Unix()),
slog.String("user", "john"),
slog.Group("metadata",
slog.String("version", "v1.2.3"),
slog.Bool("production", true),
),
)
// Or use helper functions (also works)
err := errors.Wrap(
err,
errors.Int64("timestamp", time.Now().Unix()),
errors.String("user", "john"),
)
// Or mix both styles
err := errors.Wrap(
err,
errors.SkipCaller(), // errors.Option
slog.String("user", "john"), // slog.Attr directly
errors.Int("id", 123), // errors.Option
)commonAttrs := []slog.Attr{
slog.String("service", "api"),
slog.String("version", "v1.0.0"),
}
err := errors.Wrap(err, errors.WithAttrs(commonAttrs...))Errors automatically work with slog:
err := errors.Wrap(
dbErr,
errors.String("table", "users"),
errors.Int("id", 123),
)
// The error's attributes are automatically included
slog.Error("operation failed", "error", err)go get -u github.com/muonsoft/errors@v0.5.0
go mod tidyUpdate your go.mod to require Go 1.21+:
go 1.21If you were using the logrus adapter:
// Remove this import
- import "github.com/muonsoft/errors/logging/logrusadapter"
// Replace logrusadapter.Log() calls
- logrusadapter.Log(err, logrusLogger)
// Option 1: Switch to slog
+ errors.Log(ctx, slog.Default(), err)
// Option 2: Create your own logrus adapter
+ // See "Custom Logger Adapters" section belowSearch for types implementing LogFields(logger FieldLogger):
# Find custom error types
grep -r "LogFields.*FieldLogger" .Update each one:
- func (e *MyError) LogFields(logger errors.FieldLogger) {
- logger.SetString("key", e.value)
- }
+ func (e *MyError) Attrs() []slog.Attr {
+ return []slog.Attr{
+ slog.String("key", e.value),
+ }
+ }Replace mock logger usage:
- logger := errorstest.NewLogger()
- errors.Log(err, logger)
- logger.AssertField(t, "key", "value")
+ attrs := errors.Attrs(err)
+ logger := errorstest.NewLogger()
+ logger.Attrs = attrs
+ logger.AssertField(t, "key", "value")Replace errors.Log() calls:
- errors.Log(err, myLogger)
+ // Option 1: Use Log
+ errors.Log(ctx, slog.Default(), err)
+ // Option 2: Extract and log
+ attrs := errors.Attrs(err)
+ slog.ErrorContext(ctx, err.Error(), attrsToAny(attrs)...)If you still need to use logrus or another logging library, create a simple adapter:
package myapp
import (
"log/slog"
"github.com/muonsoft/errors"
"github.com/sirupsen/logrus"
)
func LogWithLogrus(err error, logger *logrus.Logger) {
if err == nil {
return
}
// Extract attributes
attrs := errors.Attrs(err)
// Convert to logrus fields
fields := logrus.Fields{}
for _, attr := range attrs {
fields[attr.Key] = attrValue(attr)
}
// Log with logrus
logger.WithFields(fields).Error(err.Error())
}
func attrValue(attr slog.Attr) interface{} {
if attr.Value.Kind() == slog.KindGroup {
// Handle groups recursively
group := make(map[string]interface{})
for _, a := range attr.Value.Group() {
group[a.Key] = attrValue(a)
}
return group
}
return attr.Value.Any()
}package myapp
import (
"log/slog"
"github.com/muonsoft/errors"
"github.com/rs/zerolog"
)
func LogWithZerolog(err error, logger zerolog.Logger) {
if err == nil {
return
}
event := logger.Error()
// Add attributes
attrs := errors.Attrs(err)
for _, attr := range attrs {
addAttrToZerolog(event, attr)
}
event.Msg(err.Error())
}
func addAttrToZerolog(event *zerolog.Event, attr slog.Attr) {
switch attr.Value.Kind() {
case slog.KindString:
event.Str(attr.Key, attr.Value.String())
case slog.KindInt64:
event.Int64(attr.Key, attr.Value.Int64())
case slog.KindBool:
event.Bool(attr.Key, attr.Value.Bool())
case slog.KindGroup:
dict := zerolog.Dict()
for _, a := range attr.Value.Group() {
addAttrToDict(dict, a)
}
event.Dict(attr.Key, dict)
default:
event.Interface(attr.Key, attr.Value.Any())
}
}v0.5.0 has zero external dependencies. Everything is based on Go standard library.
Grouped attributes provide better organization:
{
"error": "database query failed",
"database": {
"host": "localhost",
"port": 5432,
"name": "production"
},
"query": {
"sql": "SELECT * FROM users WHERE id = ?",
"params": [123],
"duration": "150ms"
}
}Works seamlessly with any slog-compatible logger:
// OpenTelemetry
logger := otelslog.NewHandler(...)
// Custom handler
logger := slog.New(myHandler)
// Works the same way
errors.Log(ctx, logger, err)Testing is more straightforward with direct attribute access:
attrs := errors.Attrs(err)
require.Len(t, attrs, 3)
require.Equal(t, "user_id", attrs[0].Key)
require.Equal(t, int64(123), attrs[0].Value.Any())Cause: Import path is wrong or mixing v0.4.1 and v0.5.0.
Solution: Ensure you're using v0.5.0 consistently:
go get github.com/muonsoft/errors@v0.5.0
go mod tidyCause: Trying to use removed interface.
Solution: Implement LoggableError.Attrs() instead:
func (e *MyError) Attrs() []slog.Attr {
return []slog.Attr{
slog.String("key", e.Value),
}
}Cause: Trying to use []slog.Attr directly with slog methods.
Solution: Convert to []any or use Log():
// Option 1: Use Log
errors.LogLevel(ctx, logger, level, err)
// Option 2: Convert manually
attrs := errors.Attrs(err)
args := make([]any, len(attrs))
for i, a := range attrs {
args[i] = a
}
slog.ErrorContext(ctx, err.Error(), args...)Cause: Old mock logger usage.
Solution: Update to new mock logger API:
logger := errorstest.NewLogger()
logger.Attrs = errors.Attrs(err)
logger.AssertField(t, "key", "value")If you encounter issues during migration:
- Check examples directory for working code
- Open an issue on GitHub
- Start a discussion for questions
The migration to v0.5.0 modernizes the errors package by embracing Go's native log/slog. While it requires some code changes, the benefits include:
- ✅ Zero external dependencies
- ✅ Native slog integration
- ✅ Better structured logging with groups
- ✅ Simplified API
- ✅ Future-proof with Go's standard library
Most migrations can be completed in a few hours by following this guide.