diff --git a/CHANGELOG.md b/CHANGELOG.md index 9fdf6a9..6b1a149 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 } ``` +- Add `LogAttr` function to log any error with log/slog with the key "error". + If the provided error is wrapped with errtrace, it will log the full trace. + Otherwise, the original error message will be logged. - cmd/errtrace: Add `-no-wrapn` option to disable wrapping with generic `WrapN` functions. This is only useful for toolexec mode due to tooling limitations. @@ -38,6 +41,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Update `go` directive in go.mod to 1.21, and drop compatibility with Go 1.20 and earlier. +- Errors wrapped with errtrace are now compatible with log/slog-based loggers, + and will report the full error trace when logged. ### Fixed - cmd/errtrace: Don't exit with a non-zero status when `-h` is used. diff --git a/errtrace.go b/errtrace.go index 0633b1f..638f29b 100644 --- a/errtrace.go +++ b/errtrace.go @@ -56,6 +56,7 @@ package errtrace import ( "fmt" "io" + "log/slog" "strings" ) @@ -123,6 +124,11 @@ func (e *errTrace) Format(s fmt.State, verb rune) { fmt.Fprintf(s, fmt.FormatString(s, verb), e.err) } +// LogValue implements the [slog.LogValuer] interface. +func (e *errTrace) LogValue() slog.Value { + return slog.StringValue(FormatString(e)) +} + // TracePC returns the program counter for the location // in the frame that the error originated with. // diff --git a/example_trace_test.go b/example_trace_test.go index 1d93683..bd987c4 100644 --- a/example_trace_test.go +++ b/example_trace_test.go @@ -3,6 +3,7 @@ package errtrace_test import ( "errors" "fmt" + "log/slog" "runtime" "strings" @@ -80,3 +81,37 @@ func ExampleUnwrapFrame() { //braces.dev/errtrace_test.f3 // /path/to/errtrace/example_trace_test.go:3 } + +func Example_logWithSlog() { + // This example demonstrates how to log an errtrace-wrapped error + // with the slog package. + // Unlike LogAttr, we're able to use any key name here. + logger, printLogOutput := newExampleLogger() + + if err := f1(); err != nil { + logger.Error("f1 failed", "my-error", err) + } + + printLogOutput() + + // Output: + // {"level":"ERROR","msg":"f1 failed","my-error":"failed\n\nbraces.dev/errtrace_test.f3\n\t/path/to/errtrace/example_trace_test.go:3\nbraces.dev/errtrace_test.f2\n\t/path/to/errtrace/example_trace_test.go:2\nbraces.dev/errtrace_test.f1\n\t/path/to/errtrace/example_trace_test.go:1\n"} +} + +// newExampleLogger creates a new slog.Logger for use in examples. +// It omits timestamps from the output to allow for output matching, +// and cleans paths in trace output to make them environment-agnostic. +func newExampleLogger() (logger *slog.Logger, printOutput func()) { + var buf strings.Builder + return slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{ + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if len(groups) == 0 && a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + })), func() { + fmt.Println(tracetest.MustClean(buf.String())) + buf.Reset() + } +}