diff --git a/errors.go b/errors.go index a2420a2..86e434e 100644 --- a/errors.go +++ b/errors.go @@ -211,11 +211,11 @@ func inheritRecovery(dst *Err, err error) { // // retryAfter, ok := log.RetryAfter(err) func RetryAfter(err error) (*time.Duration, bool) { - var wrapped *Err - if As(err, &wrapped) { - if wrapped.RetryAfter != nil { + for err != nil { + if wrapped, ok := err.(*Err); ok && wrapped.RetryAfter != nil { return wrapped.RetryAfter, true } + err = errors.Unwrap(err) } return nil, false } @@ -235,13 +235,25 @@ func IsRetryable(err error) bool { // // next := log.RecoveryAction(err) func RecoveryAction(err error) string { - var wrapped *Err - if As(err, &wrapped) { - return wrapped.NextAction + for err != nil { + if wrapped, ok := err.(*Err); ok && wrapped.NextAction != "" { + return wrapped.NextAction + } + err = errors.Unwrap(err) } return "" } +func retryableHint(err error) bool { + for err != nil { + if wrapped, ok := err.(*Err); ok && wrapped.Retryable { + return true + } + err = errors.Unwrap(err) + } + return false +} + // --- Standard Library Wrappers --- // Is reports whether any error in err's tree matches target. diff --git a/errors_test.go b/errors_test.go index 5296524..3d0fa12 100644 --- a/errors_test.go +++ b/errors_test.go @@ -286,6 +286,16 @@ func TestRetryAfter_Good(t *testing.T) { assert.Equal(t, retryAfter, *got) } +func TestRetryAfter_Good_NestedChain(t *testing.T) { + retryAfter := 42 * time.Second + inner := &Err{Msg: "typed", RetryAfter: &retryAfter} + outer := &Err{Msg: "outer", Err: inner} + + got, ok := RetryAfter(outer) + assert.True(t, ok) + assert.Equal(t, retryAfter, *got) +} + func TestIsRetryable_Good(t *testing.T) { err := &Err{Msg: "typed", Retryable: true} assert.True(t, IsRetryable(err)) @@ -296,6 +306,13 @@ func TestRecoveryAction_Good(t *testing.T) { assert.Equal(t, "inspect", RecoveryAction(err)) } +func TestRecoveryAction_Good_NestedChain(t *testing.T) { + inner := &Err{Msg: "typed", NextAction: "inspect"} + outer := &Err{Msg: "outer", Err: inner} + + assert.Equal(t, "inspect", RecoveryAction(outer)) +} + func TestMessage_Good(t *testing.T) { err := E("op", "the message", errors.New("base")) assert.Equal(t, "the message", Message(err)) diff --git a/log.go b/log.go index 1e67927..60de5e6 100644 --- a/log.go +++ b/log.go @@ -259,18 +259,18 @@ func (l *Logger) log(level Level, prefix, msg string, keyvals ...any) { if As(err, &logErr) { if _, hasRetryable := existing["retryable"]; !hasRetryable { existing["retryable"] = struct{}{} - keyvals = append(keyvals, "retryable", logErr.Retryable) + keyvals = append(keyvals, "retryable", retryableHint(err)) } - if logErr.RetryAfter != nil { + if retryAfter, ok := RetryAfter(err); ok { if _, hasRetryAfter := existing["retry_after_seconds"]; !hasRetryAfter { existing["retry_after_seconds"] = struct{}{} - keyvals = append(keyvals, "retry_after_seconds", logErr.RetryAfter.Seconds()) + keyvals = append(keyvals, "retry_after_seconds", retryAfter.Seconds()) } } - if logErr.NextAction != "" { + if nextAction := RecoveryAction(err); nextAction != "" { if _, hasNextAction := existing["next_action"]; !hasNextAction { existing["next_action"] = struct{}{} - keyvals = append(keyvals, "next_action", logErr.NextAction) + keyvals = append(keyvals, "next_action", nextAction) } } } diff --git a/log_test.go b/log_test.go index b5512cd..278d695 100644 --- a/log_test.go +++ b/log_test.go @@ -121,6 +121,33 @@ func TestLogger_ErrorContextIncludesRecovery_Good(t *testing.T) { } } +func TestLogger_ErrorContextIncludesNestedRecovery_Good(t *testing.T) { + var buf bytes.Buffer + l := New(Options{Output: &buf, Level: LevelInfo}) + retryAfter := 30 * time.Second + + inner := &Err{ + Msg: "inner failure", + Retryable: true, + RetryAfter: &retryAfter, + NextAction: "retry later", + } + outer := &Err{Msg: "outer failure", Err: inner} + + l.Error("request failed", "err", outer) + + output := buf.String() + if !strings.Contains(output, "retryable=true") { + t.Errorf("expected output to contain retryable=true, got %q", output) + } + if !strings.Contains(output, "retry_after_seconds=30") { + t.Errorf("expected output to contain retry_after_seconds=30, got %q", output) + } + if !strings.Contains(output, "next_action=\"retry later\"") { + t.Errorf("expected output to contain next_action=\"retry later\", got %q", output) + } +} + func TestLogger_Redaction_Good(t *testing.T) { var buf bytes.Buffer l := New(Options{