Skip to content

Add error.Recovered().#9

Merged
jortel merged 8 commits into
masterfrom
add-recovered
Nov 16, 2025
Merged

Add error.Recovered().#9
jortel merged 8 commits into
masterfrom
add-recovered

Conversation

@jortel
Copy link
Copy Markdown
Owner

@jortel jortel commented Oct 5, 2025

Add new logr constructor that supports setting the level:

# level = 1
Log := logr.New("testing", 1)

Adjust the level.

# level = 1
Log := logr.New("testing", 1)
# level = 2
Log = logr.Level(Log, 2)

Add convenience for:

p := recover()
if p != nil {
  err = liberr.Recovered(p)
}

Ensures recovered panic is an error.

Summary by CodeRabbit

  • New Features

    • Converts recovered panics into errors for safer failure handling.
    • Adds explicit log-level control and a new logger construction approach, plus environment-driven logging configuration.
  • Tests

    • Added and updated tests covering panic recovery and logger behavior/levels.
  • Chores

    • Bumped Go language version to 1.21.

Signed-off-by: Jeff Ortel <jortel@redhat.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Oct 5, 2025

Warning

Rate limit exceeded

@jortel has exceeded the limit for the number of commits or files that can be reviewed per hour. Please wait 13 minutes and 26 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 4b663fa and af26d74.

📒 Files selected for processing (1)
  • logr/pkg.go (1 hunks)

Walkthrough

Adds error.Recovered(p any) to convert recovered panic values into errors; replaces old logr.WithName usage with a new logr.New(name, level, kvpair...) API and level constants, updates logger internals and tests to use explicit levels, and bumps the Go version to 1.21.

Changes

Cohort / File(s) Summary
Error handling
error/error.go
Adds exported Recovered(p any) (err error): returns nil for nil, returns p if already an error, otherwise returns Errorf("%+v", p). Placed before existing New.
Error tests
error/error_test.go
Adds TestRecovered validating Recovered with nil, error, and non-error panic values via defer/recover.
Logger package (new API surface)
logr/pkg.go, logr/logger.go
Introduces level constants and constructors: New(name string, level int, kvpair ...any) logr.Logger, WithName wrapper, Level helper; removes environment-level constants from logger.go, adds WithLevel(level int) on Sink, and shifts env parsing into pkg.go.
Logger tests & usage
logr/log_test.go, cmd/main.go
Tests added/updated to call New("test", 2) / New("Test", 0) and environment-driven assertions; cmd/main.go updated to New("Test", 0, "and", "fields"). Tests import os and Gomega.
Go toolchain
go.mod
Bumps Go version from 1.20 to 1.21.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant Func as FunctionWithDefer
  participant RecoveredFn as error.Recovered
  Caller->>Func: call()
  rect rgba(224,240,255,0.6)
    note right of Func: runtime may panic(value)
    Func-->>Func: panic(value)
    Func->>Func: deferred recover() captures p
    Func->>RecoveredFn: Recovered(p)
    alt p is nil
      RecoveredFn-->>Func: nil
    else p is error
      RecoveredFn-->>Func: same error
    else p non-error
      RecoveredFn-->>Func: Errorf("%+v", p)
    end
  end
  Func-->>Caller: returns error or nil
Loading
sequenceDiagram
  autonumber
  actor App
  participant New as logr.New
  participant Env as ENV(LOG_LEVEL)
  participant Sink as logr.Sink
  App->>New: New(name, providedLevel, kvpair...)
  New->>Env: read LOG_LEVEL (if set)
  alt LOG_LEVEL valid
    New-->>New: override providedLevel with env value
  end
  New->>Sink: set sink.level = max(NORMAL, computedLevel)
  New-->>App: Logger wrapping Sink
  App->>Sink: optional WithLevel/Level adjustments
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Review logging API and exported symbols across logr/pkg.go and logr/logger.go for backward-compatibility and exported-surface changes.
  • Verify LOG_LEVEL parsing precedence, minimum-level enforcement, and interactions between New, WithName, WithLevel, and Level.
  • Inspect tests (logr/log_test.go) for environment setup/teardown and Gomega usage in CI.
  • Check error.Recovered formatting (%+v) and edge-case coverage in error/error_test.go.

Poem

I hopped where panics tumbled free,
Gathered lost values tenderly.
New names call logs with level bright,
I twitched my nose and set them right.
— a rabbit, happily coding 🐇

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'Add error.Recovered()' accurately describes the primary change: adding a new Recovered function to the error package. However, the changeset also includes substantial logr package changes (new constructor signature, removal of WithName, additions to logr/pkg.go) that are not reflected in the title.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
error/error.go (1)

35-35: Consider clarifying the comment.

The phrasing "returns an error recovered panic" is slightly awkward.

Apply this diff to improve clarity:

-// Recovered returns an error recovered panic.
+// Recovered converts a recovered panic value into an error.
error/error_test.go (1)

111-111: Remove unnecessary return statement.

The return statement at the end of the test function is unnecessary.

Apply this diff:

 	err = fn(nil)
 	g.Expect(err).To(gomega.BeNil())
-	return
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a2027d4 and b38ad84.

📒 Files selected for processing (2)
  • error/error.go (1 hunks)
  • error/error_test.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
error/error_test.go (1)
error/error.go (2)
  • Recovered (36-45)
  • New (48-53)
🔇 Additional comments (1)
error/error.go (1)

40-43: Verify: Should error-type panics be wrapped to capture the recovery stack trace?

When p is already an error, it's returned as-is without wrapping. This means no stack trace is captured at the point of recovery—only the original error is preserved.

If the panic was triggered with a plain errors.New() or fmt.Errorf(), the resulting error will lack any stack trace. Consider whether wrapping with Wrap(err) would be more consistent with the package's behavior, ensuring stack traces are captured at recovery points.

If wrapping is desired, apply this diff:

 	err, cast := p.(error)
-	if !cast {
+	if cast {
+		err = Wrap(err)
+	} else {
 		err = Errorf("%+v", p)
 	}

Comment thread error/error_test.go Outdated
jortel added 2 commits October 5, 2025 15:15
Signed-off-by: Jeff Ortel <jortel@redhat.com>
Signed-off-by: Jeff Ortel <jortel@redhat.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
logr/log_test.go (1)

10-16: Consider testing the LOG_LEVEL environment override.

The test verifies basic logger creation and level assignment, but doesn't test the LOG_LEVEL environment variable override behavior introduced in New(). Consider adding a test case that sets LOG_LEVEL and verifies it overrides the passed level parameter.

Example test addition:

func TestNewWithEnvOverride(t *testing.T) {
	g := gomega.NewGomegaWithT(t)
	
	os.Setenv("LOG_LEVEL", "5")
	defer os.Unsetenv("LOG_LEVEL")
	
	log := New("test", 2)
	g.Expect(log.GetSink().(*Sink).level).To(gomega.Equal(5))
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e835a12 and ab122ef.

⛔ Files ignored due to path filters (1)
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (4)
  • cmd/main.go (1 hunks)
  • go.mod (1 hunks)
  • logr/log_test.go (1 hunks)
  • logr/logger.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
cmd/main.go (1)
logr/logger.go (1)
  • New (30-43)
logr/log_test.go (1)
logr/logger.go (2)
  • New (30-43)
  • Sink (20-27)
🪛 GitHub Actions: RunTests
logr/log_test.go

[error] 29-29: TestList execution logged an unexpected error path; test output indicates failures in New()/list path.

🔇 Additional comments (6)
go.mod (1)

3-3: Go 1.21 required for built-in max() function.

The version bump to Go 1.21 is necessary because logr/logger.go line 41 uses the max(0, level) built-in function, which was introduced in Go 1.21.

logr/log_test.go (2)

19-27: LGTM! API migration and typo fix look good.

The migration from WithName to New("Test", 0) is correct, and the struct name correction from "Persion" to "Person" fixes the typo.


29-29: Verify the original review's assumptions are incorrect.

The diff confirms that line 29 (err := liberr.New("Test")) was not modified by this PR—it existed in the previous version unchanged. The PR only modified earlier lines in TestList(): replacing WithName() with New() and fixing a typo.

Since line 29 wasn't changed, any pipeline failure at that line is not caused by this PR's changes. The review conflates "line referenced in pipeline failure" with "line modified in PR," which are different concerns. Additionally, the PR objectives mentioned error.Recovered() were addressed in commit b38ad84, not in this PR.

Likely an incorrect or invalid review comment.

cmd/main.go (1)

11-11: LGTM! Correct migration to the new API.

The change from WithName("Test", "and", "fields") to New("Test", 0, "and", "fields") correctly adopts the new constructor. Passing level=0 maintains the default verbosity behavior.

logr/logger.go (2)

29-43: Verify the LOG_LEVEL override behavior is intentional.

The New() function unconditionally overrides the level parameter when LOG_LEVEL is set and valid. This means:

  • If LOG_LEVEL="5" is set, calling New("test", 2) will result in level 5, not 2
  • The passed level parameter is silently ignored

While this enables runtime configuration, it could be surprising to callers who explicitly pass a level. Consider documenting this behavior clearly, or using the env var as a default only when level is 0 or negative.

If this override-all behavior is intentional, please confirm and ensure it's documented in the function's godoc comment.


45-48: LGTM! Backward compatibility maintained.

The WithName() function now delegates to New(name, 0, kvpair...), which maintains backward compatibility by using the lowest verbosity level (0) as the default. The compatibility wrapper pattern is appropriate here.

Signed-off-by: Jeff Ortel <jortel@redhat.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
logr/logger.go (1)

137-148: Fix incomplete state copying in Sink methods.

Both Sink.WithName and Sink.WithValues create new Sink instances but fail to copy critical fields from the parent sink:

  • WithName (lines 138-140) only copies the name, losing delegate, fields, development, structured, and crucially the level.
  • WithValues (lines 143-148) only copies name and fields, still losing delegate, development, structured, and level.

This means derived loggers created via these methods lose their configuration, including the log level introduced in this PR.

Apply this diff to preserve all sink state:

 func (s *Sink) WithName(name string) logr.LogSink {
-	return &Sink{name: name}
+	return &Sink{
+		delegate:    s.delegate,
+		fields:      s.fields,
+		name:        name,
+		development: s.development,
+		structured:  s.structured,
+		level:       s.level,
+	}
 }

 func (s *Sink) WithValues(kvpair ...any) logr.LogSink {
+	merged := log.Fields{}
+	for k, v := range s.fields {
+		merged[k] = v
+	}
+	for k, v := range fields(kvpair) {
+		merged[k] = v
+	}
 	return &Sink{
-		name:   s.name,
-		fields: fields(kvpair),
+		delegate:    s.delegate,
+		fields:      merged,
+		name:        s.name,
+		development: s.development,
+		structured:  s.structured,
+		level:       s.level,
 	}
 }
🧹 Nitpick comments (1)
logr/logger.go (1)

35-49: Consider validation and document the environment override behavior.

The implementation has a few concerns:

  1. The LOG_LEVEL environment variable always overrides the explicitly provided level parameter, which may be surprising to callers who intentionally pass a specific level value.
  2. There's no upper bound validation for the environment value—negative values are caught by max(NORMAL, level), but extremely large values (e.g., 999) are not constrained.
  3. This environment parsing happens on every call to New, which could be inefficient if called frequently.

Consider these improvements:

 func New(name string, level int, kvpair ...any) logr.Logger {
 	v := os.Getenv(EnvLevel)
 	if v != "" {
 		if n, err := strconv.Atoi(v); err == nil {
+			// Validate environment level is within reasonable bounds
+			if n < 0 || n > DEBUG {
+				// Either clamp or log warning
+				n = max(NORMAL, min(n, DEBUG))
+			}
 			level = n
 		}
 	}
 	return logr.New(
 		&Sink{
 			name:   name,
 			fields: fields(kvpair),
 			level:  max(NORMAL, level),
 		})
 }

Additionally, document in the function's godoc that LOG_LEVEL environment variable takes precedence over the level parameter.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ab122ef and dbe3c87.

📒 Files selected for processing (2)
  • logr/log_test.go (1 hunks)
  • logr/logger.go (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
logr/log_test.go (1)
logr/logger.go (2)
  • New (36-49)
  • Sink (26-33)
🔇 Additional comments (5)
logr/log_test.go (2)

3-9: LGTM!

The import additions are appropriate for the new test functionality.


24-41: LGTM!

The migration from WithName to New with a level of 0 (NORMAL) is correct, and the typo fix from Persion to Person improves code clarity.

logr/logger.go (3)

19-23: LGTM!

The log level constants provide clear semantic meaning and follow Go naming conventions.


51-54: LGTM!

The delegation to New with a default NORMAL level is a clean design. This maintains backward compatibility for callers who don't need to specify a custom level.


56-81: LGTM!

The removal of level initialization from Init is correct, as level configuration has been moved to the New constructor. The remaining environment-driven formatter configuration is appropriate.

Comment thread logr/log_test.go
Signed-off-by: Jeff Ortel <jortel@redhat.com>
Signed-off-by: Jeff Ortel <jortel@redhat.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
logr/log_test.go (1)

11-32: Restore the previous EnvLevel value instead of always unsetting it.

The anonymous block in TestNew now unsets EnvLevel after the test, but it still doesn’t restore any pre-existing value. If EnvLevel was set before this test runs, it will be cleared permanently, which can surprise other tests or callers.

Consider capturing and restoring the prior value (as suggested in the earlier review comment), for example:

func() {
    old, ok := os.LookupEnv(EnvLevel)
    _ = os.Setenv(EnvLevel, "2")
    defer func() {
        if ok {
            _ = os.Setenv(EnvLevel, old)
        } else {
            _ = os.Unsetenv(EnvLevel)
        }
    }()

    log := New("test", 0)
    g.Expect(log).NotTo(gomega.BeNil())
    g.Expect(log.GetSink().(*Sink).level).To(gomega.Equal(2))
}()

This keeps the environment fully consistent before and after the test while still validating the EnvLevel override behavior.

🧹 Nitpick comments (1)
logr/logger.go (1)

117-122: Preserve full Sink state in WithLevel instead of rebuilding a partial Sink.

WithLevel currently constructs a new Sink with only name, fields, and level, dropping delegate, development, structured, and any future fields. That makes it tightly coupled to callers re-wrapping the new sink via logr.New, and will break if the level-adjust helper is ever switched to the idiomatic logger.WithSink(s.WithLevel(n)) pattern recommended in the go-logr docs. (pkg.go.dev)

To make WithLevel robust and future-proof, shallow-copy the receiver and only override level:

func (s *Sink) WithLevel(level int) logr.LogSink {
-	return &Sink{
-		name:   s.name,
-		fields: s.fields,
-		level:  max(0, level),
-	}
+	ns := *s // preserve delegate, flags, and existing fields
+	ns.level = max(0, level)
+	return &ns
}

This keeps all existing configuration (formatter, structured/dev flags, delegate) while adjusting the level only.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e5cc3dc and 50ae9fc.

📒 Files selected for processing (3)
  • logr/log_test.go (1 hunks)
  • logr/logger.go (1 hunks)
  • logr/pkg.go (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
logr/pkg.go (1)
logr/logger.go (1)
  • Sink (14-21)
logr/log_test.go (2)
logr/pkg.go (3)
  • New (23-36)
  • Level (44-52)
  • EnvLevel (13-13)
logr/logger.go (1)
  • Sink (14-21)
🔇 Additional comments (2)
logr/log_test.go (1)

34-51: Tests correctly exercise the new New/Level API.

TestNew and TestList both validate the intended behavior of New and Level, and the migration from the old WithName constructor to New("Test", 0) looks consistent and clear.

logr/pkg.go (1)

10-41: Env constants and New constructor behavior look solid.

The Env* constants, level constants, and New/WithName helpers form a clear API:

  • New respects LOG_LEVEL when set, clamps to at least NORMAL, and seeds the Sink with name and fields.
  • WithName is a simple convenience wrapper that keeps the default level semantics.

This aligns with the PR objective of centralizing logger construction and making level selection explicit.

Comment thread logr/pkg.go
Signed-off-by: Jeff Ortel <jortel@redhat.com>
Signed-off-by: Jeff Ortel <jortel@redhat.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (3)
logr/logger.go (3)

104-113: WithName overwrites existing name rather than composing hierarchically

WithName sets name: name and discards any existing s.name. In many logr implementations, WithName appends to the existing name (e.g., parent.child) so nested components retain hierarchy. If you intend hierarchical names, consider composing with the parent rather than overwriting it.

Example:

 func (s *Sink) WithName(name string) logr.LogSink {
-	return &Sink{
-		delegate:    s.delegate,
-		fields:      s.fields,
-		name:        name,
-		development: s.development,
-		structured:  s.structured,
-		level:       s.level,
-	}
+	return &Sink{
+		delegate:    s.delegate,
+		fields:      s.fields,
+		name:        joinName(s.name, name),
+		development: s.development,
+		structured:  s.structured,
+		level:       s.level,
+	}
 }
+
+func joinName(parent, child string) string {
+	if parent == "" {
+		return child
+	}
+	return parent + "." + child
+}

117-125: WithValues drops existing fields instead of merging; verify desired semantics

WithValues builds fields: fields(kvpair) and doesn’t incorporate s.fields, so chaining logger.WithValues("a", 1).WithValues("b", 2) loses "a" from the second logger. If you want cumulative context (which is typical with logr), you likely want to merge the existing fields with the new ones.

One possible approach:

 func (s *Sink) WithValues(kvpair ...any) logr.LogSink {
-	return &Sink{
-		delegate:    s.delegate,
-		fields:      fields(kvpair),
-		name:        s.name,
-		development: s.development,
-		structured:  s.structured,
-		level:       s.level,
-	}
+	return &Sink{
+		delegate:    s.delegate,
+		fields:      mergeFields(s.fields, fields(kvpair)),
+		name:        s.name,
+		development: s.development,
+		structured:  s.structured,
+		level:       s.level,
+	}
 }
+
+func mergeFields(base, extra log.Fields) log.Fields {
+	if len(base) == 0 {
+		return extra
+	}
+	out := log.Fields{}
+	for k, v := range base {
+		out[k] = v
+	}
+	for k, v := range extra {
+		out[k] = v
+	}
+	return out
+}

If overwriting is intentional, consider documenting that WithValues is non-cumulative to avoid surprises.


128-136: WithLevel clamps negative levels to 0; confirm this matches your logging model

WithLevel uses level: max(0, n), which:

  • Prevents callers from ever setting a negative level.
  • Relies on Go 1.21’s predeclared max (consistent with the go.mod bump).
  • Means Level(logger, -1) (or similar) can’t be used to “turn off” logging via a very low threshold.

If your intended contract is “levels are non‑negative verbosity values (0 = normal, higher = more verbose)”, this is fine and worth documenting. If you instead want to allow negative levels for stricter filtering, you may want to drop the clamp:

 func (s *Sink) WithLevel(n int) logr.LogSink {
 	return &Sink{
 		delegate:    s.delegate,
 		fields:      s.fields,
 		name:        s.name,
 		development: s.development,
 		structured:  s.structured,
-		level:       max(0, n),
+		level:       n,
 	}
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50ae9fc and 4b663fa.

📒 Files selected for processing (1)
  • logr/logger.go (1 hunks)

@jortel jortel merged commit 170df01 into master Nov 16, 2025
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant