fix(log): surface nested recovery metadata
Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
f72c8daf3b
commit
e2481552b5
4 changed files with 67 additions and 11 deletions
24
errors.go
24
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.
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
10
log.go
10
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
27
log_test.go
27
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{
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue