From c729ddadca4491b90764c87e2c48ecaa67778445 Mon Sep 17 00:00:00 2001 From: Steve Coffman Date: Sun, 14 Apr 2024 20:38:56 -0400 Subject: [PATCH 1/5] Add slog compatibility Co-authored-by: Abhinav Gupta Co-authored-by: Chris Bandy Signed-off-by: Steve Coffman --- errtrace.go | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/errtrace.go b/errtrace.go index 0633b1f..e56a770 100644 --- a/errtrace.go +++ b/errtrace.go @@ -56,6 +56,7 @@ package errtrace import ( "fmt" "io" + "log/slog" "strings" ) @@ -101,6 +102,18 @@ func FormatString(target error) string { return s.String() } +// LogAttr builds a slog attribute for an error. +// It will log the error with an error trace +// if the error has been wrapped with this package. +// Otherwise, the error message will be logged as is. +// +// Usage: +// +// slog.Default().Error("msg here", errtrace.LogAttr(err)) +func LogAttr(err error) slog.Attr { + return slog.Any("error", err) +} + type errTrace struct { err error pc uintptr @@ -123,6 +136,19 @@ 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 { + var s strings.Builder + if err := Format(&s, e); err != nil { + return slog.GroupValue( + slog.String("message", e.Error()), + slog.Any("formatErr", err), + ) + } + + return slog.StringValue(s.String()) +} + // TracePC returns the program counter for the location // in the frame that the error originated with. // From b4feed79a85846329977fcae636284c5186dd852 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Sun, 1 Jun 2025 11:03:49 -0700 Subject: [PATCH 2/5] example tests and changelog --- CHANGELOG.md | 5 ++++ errtrace.go | 11 ++++----- example_trace_test.go | 55 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 7 deletions(-) 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 e56a770..8d69170 100644 --- a/errtrace.go +++ b/errtrace.go @@ -102,14 +102,11 @@ func FormatString(target error) string { return s.String() } -// LogAttr builds a slog attribute for an error. -// It will log the error with an error trace -// if the error has been wrapped with this package. -// Otherwise, the error message will be logged as is. +// LogAttr builds a slog attribute for an error with the key "error". // -// Usage: -// -// slog.Default().Error("msg here", errtrace.LogAttr(err)) +// When serialized with a slog-based logger, +// this will report an error return trace if the error has one, +// otherwise the original error message will be logged as-is. func LogAttr(err error) slog.Attr { return slog.Any("error", err) } diff --git a/example_trace_test.go b/example_trace_test.go index 1d93683..9bd5077 100644 --- a/example_trace_test.go +++ b/example_trace_test.go @@ -3,6 +3,8 @@ package errtrace_test import ( "errors" "fmt" + "log/slog" + "os" "runtime" "strings" @@ -80,3 +82,56 @@ func ExampleUnwrapFrame() { //braces.dev/errtrace_test.f3 // /path/to/errtrace/example_trace_test.go:3 } + +func ExampleLogAttr() { + // This example demonstrates use of the LogAttr function. + // The LogAttr function always uses the "error" key. + logger := newExampleLogger() + + if err := f1(); err != nil { + logger.Error("f1 failed", errtrace.LogAttr(err)) + } + + // Output: + // {"level":"ERROR","msg":"f1 failed","error":"failed\n\nbraces.dev/errtrace_test.f3\n\t/Users/abg/src/braces.dev/errtrace/example_trace_test.go:24\nbraces.dev/errtrace_test.f2\n\t/Users/abg/src/braces.dev/errtrace/example_trace_test.go:20\nbraces.dev/errtrace_test.f1\n\t/Users/abg/src/braces.dev/errtrace/example_trace_test.go:16\n"} +} + +func ExampleLogAttr_noTrace() { + // LogAttr reports the original error message + // if the error does not have a trace attached to it. + logger := newExampleLogger() + + if err := errors.New("no trace"); err != nil { + logger.Error("something broke", errtrace.LogAttr(err)) + } + + // Output: + // {"level":"ERROR","msg":"something broke","error":"no trace"} +} + +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 := newExampleLogger() + + if err := f1(); err != nil { + logger.Error("f1 failed", "my-error", err) + } + + // Output: + // {"level":"ERROR","msg":"f1 failed","my-error":"failed\n\nbraces.dev/errtrace_test.f3\n\t/Users/abg/src/braces.dev/errtrace/example_trace_test.go:24\nbraces.dev/errtrace_test.f2\n\t/Users/abg/src/braces.dev/errtrace/example_trace_test.go:20\nbraces.dev/errtrace_test.f1\n\t/Users/abg/src/braces.dev/errtrace/example_trace_test.go:16\n"} +} + +// newExampleLogger creates a new slog.Logger for use in examples. +// It omits timestamps from the output to allow for output matching. +func newExampleLogger() *slog.Logger { + return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ + ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { + if len(groups) == 0 && a.Key == slog.TimeKey { + return slog.Attr{} + } + return a + }, + })) +} From e398330871387770b37dcbcd2977b273510da958 Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Sun, 1 Jun 2025 11:09:08 -0700 Subject: [PATCH 3/5] fix: local path agnostic output assertions --- example_trace_test.go | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/example_trace_test.go b/example_trace_test.go index 9bd5077..77bcb97 100644 --- a/example_trace_test.go +++ b/example_trace_test.go @@ -4,7 +4,6 @@ import ( "errors" "fmt" "log/slog" - "os" "runtime" "strings" @@ -86,25 +85,29 @@ func ExampleUnwrapFrame() { func ExampleLogAttr() { // This example demonstrates use of the LogAttr function. // The LogAttr function always uses the "error" key. - logger := newExampleLogger() + logger, printLogOutput := newExampleLogger() if err := f1(); err != nil { logger.Error("f1 failed", errtrace.LogAttr(err)) } + printLogOutput() + // Output: - // {"level":"ERROR","msg":"f1 failed","error":"failed\n\nbraces.dev/errtrace_test.f3\n\t/Users/abg/src/braces.dev/errtrace/example_trace_test.go:24\nbraces.dev/errtrace_test.f2\n\t/Users/abg/src/braces.dev/errtrace/example_trace_test.go:20\nbraces.dev/errtrace_test.f1\n\t/Users/abg/src/braces.dev/errtrace/example_trace_test.go:16\n"} + // {"level":"ERROR","msg":"f1 failed","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"} } func ExampleLogAttr_noTrace() { // LogAttr reports the original error message // if the error does not have a trace attached to it. - logger := newExampleLogger() + logger, printLogOutput := newExampleLogger() if err := errors.New("no trace"); err != nil { logger.Error("something broke", errtrace.LogAttr(err)) } + printLogOutput() + // Output: // {"level":"ERROR","msg":"something broke","error":"no trace"} } @@ -113,25 +116,32 @@ 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 := newExampleLogger() + 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/Users/abg/src/braces.dev/errtrace/example_trace_test.go:24\nbraces.dev/errtrace_test.f2\n\t/Users/abg/src/braces.dev/errtrace/example_trace_test.go:20\nbraces.dev/errtrace_test.f1\n\t/Users/abg/src/braces.dev/errtrace/example_trace_test.go:16\n"} + // {"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. -func newExampleLogger() *slog.Logger { - return slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{ - ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { - if len(groups) == 0 && a.Key == slog.TimeKey { - return slog.Attr{} - } - return a - }, - })) +// 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() + } } From 7332cd0fe3a6a8518b47988d2e5448c87bbbb17b Mon Sep 17 00:00:00 2001 From: Abhinav Gupta Date: Sun, 1 Jun 2025 11:10:32 -0700 Subject: [PATCH 4/5] Format => FormatString Writing to a strings.Builder can never fail so we don't need the impossible error path. Use FormatString instead, which already does this. --- errtrace.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/errtrace.go b/errtrace.go index 8d69170..079f3fa 100644 --- a/errtrace.go +++ b/errtrace.go @@ -135,15 +135,7 @@ func (e *errTrace) Format(s fmt.State, verb rune) { // LogValue implements the [slog.LogValuer] interface. func (e *errTrace) LogValue() slog.Value { - var s strings.Builder - if err := Format(&s, e); err != nil { - return slog.GroupValue( - slog.String("message", e.Error()), - slog.Any("formatErr", err), - ) - } - - return slog.StringValue(s.String()) + return slog.StringValue(FormatString(e)) } // TracePC returns the program counter for the location From 99bec3d95d11d729e152b2c7a9dbee71078fc5bc Mon Sep 17 00:00:00 2001 From: Prashant V Date: Sun, 1 Jun 2025 17:42:51 -0700 Subject: [PATCH 5/5] Drop LogAttr and related tests --- errtrace.go | 9 --------- example_trace_test.go | 30 ------------------------------ 2 files changed, 39 deletions(-) diff --git a/errtrace.go b/errtrace.go index 079f3fa..638f29b 100644 --- a/errtrace.go +++ b/errtrace.go @@ -102,15 +102,6 @@ func FormatString(target error) string { return s.String() } -// LogAttr builds a slog attribute for an error with the key "error". -// -// When serialized with a slog-based logger, -// this will report an error return trace if the error has one, -// otherwise the original error message will be logged as-is. -func LogAttr(err error) slog.Attr { - return slog.Any("error", err) -} - type errTrace struct { err error pc uintptr diff --git a/example_trace_test.go b/example_trace_test.go index 77bcb97..bd987c4 100644 --- a/example_trace_test.go +++ b/example_trace_test.go @@ -82,36 +82,6 @@ func ExampleUnwrapFrame() { // /path/to/errtrace/example_trace_test.go:3 } -func ExampleLogAttr() { - // This example demonstrates use of the LogAttr function. - // The LogAttr function always uses the "error" key. - logger, printLogOutput := newExampleLogger() - - if err := f1(); err != nil { - logger.Error("f1 failed", errtrace.LogAttr(err)) - } - - printLogOutput() - - // Output: - // {"level":"ERROR","msg":"f1 failed","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"} -} - -func ExampleLogAttr_noTrace() { - // LogAttr reports the original error message - // if the error does not have a trace attached to it. - logger, printLogOutput := newExampleLogger() - - if err := errors.New("no trace"); err != nil { - logger.Error("something broke", errtrace.LogAttr(err)) - } - - printLogOutput() - - // Output: - // {"level":"ERROR","msg":"something broke","error":"no trace"} -} - func Example_logWithSlog() { // This example demonstrates how to log an errtrace-wrapped error // with the slog package.