Repository: uber-go/dig
Pull request #431 (2025-03-24 22:09:39)

Adding a before-call-begins callback, to help with troubleshooting
# What

Adds a "With...BeforeCallback" option, mirroring "With...Callback" but it is called _before_ the provided func is called by Dig, rather than after.

This exists primarily to support https://github.com/uber-go/fx/pull/1267

# Motivation

A recent startup-issue debugging session included logs like this:
```
{"level":"info","ts":1742516783.6106565,"caller":"fxevent/zap.go:51","message":"provided","constructor":"a", ...
...
{"level":"info","ts":1742516783.6106653,"caller":"fxevent/zap.go:51","message":"provided","constructor":"q", ...
...
{"level":"info","ts":1742516783.6106734,"caller":"fxevent/zap.go:51","message":"provided","constructor":"z", ...
...
{"level":"info","ts":1742516784.7998207,"caller":"fxevent/zap.go:51","message":"run","name":"a", ...
{"level":"info","ts":1742516784.7999196,"caller":"fxevent/zap.go:51","message":"run","name":"b", ...
{"level":"info","ts":1742516784.9025834,"caller":"application.go:123", ...
{"level":"info","ts":1742516789.5810778,"caller":"application.go:234", ... 
{"level":"info","ts":1742516790.740516,"caller":"application.go:345", ...
# EOF
```
which accurately reflected the logs the app produced: during startup, it was getting OOM-killed after the final log, several seconds after the last Fx-"run" event.

Unfortunately, Fx's `Run` event is logged _after_ a call completes, so the fxevent log immediately before the application-produced logs tells us _almost nothing_ about what is currently running.  That seems... kinda not ideal.  I've run into this quite a few times over the years, but I finally decided to do something about it.

So I added a "before run" event, so this log will be much easier to diagnose:
```
{"level":"info","ts":1742516784.7899196,"caller":"fxevent/zap.go:51","message":"before run","name":"b", ...
{"level":"info","ts":1742516784.7999196,"caller":"fxevent/zap.go:51","message":"run","name":"b", ...
{"level":"info","ts":1742516784.8000196,"caller":"fxevent/zap.go:51","message":"before run","name":"c", ...
{"level":"info","ts":1742516784.9025834,"caller":"application.go:123", ...
# ^ clearly logged during "c"
```

After a quick attempt in Fx it became pretty clear that it would be difficult or almost impossible to do without changing Dig, so this PR changes Dig to support https://github.com/uber-go/fx/pull/1267

# How

There's not much type-safety for a change like this, as nothing tells you "you forgot to add X / inspect field Y", so pretty much all of this was:
- copy/paste Callback's core file
- find Callback uses, add BeforeCallback equivalents
- hope I didn't miss anything

I'm not entirely sure what else I should be checking, in case there are gaps I didn't see, but the Fx PR does a decent job showing that the core bits of it work IMO so I haven't tried reading, like, _everything_ to verify it.

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->

## Summary by CodeRabbit

- **New Features**
	- Added pre-execution hooks for dependency injection, allowing additional logic (e.g., logging or validation) to run before providers or decorators execute.
- **Tests**
	- Updated testing routines to verify that the new pre-execution hooks trigger correctly and enforce stricter error handling during the dependency injection process.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Draft detected.
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=uber-go/dig&utm_content=431):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai plan` to trigger planning for file edits and PR creation.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
coderabbitai[bot]: > [!NOTE]
> Generated docstrings for this pull request at https://github.com/uber-go/dig/pull/432
JacobOaks: I'm in favor of this change, modulo some small doc nits. @sywhang or @tchung1118 can one of you also take a look?

Not sure why CI didn't run for this PR though 🤔 
sywhang: Weird. @Groxx - could you please try rebasing and pushing to the branch again? Not sure why the CI here isn't getting triggered. Maybe some GitHub issue...

Patch:
diff --git a/before_callback.go b/before_callback.go
new file mode 100644
index 0000000..2ea1125
--- /dev/null
+++ b/before_callback.go
@@ -0,0 +1,94 @@
+// Copyright (c) 2025 Uber Technologies, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+// THE SOFTWARE.
+
+package dig
+
+// BeforeCallbackInfo contains information about a provided function or decorator
+// called by Dig, and is passed to a [BeforeCallback] registered with
+// [WithProviderBeforeCallback] or [WithDecoratorBeforeCallback].
+type BeforeCallbackInfo struct {
+	// Name is the name of the function in the format:
+	// <package_name>.<function_name>
+	Name string
+}
+
+// BeforeCallback is a function that can be registered with a provided function
+// using [WithProviderBeforeCallback] or decorator using [WithDecoratorBeforeCallback]
+// to cause it to be called before the provided function or decorator is run.
+type BeforeCallback func(bci BeforeCallbackInfo)
+
+// WithProviderBeforeCallback returns a [ProvideOption] which has Dig call
+// the passed in [BeforeCallback] before the corresponding constructor begins running.
+//
+// For example, the following prints a message
+// before "myConstructor" is called:
+//
+//	c := dig.New()
+//	myCallback := func(bci BeforeCallbackInfo) {
+//		fmt.Printf("%q started", bci.Name)
+//	}
+//	c.Provide(myConstructor, WithProviderBeforeCallback(myCallback)),
+//
+// BeforeCallbacks can also be specified for Decorators with [WithDecoratorBeforeCallback].
+//
+// See [BeforeCallbackInfo] for more info on the information passed to the [BeforeCallback].
+func WithProviderBeforeCallback(callback BeforeCallback) ProvideOption {
+	return withBeforeCallbackOption{
+		callback: callback,
+	}
+}
+
+// WithDecoratorBeforeCallback returns a [DecorateOption] which has Dig call
+// the passed in [BeforeCallback] before the corresponding decorator begins running.
+//
+// For example, the following prints a message
+// before "myDecorator" is called:
+//
+//	c := dig.New()
+//	myCallback := func(bci BeforeCallbackInfo) {
+//		fmt.Printf("%q started", bci.Name)
+//	}
+//	c.Decorate(myDecorator, WithDecoratorBeforeCallback(myCallback)),
+//
+// BeforeCallbacks can also be specified for Constructors with [WithProviderBeforeCallback].
+//
+// See [BeforeCallbackInfo] for more info on the information passed to the [BeforeCallback].
+func WithDecoratorBeforeCallback(callback BeforeCallback) DecorateOption {
+	return withBeforeCallbackOption{
+		callback: callback,
+	}
+}
+
+type withBeforeCallbackOption struct {
+	callback BeforeCallback
+}
+
+var (
+	_ ProvideOption  = withBeforeCallbackOption{}
+	_ DecorateOption = withBeforeCallbackOption{}
+)
+
+func (o withBeforeCallbackOption) applyProvideOption(po *provideOptions) {
+	po.BeforeCallback = o.callback
+}
+
+func (o withBeforeCallbackOption) apply(do *decorateOptions) {
+	do.BeforeCallback = o.callback
+}
diff --git a/callback.go b/callback.go
index ba0c36b..18229a5 100644
--- a/callback.go
+++ b/callback.go
@@ -41,8 +41,8 @@ type CallbackInfo struct {
 }
 
 // Callback is a function that can be registered with a provided function
-// or decorator with [WithCallback] to cause it to be called after the
-// provided function or decorator is run.
+// or decorator with [WithProviderCallback] or decorator with [WithDecoratorCallback]
+// to cause it to be called after the provided function or decorator is run.
 type Callback func(CallbackInfo)
 
 // WithProviderCallback returns a [ProvideOption] which has Dig call
diff --git a/constructor.go b/constructor.go
index adec5fd..d46a273 100644
--- a/constructor.go
+++ b/constructor.go
@@ -66,16 +66,20 @@ type constructorNode struct {
 
 	// Callback for this provided function, if there is one.
 	callback Callback
+
+	// BeforeCallback for this provided function, if there is one.
+	beforeCallback BeforeCallback
 }
 
 type constructorOptions struct {
 	// If specified, all values produced by this constructor have the provided name
 	// belong to the specified value group or implement any of the interfaces.
-	ResultName  string
-	ResultGroup string
-	ResultAs    []interface{}
-	Location    *digreflect.Func
-	Callback    Callback
+	ResultName     string
+	ResultGroup    string
+	ResultAs       []interface{}
+	Location       *digreflect.Func
+	Callback       Callback
+	BeforeCallback BeforeCallback
 }
 
 func newConstructorNode(ctor interface{}, s *Scope, origS *Scope, opts constructorOptions) (*constructorNode, error) {
@@ -106,16 +110,17 @@ func newConstructorNode(ctor interface{}, s *Scope, origS *Scope, opts construct
 	}
 
 	n := &constructorNode{
-		ctor:       ctor,
-		ctype:      ctype,
-		location:   location,
-		id:         dot.CtorID(cptr),
-		paramList:  params,
-		resultList: results,
-		orders:     make(map[*Scope]int),
-		s:          s,
-		origS:      origS,
-		callback:   opts.Callback,
+		ctor:           ctor,
+		ctype:          ctype,
+		location:       location,
+		id:             dot.CtorID(cptr),
+		paramList:      params,
+		resultList:     results,
+		orders:         make(map[*Scope]int),
+		s:              s,
+		origS:          origS,
+		callback:       opts.Callback,
+		beforeCallback: opts.BeforeCallback,
 	}
 	s.newGraphNode(n, n.orders)
 	return n, nil
@@ -160,6 +165,12 @@ func (n *constructorNode) Call(c containerStore) (err error) {
 		}
 	}
 
+	if n.beforeCallback != nil {
+		n.beforeCallback(BeforeCallbackInfo{
+			Name: fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
+		})
+	}
+
 	if n.callback != nil {
 		start := c.clock().Now()
 		// Wrap in separate func to include PanicErrors
diff --git a/decorate.go b/decorate.go
index f4c6be1..5cc500d 100644
--- a/decorate.go
+++ b/decorate.go
@@ -68,6 +68,9 @@ type decoratorNode struct {
 
 	// Callback for this decorator, if there is one.
 	callback Callback
+
+	// BeforeCallback for this decorator, if there is one
+	beforeCallback BeforeCallback
 }
 
 func newDecoratorNode(dcor interface{}, s *Scope, opts decorateOptions) (*decoratorNode, error) {
@@ -86,15 +89,16 @@ func newDecoratorNode(dcor interface{}, s *Scope, opts decorateOptions) (*decora
 	}
 
 	n := &decoratorNode{
-		dcor:     dcor,
-		dtype:    dtype,
-		id:       dot.CtorID(dptr),
-		location: digreflect.InspectFunc(dcor),
-		orders:   make(map[*Scope]int),
-		params:   pl,
-		results:  rl,
-		s:        s,
-		callback: opts.Callback,
+		dcor:           dcor,
+		dtype:          dtype,
+		id:             dot.CtorID(dptr),
+		location:       digreflect.InspectFunc(dcor),
+		orders:         make(map[*Scope]int),
+		params:         pl,
+		results:        rl,
+		s:              s,
+		callback:       opts.Callback,
+		beforeCallback: opts.BeforeCallback,
 	}
 	return n, nil
 }
@@ -120,6 +124,11 @@ func (n *decoratorNode) Call(s containerStore) (err error) {
 			Reason: err,
 		}
 	}
+	if n.beforeCallback != nil {
+		n.beforeCallback(BeforeCallbackInfo{
+			Name: fmt.Sprintf("%v.%v", n.location.Package, n.location.Name),
+		})
+	}
 
 	if n.callback != nil {
 		start := s.clock().Now()
@@ -162,8 +171,9 @@ type DecorateOption interface {
 }
 
 type decorateOptions struct {
-	Info     *DecorateInfo
-	Callback Callback
+	Info           *DecorateInfo
+	Callback       Callback
+	BeforeCallback BeforeCallback
 }
 
 // FillDecorateInfo is a DecorateOption that writes info on what Dig was
diff --git a/provide.go b/provide.go
index 3523c1f..9e47b6d 100644
--- a/provide.go
+++ b/provide.go
@@ -37,13 +37,14 @@ type ProvideOption interface {
 }
 
 type provideOptions struct {
-	Name     string
-	Group    string
-	Info     *ProvideInfo
-	As       []interface{}
-	Location *digreflect.Func
-	Exported bool
-	Callback Callback
+	Name           string
+	Group          string
+	Info           *ProvideInfo
+	As             []interface{}
+	Location       *digreflect.Func
+	Exported       bool
+	Callback       Callback
+	BeforeCallback BeforeCallback
 }
 
 func (o *provideOptions) Validate() error {
@@ -467,11 +468,12 @@ func (s *Scope) provide(ctor interface{}, opts provideOptions) (err error) {
 		s,
 		origScope,
 		constructorOptions{
-			ResultName:  opts.Name,
-			ResultGroup: opts.Group,
-			ResultAs:    opts.As,
-			Location:    opts.Location,
-			Callback:    opts.Callback,
+			ResultName:     opts.Name,
+			ResultGroup:    opts.Group,
+			ResultAs:       opts.As,
+			Location:       opts.Location,
+			Callback:       opts.Callback,
+			BeforeCallback: opts.BeforeCallback,
 		},
 	)
 	if err != nil {


Test patch:
diff --git a/decorate_test.go b/decorate_test.go
index 206e8fc..611a0f1 100644
--- a/decorate_test.go
+++ b/decorate_test.go
@@ -28,6 +28,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+
 	"go.uber.org/dig"
 	"go.uber.org/dig/internal/digtest"
 )
@@ -164,7 +165,7 @@ func TestDecorateSuccess(t *testing.T) {
 			i := i
 			c.RequireProvide(func() *someInt { return newSomeInt(i) }, dig.Group("values"), dig.As(new(myInt)))
 		}
-		c.Decorate(func(in A) B {
+		c.RequireDecorate(func(in A) B {
 			for _, v := range in.Values {
 				v.Increment()
 			}
diff --git a/dig_test.go b/dig_test.go
index b8f4a37..647c149 100644
--- a/dig_test.go
+++ b/dig_test.go
@@ -33,6 +33,7 @@ import (
 
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
+
 	"go.uber.org/dig"
 	"go.uber.org/dig/internal/digclock"
 	"go.uber.org/dig/internal/digtest"
@@ -1689,13 +1690,19 @@ func giveInt() int { return 5 }
 func TestCallback(t *testing.T) {
 	t.Run("no errors", func(t *testing.T) {
 		var (
-			provideCallbackCalled  bool
-			decorateCallbackCalled bool
+			provideBeforeCallbackCalled  bool
+			decorateBeforeCallbackCalled bool
+			provideCallbackCalled        bool
+			decorateCallbackCalled       bool
 		)
 
 		c := digtest.New(t)
 		c.RequireProvide(
 			giveInt,
+			dig.WithProviderBeforeCallback(func(bci dig.BeforeCallbackInfo) {
+				assert.Equal(t, "go.uber.org/dig_test.giveInt", bci.Name)
+				provideBeforeCallbackCalled = true
+			}),
 			dig.WithProviderCallback(func(ci dig.CallbackInfo) {
 				assert.Equal(t, "go.uber.org/dig_test.giveInt", ci.Name)
 				assert.NoError(t, ci.Error)
@@ -1704,8 +1711,12 @@ func TestCallback(t *testing.T) {
 		)
 		c.RequireDecorate(
 			func(a int) int { return a + 5 },
+			dig.WithDecoratorBeforeCallback(func(bci dig.BeforeCallbackInfo) {
+				assert.Equal(t, "go.uber.org/dig_test.TestCallback.func1.3", bci.Name)
+				decorateBeforeCallbackCalled = true
+			}),
 			dig.WithDecoratorCallback(func(ci dig.CallbackInfo) {
-				assert.Equal(t, "go.uber.org/dig_test.TestCallback.func1.2", ci.Name)
+				assert.Equal(t, "go.uber.org/dig_test.TestCallback.func1.3", ci.Name)
 				assert.NoError(t, ci.Error)
 				decorateCallbackCalled = true
 			}),
@@ -1713,8 +1724,10 @@ func TestCallback(t *testing.T) {
 
 		c.RequireInvoke(func(a int) {})
 
-		assert.True(t, provideCallbackCalled)
-		assert.True(t, decorateCallbackCalled)
+		assert.True(t, provideCallbackCalled, "missing provide callback")
+		assert.True(t, decorateCallbackCalled, "missing decorate callback")
+		assert.True(t, provideBeforeCallbackCalled, "missing provide before-callback")
+		assert.True(t, decorateBeforeCallbackCalled, "missing decorate before-callback")
 	})
 
 	t.Run("provide error", func(t *testing.T) {
@@ -1732,7 +1745,7 @@ func TestCallback(t *testing.T) {
 			}),
 		)
 
-		c.Invoke(func(a int) {})
+		assert.Error(t, c.Invoke(func(a int) {}))
 		assert.True(t, called)
 	})
 
@@ -1752,7 +1765,7 @@ func TestCallback(t *testing.T) {
 			}),
 		)
 
-		c.Invoke(func(a int) {})
+		assert.Error(t, c.Invoke(func(a int) {}))
 		assert.True(t, called)
 	})
 
@@ -1771,7 +1784,7 @@ func TestCallback(t *testing.T) {
 			}),
 		)
 
-		c.Invoke(func(int) {})
+		assert.Error(t, c.Invoke(func(int) {}))
 		assert.True(t, called)
 	})