Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce Callback Functions for Constructor & Decorator Calls #377

Merged
merged 11 commits into from
May 1, 2023

Conversation

JacobOaks
Copy link
Contributor

@JacobOaks JacobOaks commented Feb 17, 2023

Recently folks have been asking for more visiblity into what constructors and decorators actually get called, especially Fx users (uber-go/fx#1039).

This PR allows users to provide callback functions for Dig to call when it invokes a constructor or decorator. Dig will pass along some basic information to the callback function: the name of the function, and the error it returned if any.

Users can provide a callback by using the two new APIs, dig.WithProviderCallback and dig.WithDecoratorCallback, which return a type implementing both dig.ProvideOption and dig.DecorateOption. For example, this code prints a simple message when myFunc and myDecorator are called (presumably defined elsewhere):

c := dig.New()
myCallback := func(ci CallbackInfo) {
	var errorAdd string
	if ci.Error != nil {
		errorAdd = fmt.Sprintf("with error: %v", ci.Error)
	}
	fmt.Printf("%q finished%v", ci.Name, errorAdd)
}
c.Provide(myFunc, WithProviderCallback(myCallback))
c.Decorate(myDecorator, WithDecoratorCallback(myCallback))

We can discuss Fx-side consumption of this functionality to aid observability in this way and help identify dead code, but one idea is to register callback functions that will generate new Fx events when constructors and decorators get called. I wrote a prototype implementation of this idea for Fx, and then wrote a demo showing how this can help identify dead code.

Internal Ref: GO-1873

Recently folks have been asking for more visiblity into what constructors
and decorators actually get called, especially Fx users.

This implementation allows users to provide callback functions
for Dig to call when it invokes a constructor or decorator.
Dig will pass along info such as which function was actually called,
the name (package + name) of the function, and what kind it was
(provided or decorated), when it calls the provided callback function.

Users can provide a callback by using the new `dig.Option`, `dig.WithCallback`:

```go
type MyCallback struct {}

func (MyCallback) Called(ci dig.CallbackInfo) {
	switch ci.Kind {
	case dig.Provided:
		fmt.Printf("My provided function %q was called!\n", ci.Name)
	case dig.Decorated:
		fmt.Printf("My decorator %q was called!\n", ci.Name)
	}
}

func main() {
	// ...
	c := dig.New(WithCallback(MyCallback{}))
	// ...
}
```

With this addition, from the Fx side, we can register a callback function
that will generate new Fx events when constructors and decorators get
called. This will help users identify which _do not_ get called, and thus
identify dead code.

Internal Ref: GO-1873
@codecov
Copy link

codecov bot commented Feb 17, 2023

Codecov Report

Merging #377 (6ec4a1a) into master (87cf89e) will increase coverage by 0.03%.
The diff coverage is 100.00%.

❗ Current head 6ec4a1a differs from pull request most recent head e281032. Consider uploading reports for the commit e281032 to get more accurate results

@@            Coverage Diff             @@
##           master     #377      +/-   ##
==========================================
+ Coverage   98.35%   98.38%   +0.03%     
==========================================
  Files          21       22       +1     
  Lines        1458     1490      +32     
==========================================
+ Hits         1434     1466      +32     
  Misses         15       15              
  Partials        9        9              
Impacted Files Coverage Δ
callback.go 100.00% <100.00%> (ø)
constructor.go 97.46% <100.00%> (+0.24%) ⬆️
decorate.go 100.00% <100.00%> (ø)
provide.go 100.00% <100.00%> (ø)

... and 1 file with indirect coverage changes

📣 We’re building smart automated test selection to slash your CI/CD build times. Learn more

@JacobOaks JacobOaks changed the title Introduce Callback Functions for Constructor & Decorator Calls [WIP] Introduce Callback Functions for Constructor & Decorator Calls Feb 17, 2023
@JacobOaks JacobOaks marked this pull request as draft February 17, 2023 22:29
@JacobOaks JacobOaks marked this pull request as ready for review April 18, 2023 14:20
@JacobOaks JacobOaks changed the title [WIP] Introduce Callback Functions for Constructor & Decorator Calls Introduce Callback Functions for Constructor & Decorator Calls Apr 18, 2023
callback.go Outdated Show resolved Hide resolved
dig_test.go Outdated Show resolved Hide resolved
callback.go Show resolved Hide resolved
dig_test.go Outdated Show resolved Hide resolved
dig_test.go Outdated Show resolved Hide resolved
callback.go Outdated
Comment on lines 31 to 33
// Error contains the error returned by the [Callback]'s associated
// function, if there was one.
Error error
Copy link
Contributor

Choose a reason for hiding this comment

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

It would be nice if we can allow differentiation between regular errors vs panic errors.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Users should be able to do this using errors.As
https://pkg.go.dev/go.uber.org/dig#PanicError

dig_test.go Outdated Show resolved Hide resolved
Copy link
Contributor

@manjari25 manjari25 left a comment

Choose a reason for hiding this comment

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

LGTM

callback.go Outdated Show resolved Hide resolved
callback.go Outdated Show resolved Hide resolved
@JacobOaks JacobOaks merged commit 027aa21 into uber-go:master May 1, 2023
3 checks passed
JacobOaks added a commit to uber-go/fx that referenced this pull request May 15, 2023
Folks have been asking for additional observability into what
constructors/decorators they give to Fx actually get invoked. With
uber-go/dig#377 being merged, we can use Dig
callbacks to provide this observability point. This PR does this by
registering callbacks with Dig that generate a new
[fxevent](https://pkg.go.dev/go.uber.org/fx/fxevent#Event) called
`fxevent.Run`, containing information about the provided/decorated
function that was invoked by Dig:

```go
// Run is emitted after a constructor, decorator, or supply/replace stub is run by Fx.
type Run struct {
	// Name is the name of the function that was run.
	Name string

	// Kind indicates which Fx option was used to pass along the function.
	// It is either "provide", "decorate", "supply", or "replace".
	Kind string

	// ModuleName is the name of the module in which the function belongs.
	ModuleName string

	// Err is non-nil if the function returned an error.
	// If fx.RecoverFromPanics is used, this will include panics.
	Err error
}
```

The default
[fxevent.Logger](https://pkg.go.dev/go.uber.org/fx/fxevent#Logger)s have
been updated to print to the console/log with Zap accordingly. Folks can
use custom `fxevent.Logger`s to respond to these events as they would
any other event:

```go
type myLogger struct{ /* ... */ }

func (l *myLogger) LogEvent(event fxevent.Event) {
    switch e := event.(type) {
    // ...
    case *fxevent.Run:
        // can access e.Name, e.Kind, e.ModuleName, and e.Err
        // ...
    }
}

func main() {
    app := fx.New(
            fx.WithLogger(func() fxevent.Logger { return &myLogger{} } ),
        // ...
    )
}
```
I wrote a small demo showing a custom logger like this can be used to
identify "dead functions" within Fx:
https://github.com/JacobOaks/fx_dead_code_demo.

This PR also adds stack trace information into the `Provided`,
`Decorated`, `Supplied`, and `Decorated` fxevents to aid folks with
larger codebases that consume many modules, who might otherwise have a
hard time finding where exactly a "dead function" is being given to Fx
with just the `name` and `ModuleName` fields of the `Run` event. Fx was
already keeping track of this information, so I don't think this
addition incurs too much additional overhead for Fx, but if folks
disagree we can remove this addition or perhaps make it opt-in by gating
it behind a new option `fx.ReportStackTraces()` or something.
alexisvisco pushed a commit to alexisvisco/dig that referenced this pull request Aug 31, 2023
…go#377)

Recently folks have been asking for more visiblity into what constructors
and decorators actually get called, especially Fx users.

This implementation allows users to provide callback functions
for Dig to call when it invokes a constructor or decorator.
Dig will pass along info such as which function was actually called,
the name (package + name) of the function, and what kind it was
(provided or decorated), when it calls the provided callback function.

Users can provide a callback by using the new `dig.Option`, `dig.WithCallback`:

```go
type MyCallback struct {}

func (MyCallback) Called(ci dig.CallbackInfo) {
	switch ci.Kind {
	case dig.Provided:
		fmt.Printf("My provided function %q was called!\n", ci.Name)
	case dig.Decorated:
		fmt.Printf("My decorator %q was called!\n", ci.Name)
	}
}

func main() {
	// ...
	c := dig.New(WithCallback(MyCallback{}))
	// ...
}
```

With this addition, from the Fx side, we can register a callback function
that will generate new Fx events when constructors and decorators get
called. This will help users identify which _do not_ get called, and thus
identify dead code.

Internal Ref: GO-1873
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants