Repository: uber-go/fx
Pull request #1077 (2023-05-02 17:26:59)

Use Dig Callbacks to Emit Run Events
Folks have been asking for additional observability into what constructors/decorators they give to Fx actually get invoked. With https://github.com/uber-go/dig/pull/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.


Patch:
diff --git a/app.go b/app.go
index 14e050b..d524a5e 100644
--- a/app.go
+++ b/app.go
@@ -482,7 +482,7 @@ func New(opts ...Option) *App {
 
 	// Run decorators before executing any Invokes -- including the one
 	// inside constructCustomLogger.
-	app.err = multierr.Append(app.err, app.root.decorate())
+	app.err = multierr.Append(app.err, app.root.decorateAll())
 
 	// If you are thinking about returning here after provides: do not (just yet)!
 	// If a custom logger was being used, we're still buffering messages.
diff --git a/decorate.go b/decorate.go
index 6294a4e..5091345 100644
--- a/decorate.go
+++ b/decorate.go
@@ -22,6 +22,7 @@ package fx
 
 import (
 	"fmt"
+	"reflect"
 	"strings"
 
 	"go.uber.org/dig"
@@ -207,7 +208,8 @@ type decorator struct {
 	Stack fxreflect.Stack
 
 	// Whether this decorator was specified via fx.Replace
-	IsReplace bool
+	IsReplace   bool
+	ReplaceType reflect.Type // set only if IsReplace
 }
 
 func runDecorator(c container, d decorator, opts ...dig.DecorateOption) (err error) {
diff --git a/fxevent/console.go b/fxevent/console.go
index 320fdfd..d56990c 100644
--- a/fxevent/console.go
+++ b/fxevent/console.go
@@ -82,7 +82,6 @@ func (l *ConsoleLogger) LogEvent(event Event) {
 		if e.Err != nil {
 			l.logf("Error after options were applied: %+v", e.Err)
 		}
-
 	case *Replaced:
 		for _, rtype := range e.OutputTypeNames {
 			if e.ModuleName != "" {
@@ -105,6 +104,16 @@ func (l *ConsoleLogger) LogEvent(event Event) {
 		if e.Err != nil {
 			l.logf("Error after options were applied: %+v", e.Err)
 		}
+	case *Run:
+		var moduleStr string
+		if e.ModuleName != "" {
+			moduleStr = fmt.Sprintf(" from module %q", e.ModuleName)
+		}
+		l.logf("RUN\t%v: %v%v", e.Kind, e.Name, moduleStr)
+		if e.Err != nil {
+			l.logf("Error returned: %+v", e.Err)
+		}
+
 	case *Invoking:
 		if e.ModuleName != "" {
 			l.logf("INVOKE\t\t%s from module %q", e.FunctionName, e.ModuleName)
diff --git a/fxevent/event.go b/fxevent/event.go
index f7902b8..c87f9a3 100644
--- a/fxevent/event.go
+++ b/fxevent/event.go
@@ -39,6 +39,7 @@ func (*Supplied) event()          {}
 func (*Provided) event()          {}
 func (*Replaced) event()          {}
 func (*Decorated) event()         {}
+func (*Run) event()               {}
 func (*Invoking) event()          {}
 func (*Invoked) event()           {}
 func (*Stopping) event()          {}
@@ -109,6 +110,9 @@ type Supplied struct {
 	// TypeName is the name of the type of value that was added.
 	TypeName string
 
+	// StackTrace is the stack trace of the call to Supply.
+	StackTrace []string
+
 	// ModuleName is the name of the module in which the value was added to.
 	ModuleName string
 
@@ -122,6 +126,9 @@ type Provided struct {
 	// Fx.
 	ConstructorName string
 
+	// StackTrace is the stack trace of where the constructor was provided to Fx.
+	StackTrace []string
+
 	// OutputTypeNames is a list of names of types that are produced by
 	// this constructor.
 	OutputTypeNames []string
@@ -142,6 +149,9 @@ type Replaced struct {
 	// OutputTypeNames is a list of names of types that were replaced.
 	OutputTypeNames []string
 
+	// StackTrace is the stack trace of the call to Replace.
+	StackTrace []string
+
 	// ModuleName is the name of the module in which the value was added to.
 	ModuleName string
 
@@ -155,6 +165,9 @@ type Decorated struct {
 	// provided to Fx.
 	DecoratorName string
 
+	// StackTrace is the stack trace of where the decorator was given to Fx.
+	StackTrace []string
+
 	// ModuleName is the name of the module in which the value was added to.
 	ModuleName string
 
@@ -166,6 +179,23 @@ type Decorated struct {
 	Err error
 }
 
+// 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
+}
+
 // Invoking is emitted before we invoke a function specified with fx.Invoke.
 type Invoking struct {
 	// FunctionName is the name of the function that will be invoked.
diff --git a/fxevent/zap.go b/fxevent/zap.go
index b558b2a..c304ea0 100644
--- a/fxevent/zap.go
+++ b/fxevent/zap.go
@@ -104,11 +104,13 @@ func (l *ZapLogger) LogEvent(event Event) {
 		if e.Err != nil {
 			l.logError("error encountered while applying options",
 				zap.String("type", e.TypeName),
+				zap.Strings("stacktrace", e.StackTrace),
 				moduleField(e.ModuleName),
 				zap.Error(e.Err))
 		} else {
 			l.logEvent("supplied",
 				zap.String("type", e.TypeName),
+				zap.Strings("stacktrace", e.StackTrace),
 				moduleField(e.ModuleName),
 			)
 		}
@@ -116,6 +118,7 @@ func (l *ZapLogger) LogEvent(event Event) {
 		for _, rtype := range e.OutputTypeNames {
 			l.logEvent("provided",
 				zap.String("constructor", e.ConstructorName),
+				zap.Strings("stacktrace", e.StackTrace),
 				moduleField(e.ModuleName),
 				zap.String("type", rtype),
 				maybeBool("private", e.Private),
@@ -124,17 +127,20 @@ func (l *ZapLogger) LogEvent(event Event) {
 		if e.Err != nil {
 			l.logError("error encountered while applying options",
 				moduleField(e.ModuleName),
+				zap.Strings("stacktrace", e.StackTrace),
 				zap.Error(e.Err))
 		}
 	case *Replaced:
 		for _, rtype := range e.OutputTypeNames {
 			l.logEvent("replaced",
+				zap.Strings("stacktrace", e.StackTrace),
 				moduleField(e.ModuleName),
 				zap.String("type", rtype),
 			)
 		}
 		if e.Err != nil {
 			l.logError("error encountered while replacing",
+				zap.Strings("stacktrace", e.StackTrace),
 				moduleField(e.ModuleName),
 				zap.Error(e.Err))
 		}
@@ -142,15 +148,32 @@ func (l *ZapLogger) LogEvent(event Event) {
 		for _, rtype := range e.OutputTypeNames {
 			l.logEvent("decorated",
 				zap.String("decorator", e.DecoratorName),
+				zap.Strings("stacktrace", e.StackTrace),
 				moduleField(e.ModuleName),
 				zap.String("type", rtype),
 			)
 		}
 		if e.Err != nil {
 			l.logError("error encountered while applying options",
+				zap.Strings("stacktrace", e.StackTrace),
 				moduleField(e.ModuleName),
 				zap.Error(e.Err))
 		}
+	case *Run:
+		if e.Err != nil {
+			l.logError("error returned",
+				zap.String("name", e.Name),
+				zap.String("kind", e.Kind),
+				moduleField(e.ModuleName),
+				zap.Error(e.Err),
+			)
+		} else {
+			l.logEvent("run",
+				zap.String("name", e.Name),
+				zap.String("kind", e.Kind),
+				moduleField(e.ModuleName),
+			)
+		}
 	case *Invoking:
 		// Do not log stack as it will make logs hard to read.
 		l.logEvent("invoking",
diff --git a/internal/fxreflect/stack.go b/internal/fxreflect/stack.go
index eb64618..6618634 100644
--- a/internal/fxreflect/stack.go
+++ b/internal/fxreflect/stack.go
@@ -85,14 +85,23 @@ const _defaultCallersDepth = 8
 //		bar/baz/qux.go:12
 type Stack []Frame
 
-// Returns a single-line, semi-colon representation of a Stack. For a
-// multi-line representation, use %+v.
+// String returns a single-line, semi-colon representation of a Stack.
+// For a list of strings where each represents one frame, use Strings.
+// For a cleaner multi-line representation, use %+v.
 func (fs Stack) String() string {
+	return strings.Join(fs.Strings(), "; ")
+}
+
+// Strings returns a list of strings, each representing a frame in the stack.
+// Each line will be in the form,
+//
+//	foo/bar.Baz() (path/to/foo.go:42)
+func (fs Stack) Strings() []string {
 	items := make([]string, len(fs))
 	for i, f := range fs {
 		items[i] = f.String()
 	}
-	return strings.Join(items, "; ")
+	return items
 }
 
 // Format implements fmt.Formatter to handle "%+v".
diff --git a/module.go b/module.go
index 7a28ca8..d678c4d 100644
--- a/module.go
+++ b/module.go
@@ -146,34 +146,67 @@ func (m *module) provide(p provide) {
 		return
 	}
 
+	if p.IsSupply {
+		m.supply(p)
+		return
+	}
+
+	funcName := fxreflect.FuncName(p.Target)
 	var info dig.ProvideInfo
-	if err := runProvide(m.scope, p, dig.FillProvideInfo(&info), dig.Export(!p.Private)); err != nil {
+	opts := []dig.ProvideOption{
+		dig.FillProvideInfo(&info),
+		dig.Export(!p.Private),
+		dig.WithProviderCallback(func(ci dig.CallbackInfo) {
+			m.log.LogEvent(&fxevent.Run{
+				Name:       funcName,
+				Kind:       "provide",
+				ModuleName: m.name,
+				Err:        ci.Error,
+			})
+		}),
+	}
+
+	if err := runProvide(m.scope, p, opts...); err != nil {
 		m.app.err = err
 	}
-	var ev fxevent.Event
-	switch {
-	case p.IsSupply:
-		ev = &fxevent.Supplied{
-			TypeName:   p.SupplyType.String(),
-			ModuleName: m.name,
-			Err:        m.app.err,
-		}
+	outputNames := make([]string, len(info.Outputs))
+	for i, o := range info.Outputs {
+		outputNames[i] = o.String()
+	}
 
-	default:
-		outputNames := make([]string, len(info.Outputs))
-		for i, o := range info.Outputs {
-			outputNames[i] = o.String()
-		}
+	m.log.LogEvent(&fxevent.Provided{
+		ConstructorName: funcName,
+		StackTrace:      p.Stack.Strings(),
+		ModuleName:      m.name,
+		OutputTypeNames: outputNames,
+		Err:             m.app.err,
+		Private:         p.Private,
+	})
+}
 
-		ev = &fxevent.Provided{
-			ConstructorName: fxreflect.FuncName(p.Target),
-			ModuleName:      m.name,
-			OutputTypeNames: outputNames,
-			Err:             m.app.err,
-			Private:         p.Private,
-		}
+func (m *module) supply(p provide) {
+	typeName := p.SupplyType.String()
+	opts := []dig.ProvideOption{
+		dig.Export(!p.Private),
+		dig.WithProviderCallback(func(ci dig.CallbackInfo) {
+			m.log.LogEvent(&fxevent.Run{
+				Name:       fmt.Sprintf("stub(%v)", typeName),
+				Kind:       "supply",
+				ModuleName: m.name,
+			})
+		}),
+	}
+
+	if err := runProvide(m.scope, p, opts...); err != nil {
+		m.app.err = err
 	}
-	m.log.LogEvent(ev)
+
+	m.log.LogEvent(&fxevent.Supplied{
+		TypeName:   typeName,
+		StackTrace: p.Stack.Strings(),
+		ModuleName: m.name,
+		Err:        m.app.err,
+	})
 }
 
 // Constructs custom loggers for all modules in the tree
@@ -253,38 +286,76 @@ func (m *module) executeInvoke(i invoke) (err error) {
 	return err
 }
 
-func (m *module) decorate() (err error) {
-	for _, decorator := range m.decorators {
-		var info dig.DecorateInfo
-		err := runDecorator(m.scope, decorator, dig.FillDecorateInfo(&info))
-		outputNames := make([]string, len(info.Outputs))
-		for i, o := range info.Outputs {
-			outputNames[i] = o.String()
-		}
-
-		if decorator.IsReplace {
-			m.log.LogEvent(&fxevent.Replaced{
-				ModuleName:      m.name,
-				OutputTypeNames: outputNames,
-				Err:             err,
-			})
-		} else {
-
-			m.log.LogEvent(&fxevent.Decorated{
-				DecoratorName:   fxreflect.FuncName(decorator.Target),
-				ModuleName:      m.name,
-				OutputTypeNames: outputNames,
-				Err:             err,
-			})
-		}
-		if err != nil {
+func (m *module) decorateAll() error {
+	for _, d := range m.decorators {
+		if err := m.decorate(d); err != nil {
 			return err
 		}
 	}
+
 	for _, m := range m.modules {
-		if err := m.decorate(); err != nil {
+		if err := m.decorateAll(); err != nil {
 			return err
 		}
 	}
 	return nil
 }
+
+func (m *module) decorate(d decorator) (err error) {
+	if d.IsReplace {
+		return m.replace(d)
+	}
+
+	funcName := fxreflect.FuncName(d.Target)
+	var info dig.DecorateInfo
+	opts := []dig.DecorateOption{
+		dig.FillDecorateInfo(&info),
+		dig.WithDecoratorCallback(func(ci dig.CallbackInfo) {
+			m.log.LogEvent(&fxevent.Run{
+				Name:       funcName,
+				Kind:       "decorate",
+				ModuleName: m.name,
+				Err:        ci.Error,
+			})
+		}),
+	}
+
+	err = runDecorator(m.scope, d, opts...)
+	outputNames := make([]string, len(info.Outputs))
+	for i, o := range info.Outputs {
+		outputNames[i] = o.String()
+	}
+
+	m.log.LogEvent(&fxevent.Decorated{
+		DecoratorName:   funcName,
+		StackTrace:      d.Stack.Strings(),
+		ModuleName:      m.name,
+		OutputTypeNames: outputNames,
+		Err:             err,
+	})
+
+	return err
+}
+
+func (m *module) replace(d decorator) error {
+	typeName := d.ReplaceType.String()
+	opts := []dig.DecorateOption{
+		dig.WithDecoratorCallback(func(ci dig.CallbackInfo) {
+			m.log.LogEvent(&fxevent.Run{
+				Name:       fmt.Sprintf("stub(%v)", typeName),
+				Kind:       "replace",
+				ModuleName: m.name,
+				Err:        ci.Error,
+			})
+		}),
+	}
+
+	err := runDecorator(m.scope, d, opts...)
+	m.log.LogEvent(&fxevent.Replaced{
+		ModuleName:      m.name,
+		StackTrace:      d.Stack.Strings(),
+		OutputTypeNames: []string{typeName},
+		Err:             err,
+	})
+	return err
+}
diff --git a/replace.go b/replace.go
index 3ced9a9..b457797 100644
--- a/replace.go
+++ b/replace.go
@@ -103,11 +103,12 @@ type replaceOption struct {
 }
 
 func (o replaceOption) apply(m *module) {
-	for _, target := range o.Targets {
+	for i, target := range o.Targets {
 		m.decorators = append(m.decorators, decorator{
-			Target:    target,
-			Stack:     o.Stack,
-			IsReplace: true,
+			Target:      target,
+			Stack:       o.Stack,
+			IsReplace:   true,
+			ReplaceType: o.Types[i],
 		})
 	}
 }


Test patch:
diff --git a/app_test.go b/app_test.go
index 44301af..8e0bdc6 100644
--- a/app_test.go
+++ b/app_test.go
@@ -357,7 +357,7 @@ func TestNewApp(t *testing.T) {
 		defer app.RequireStart().RequireStop()
 
 		require.Equal(t,
-			[]string{"Provided", "Provided", "Provided", "Provided", "Decorated", "LoggerInitialized", "Invoking", "Invoked", "Started"},
+			[]string{"Provided", "Provided", "Provided", "Provided", "Decorated", "LoggerInitialized", "Invoking", "Run", "Run", "Invoked", "Started"},
 			spy.EventTypes())
 	})
 
@@ -573,7 +573,7 @@ func TestWithLogger(t *testing.T) {
 		)
 
 		assert.Equal(t, []string{
-			"Supplied", "Provided", "Provided", "Provided", "LoggerInitialized",
+			"Supplied", "Provided", "Provided", "Provided", "Run", "LoggerInitialized",
 		}, spy.EventTypes())
 
 		spy.Reset()
@@ -603,7 +603,7 @@ func TestWithLogger(t *testing.T) {
 			"must provide constructor function, got  (type *bytes.Buffer)",
 		)
 
-		assert.Equal(t, []string{"Supplied", "Provided", "LoggerInitialized"}, spy.EventTypes())
+		assert.Equal(t, []string{"Supplied", "Provided", "Run", "LoggerInitialized"}, spy.EventTypes())
 	})
 
 	t.Run("logger failed to build", func(t *testing.T) {
@@ -654,6 +654,153 @@ type errHandlerFunc func(error)
 
 func (f errHandlerFunc) HandleError(err error) { f(err) }
 
+func TestRunEventEmission(t *testing.T) {
+	t.Parallel()
+
+	for _, tt := range []struct {
+		desc          string
+		giveOpts      []Option
+		wantRunEvents []fxevent.Run
+		wantErr       string
+	}{
+		{
+			desc: "Simple Provide And Decorate",
+			giveOpts: []Option{
+				Provide(func() int { return 5 }),
+				Decorate(func(int) int { return 6 }),
+				Invoke(func(int) {}),
+			},
+			wantRunEvents: []fxevent.Run{
+				{
+					Name: "go.uber.org/fx_test.TestRunEventEmission.func1()",
+					Kind: "provide",
+				},
+				{
+					Name: "go.uber.org/fx_test.TestRunEventEmission.func2()",
+					Kind: "decorate",
+				},
+			},
+		},
+		{
+			desc: "Supply and Decorator Error",
+			giveOpts: []Option{
+				Supply(5),
+				Decorate(func(int) (int, error) {
+					return 0, errors.New("humongous despair")
+				}),
+				Invoke(func(int) {}),
+			},
+			wantRunEvents: []fxevent.Run{
+				{
+					Name: "stub(int)",
+					Kind: "supply",
+				},
+				{
+					Name: "go.uber.org/fx_test.TestRunEventEmission.func4()",
+					Kind: "decorate",
+				},
+			},
+			wantErr: "humongous despair",
+		},
+		{
+			desc: "Replace",
+			giveOpts: []Option{
+				Provide(func() int { return 5 }),
+				Replace(6),
+				Invoke(func(int) {}),
+			},
+			wantRunEvents: []fxevent.Run{
+				{
+					Name: "stub(int)",
+					Kind: "replace",
+				},
+			},
+		},
+		{
+			desc: "Provide Error",
+			giveOpts: []Option{
+				Provide(func() (int, error) {
+					return 0, errors.New("terrible sadness")
+				}),
+				Invoke(func(int) {}),
+			},
+			wantRunEvents: []fxevent.Run{
+				{
+					Name: "go.uber.org/fx_test.TestRunEventEmission.func8()",
+					Kind: "provide",
+				},
+			},
+			wantErr: "terrible sadness",
+		},
+		{
+			desc: "Provide Panic",
+			giveOpts: []Option{
+				Provide(func() int {
+					panic("bad provide")
+				}),
+				RecoverFromPanics(),
+				Invoke(func(int) {}),
+			},
+			wantRunEvents: []fxevent.Run{
+				{
+					Name: "go.uber.org/fx_test.TestRunEventEmission.func10()",
+					Kind: "provide",
+				},
+			},
+			wantErr: `panic: "bad provide"`,
+		},
+		{
+			desc: "Decorate Panic",
+			giveOpts: []Option{
+				Supply(5),
+				Decorate(func(int) int {
+					panic("bad decorate")
+				}),
+				RecoverFromPanics(),
+				Invoke(func(int) {}),
+			},
+			wantRunEvents: []fxevent.Run{
+				{
+					Name: "stub(int)",
+					Kind: "supply",
+				},
+				{
+					Name: "go.uber.org/fx_test.TestRunEventEmission.func12()",
+					Kind: "decorate",
+				},
+			},
+			wantErr: `panic: "bad decorate"`,
+		},
+	} {
+		tt := tt
+		t.Run(tt.desc, func(t *testing.T) {
+			t.Parallel()
+
+			app, spy := NewSpied(tt.giveOpts...)
+			if tt.wantErr != "" {
+				assert.ErrorContains(t, app.Err(), tt.wantErr)
+			} else {
+				assert.NoError(t, app.Err())
+			}
+
+			gotEvents := spy.Events().SelectByTypeName("Run")
+			require.Len(t, gotEvents, len(tt.wantRunEvents))
+			for i, event := range gotEvents {
+				rEvent, ok := event.(*fxevent.Run)
+				require.True(t, ok)
+
+				assert.Equal(t, tt.wantRunEvents[i].Name, rEvent.Name)
+				assert.Equal(t, tt.wantRunEvents[i].Kind, rEvent.Kind)
+				if tt.wantErr != "" && i == len(gotEvents)-1 {
+					assert.ErrorContains(t, rEvent.Err, tt.wantErr)
+				} else {
+					assert.NoError(t, rEvent.Err)
+				}
+			}
+		})
+	}
+}
+
 type customError struct {
 	err error
 }
@@ -1338,6 +1485,8 @@ func TestAppStart(t *testing.T) {
 			"Provided", "Provided", "Provided", "Provided",
 			"LoggerInitialized",
 			"Invoking",
+			"Run",
+			"Run",
 			"Invoked",
 			"OnStartExecuting", "OnStartExecuted",
 			"RollingBack",
@@ -1374,6 +1523,8 @@ func TestAppStart(t *testing.T) {
 			"Provided", "Provided", "Provided", "Provided",
 			"LoggerInitialized",
 			"Invoking",
+			"Run",
+			"Run",
 			"Invoked",
 			"OnStartExecuting", "OnStartExecuted",
 			"OnStartExecuting", "OnStartExecuted",
@@ -2066,6 +2217,7 @@ func TestCustomLoggerWithLifecycle(t *testing.T) {
 		"Provided",
 		"Provided",
 		"Provided",
+		"Run",
 		"LoggerInitialized",
 		"OnStartExecuting", "OnStartExecuted",
 		"Started",
diff --git a/fxevent/console_test.go b/fxevent/console_test.go
index 6d54ff2..2d346dc 100644
--- a/fxevent/console_test.go
+++ b/fxevent/console_test.go
@@ -137,12 +137,19 @@ func TestConsoleLogger(t *testing.T) {
 		},
 		{
 			name: "Supplied",
-			give: &Supplied{TypeName: "*bytes.Buffer"},
+			give: &Supplied{
+				TypeName:   "*bytes.Buffer",
+				StackTrace: []string{"main.main", "runtime.main"},
+			},
 			want: "[Fx] SUPPLY	*bytes.Buffer\n",
 		},
 		{
 			name: "Supplied with module",
-			give: &Supplied{TypeName: "*bytes.Buffer", ModuleName: "myModule"},
+			give: &Supplied{
+				TypeName:   "*bytes.Buffer",
+				ModuleName: "myModule",
+				StackTrace: []string{"main.main", "runtime.main"},
+			},
 			want: "[Fx] SUPPLY	*bytes.Buffer from module \"myModule\"\n",
 		},
 		{
@@ -161,6 +168,7 @@ func TestConsoleLogger(t *testing.T) {
 				ConstructorName: "bytes.NewBuffer()",
 				OutputTypeNames: []string{"*bytes.Buffer"},
 				Private:         false,
+				StackTrace:      []string{"main.main", "runtime.main"},
 			},
 			want: "[Fx] PROVIDE	*bytes.Buffer <= bytes.NewBuffer()\n",
 		},
@@ -170,6 +178,7 @@ func TestConsoleLogger(t *testing.T) {
 				ConstructorName: "bytes.NewBuffer()",
 				OutputTypeNames: []string{"*bytes.Buffer"},
 				Private:         true,
+				StackTrace:      []string{"main.main", "runtime.main"},
 			},
 			want: "[Fx] PROVIDE (PRIVATE)	*bytes.Buffer <= bytes.NewBuffer()\n",
 		},
@@ -180,6 +189,7 @@ func TestConsoleLogger(t *testing.T) {
 				ModuleName:      "myModule",
 				OutputTypeNames: []string{"*bytes.Buffer"},
 				Private:         false,
+				StackTrace:      []string{"main.main", "runtime.main"},
 			},
 			want: "[Fx] PROVIDE	*bytes.Buffer <= bytes.NewBuffer() from module \"myModule\"\n",
 		},
@@ -190,6 +200,7 @@ func TestConsoleLogger(t *testing.T) {
 				ModuleName:      "myModule",
 				OutputTypeNames: []string{"*bytes.Buffer"},
 				Private:         true,
+				StackTrace:      []string{"main.main", "runtime.main"},
 			},
 			want: "[Fx] PROVIDE (PRIVATE)	*bytes.Buffer <= bytes.NewBuffer() from module \"myModule\"\n",
 		},
@@ -197,6 +208,7 @@ func TestConsoleLogger(t *testing.T) {
 			name: "Replaced",
 			give: &Replaced{
 				OutputTypeNames: []string{"*bytes.Buffer"},
+				StackTrace:      []string{"main.main", "runtime.main"},
 			},
 			want: "[Fx] REPLACE	*bytes.Buffer\n",
 		},
@@ -205,6 +217,7 @@ func TestConsoleLogger(t *testing.T) {
 			give: &Replaced{
 				ModuleName:      "myModule",
 				OutputTypeNames: []string{"*bytes.Buffer"},
+				StackTrace:      []string{"main.main", "runtime.main"},
 			},
 			want: "[Fx] REPLACE	*bytes.Buffer from module \"myModule\"\n",
 		},
@@ -218,6 +231,7 @@ func TestConsoleLogger(t *testing.T) {
 			give: &Decorated{
 				DecoratorName:   "bytes.NewBuffer()",
 				OutputTypeNames: []string{"*bytes.Buffer"},
+				StackTrace:      []string{"main.main", "runtime.main"},
 			},
 			want: "[Fx] DECORATE	*bytes.Buffer <= bytes.NewBuffer()\n",
 		},
@@ -227,6 +241,7 @@ func TestConsoleLogger(t *testing.T) {
 				DecoratorName:   "bytes.NewBuffer()",
 				ModuleName:      "myModule",
 				OutputTypeNames: []string{"*bytes.Buffer"},
+				StackTrace:      []string{"main.main", "runtime.main"},
 			},
 			want: "[Fx] DECORATE	*bytes.Buffer <= bytes.NewBuffer() from module \"myModule\"\n",
 		},
@@ -240,6 +255,32 @@ func TestConsoleLogger(t *testing.T) {
 			give: &Decorated{Err: &richError{}},
 			want: "[Fx] Error after options were applied: rich error\n",
 		},
+		{
+			name: "Run",
+			give: &Run{Name: "bytes.NewBuffer()", Kind: "constructor"},
+			want: "[Fx] RUN\tconstructor: bytes.NewBuffer()\n",
+		},
+		{
+			name: "Run with module",
+			give: &Run{
+				Name:       "bytes.NewBuffer()",
+				Kind:       "constructor",
+				ModuleName: "myModule",
+			},
+			want: "[Fx] RUN\tconstructor: bytes.NewBuffer() from module \"myModule\"\n",
+		},
+		{
+			name: "RunError",
+			give: &Run{
+				Name: "bytes.NewBuffer()",
+				Kind: "constructor",
+				Err:  errors.New("terrible constructor error"),
+			},
+			want: joinLines(
+				"[Fx] RUN\tconstructor: bytes.NewBuffer()",
+				"[Fx] Error returned: terrible constructor error",
+			),
+		},
 		{
 			name: "Invoking",
 			give: &Invoking{FunctionName: "bytes.NewBuffer()"},
diff --git a/fxevent/event_test.go b/fxevent/event_test.go
index 77f3f83..0546b02 100644
--- a/fxevent/event_test.go
+++ b/fxevent/event_test.go
@@ -39,6 +39,7 @@ func TestForCoverage(t *testing.T) {
 		&Provided{},
 		&Replaced{},
 		&Decorated{},
+		&Run{},
 		&Invoking{},
 		&Invoked{},
 		&Stopping{},
diff --git a/fxevent/zap_test.go b/fxevent/zap_test.go
index c3d3780..d230255 100644
--- a/fxevent/zap_test.go
+++ b/fxevent/zap_test.go
@@ -129,26 +129,36 @@ func TestZapLogger(t *testing.T) {
 			},
 		},
 		{
-			name:        "Supplied",
-			give:        &Supplied{TypeName: "*bytes.Buffer"},
+			name: "Supplied",
+			give: &Supplied{
+				TypeName:   "*bytes.Buffer",
+				StackTrace: []string{"main.main", "runtime.main"},
+			},
 			wantMessage: "supplied",
 			wantFields: map[string]interface{}{
-				"type": "*bytes.Buffer",
+				"type":       "*bytes.Buffer",
+				"stacktrace": []interface{}{"main.main", "runtime.main"},
 			},
 		},
 		{
-			name:        "Supplied/Error",
-			give:        &Supplied{TypeName: "*bytes.Buffer", Err: someError},
+			name: "Supplied/Error",
+			give: &Supplied{
+				TypeName:   "*bytes.Buffer",
+				StackTrace: []string{"main.main", "runtime.main"},
+				Err:        someError,
+			},
 			wantMessage: "error encountered while applying options",
 			wantFields: map[string]interface{}{
-				"type":  "*bytes.Buffer",
-				"error": "some error",
+				"type":       "*bytes.Buffer",
+				"stacktrace": []interface{}{"main.main", "runtime.main"},
+				"error":      "some error",
 			},
 		},
 		{
 			name: "Provide",
 			give: &Provided{
 				ConstructorName: "bytes.NewBuffer()",
+				StackTrace:      []string{"main.main", "runtime.main"},
 				ModuleName:      "myModule",
 				OutputTypeNames: []string{"*bytes.Buffer"},
 				Private:         false,
@@ -156,6 +166,7 @@ func TestZapLogger(t *testing.T) {
 			wantMessage: "provided",
 			wantFields: map[string]interface{}{
 				"constructor": "bytes.NewBuffer()",
+				"stacktrace":  []interface{}{"main.main", "runtime.main"},
 				"type":        "*bytes.Buffer",
 				"module":      "myModule",
 			},
@@ -164,6 +175,7 @@ func TestZapLogger(t *testing.T) {
 			name: "PrivateProvide",
 			give: &Provided{
 				ConstructorName: "bytes.NewBuffer()",
+				StackTrace:      []string{"main.main", "runtime.main"},
 				ModuleName:      "myModule",
 				OutputTypeNames: []string{"*bytes.Buffer"},
 				Private:         true,
@@ -171,59 +183,113 @@ func TestZapLogger(t *testing.T) {
 			wantMessage: "provided",
 			wantFields: map[string]interface{}{
 				"constructor": "bytes.NewBuffer()",
+				"stacktrace":  []interface{}{"main.main", "runtime.main"},
 				"type":        "*bytes.Buffer",
 				"module":      "myModule",
 				"private":     true,
 			},
 		},
 		{
-			name:        "Provide/Error",
-			give:        &Provided{Err: someError},
+			name: "Provide/Error",
+			give: &Provided{
+				StackTrace: []string{"main.main", "runtime.main"},
+				Err:        someError,
+			},
 			wantMessage: "error encountered while applying options",
 			wantFields: map[string]interface{}{
-				"error": "some error",
+				"stacktrace": []interface{}{"main.main", "runtime.main"},
+				"error":      "some error",
 			},
 		},
 		{
 			name: "Replace",
 			give: &Replaced{
 				ModuleName:      "myModule",
+				StackTrace:      []string{"main.main", "runtime.main"},
 				OutputTypeNames: []string{"*bytes.Buffer"},
 			},
 			wantMessage: "replaced",
 			wantFields: map[string]interface{}{
-				"type":   "*bytes.Buffer",
-				"module": "myModule",
+				"type":       "*bytes.Buffer",
+				"stacktrace": []interface{}{"main.main", "runtime.main"},
+				"module":     "myModule",
 			},
 		},
 		{
 			name: "Replace/Error",
-			give: &Replaced{Err: someError},
+			give: &Replaced{
+				StackTrace: []string{"main.main", "runtime.main"},
+				Err:        someError,
+			},
 
 			wantMessage: "error encountered while replacing",
 			wantFields: map[string]interface{}{
-				"error": "some error",
+				"stacktrace": []interface{}{"main.main", "runtime.main"},
+				"error":      "some error",
 			},
 		},
 		{
 			name: "Decorate",
 			give: &Decorated{
 				DecoratorName:   "bytes.NewBuffer()",
+				StackTrace:      []string{"main.main", "runtime.main"},
 				ModuleName:      "myModule",
 				OutputTypeNames: []string{"*bytes.Buffer"},
 			},
 			wantMessage: "decorated",
 			wantFields: map[string]interface{}{
-				"decorator": "bytes.NewBuffer()",
-				"type":      "*bytes.Buffer",
-				"module":    "myModule",
+				"decorator":  "bytes.NewBuffer()",
+				"stacktrace": []interface{}{"main.main", "runtime.main"},
+				"type":       "*bytes.Buffer",
+				"module":     "myModule",
 			},
 		},
 		{
-			name:        "Decorate/Error",
-			give:        &Decorated{Err: someError},
+			name: "Decorate/Error",
+			give: &Decorated{
+				StackTrace: []string{"main.main", "runtime.main"},
+				Err:        someError,
+			},
 			wantMessage: "error encountered while applying options",
 			wantFields: map[string]interface{}{
+				"stacktrace": []interface{}{"main.main", "runtime.main"},
+				"error":      "some error",
+			},
+		},
+		{
+			name:        "Run",
+			give:        &Run{Name: "bytes.NewBuffer()", Kind: "constructor"},
+			wantMessage: "run",
+			wantFields: map[string]interface{}{
+				"name": "bytes.NewBuffer()",
+				"kind": "constructor",
+			},
+		},
+		{
+			name: "Run with module",
+			give: &Run{
+				Name:       "bytes.NewBuffer()",
+				Kind:       "constructor",
+				ModuleName: "myModule",
+			},
+			wantMessage: "run",
+			wantFields: map[string]interface{}{
+				"name":   "bytes.NewBuffer()",
+				"kind":   "constructor",
+				"module": "myModule",
+			},
+		},
+		{
+			name: "Run/Error",
+			give: &Run{
+				Name: "bytes.NewBuffer()",
+				Kind: "constructor",
+				Err:  someError,
+			},
+			wantMessage: "error returned",
+			wantFields: map[string]interface{}{
+				"name":  "bytes.NewBuffer()",
+				"kind":  "constructor",
 				"error": "some error",
 			},
 		},
diff --git a/internal/fxreflect/stack_test.go b/internal/fxreflect/stack_test.go
index d0749ba..6296485 100644
--- a/internal/fxreflect/stack_test.go
+++ b/internal/fxreflect/stack_test.go
@@ -257,4 +257,15 @@ path/to/another/module.AnotherFunction()
 `, fmt.Sprintf("%+v", stack))
 	})
 
+	t.Run("strings", func(t *testing.T) {
+		t.Parallel()
+
+		assert.Equal(t,
+			[]string{
+				"path/to/module.SomeFunction() (path/to/file.go:42)",
+				"path/to/another/module.AnotherFunction() (path/to/another/file.go:12)",
+			},
+			stack.Strings(),
+		)
+	})
 }
diff --git a/module_test.go b/module_test.go
index f8a7ace..a1104b5 100644
--- a/module_test.go
+++ b/module_test.go
@@ -262,15 +262,15 @@ func TestModuleSuccess(t *testing.T) {
 				giveWithLogger: fx.NopLogger,
 				wantEvents: []string{
 					"Supplied", "Provided", "Provided", "Provided",
-					"LoggerInitialized", "Invoking", "Invoked",
+					"Run", "LoggerInitialized", "Invoking", "Invoked",
 				},
 			},
 			{
 				desc:           "Not using a custom logger for module defaults to app logger",
 				giveWithLogger: fx.Options(),
 				wantEvents: []string{
-					"Supplied", "Provided", "Provided", "Provided", "Provided",
-					"LoggerInitialized", "Invoking", "Invoked", "Invoking", "Invoked",
+					"Supplied", "Provided", "Provided", "Provided", "Provided", "Run",
+					"LoggerInitialized", "Invoking", "Run", "Invoked", "Invoking", "Invoked",
 				},
 			},
 		}
@@ -353,8 +353,8 @@ func TestModuleSuccess(t *testing.T) {
 		)
 
 		assert.Equal(t, []string{
-			"Provided", "Supplied", "Replaced",
-			"LoggerInitialized", "Invoking", "Invoked",
+			"Provided", "Supplied", "Replaced", "Run", "Run",
+			"LoggerInitialized", "Invoking", "Run", "Invoked",
 		}, moduleSpy.EventTypes())
 
 		assert.Equal(t, []string{
@@ -413,9 +413,9 @@ func TestModuleSuccess(t *testing.T) {
 		)
 
 		assert.Equal(t, []string{
-			"Supplied", "Provided", "Replaced", "LoggerInitialized",
+			"Supplied", "Provided", "Replaced", "Run", "Run", "LoggerInitialized",
 			//Invoke logged twice, once from child and another from grandchild
-			"Invoking", "Invoked", "Invoking", "Invoked",
+			"Invoking", "Run", "Invoked", "Invoking", "Invoked",
 		}, childSpy.EventTypes(), "events from grandchild also logged in child logger")
 
 		assert.Equal(t, []string{
@@ -649,7 +649,7 @@ func TestModuleFailures(t *testing.T) {
 					"must provide constructor function, got  (type *bytes.Buffer)",
 				},
 				wantEvents: []string{
-					"Supplied", "Provided", "LoggerInitialized",
+					"Supplied", "Provided", "Run", "LoggerInitialized",
 				},
 			},
 			{
@@ -660,7 +660,7 @@ func TestModuleFailures(t *testing.T) {
 				giveAppOpts:     spyAsLogger,
 				wantErrContains: []string{"error building logger"},
 				wantEvents: []string{
-					"Supplied", "Provided", "Provided", "Provided",
+					"Supplied", "Provided", "Provided", "Provided", "Run",
 					"LoggerInitialized", "Provided", "LoggerInitialized",
 				},
 			},
@@ -678,8 +678,8 @@ func TestModuleFailures(t *testing.T) {
 				giveAppOpts:     spyAsLogger,
 				wantErrContains: []string{"error building logger dependency"},
 				wantEvents: []string{
-					"Supplied", "Provided", "Provided", "Provided",
-					"LoggerInitialized", "Provided", "Provided", "LoggerInitialized",
+					"Supplied", "Provided", "Provided", "Provided", "Run",
+					"LoggerInitialized", "Provided", "Provided", "Run", "LoggerInitialized",
 				},
 			},
 			{
@@ -690,7 +690,7 @@ func TestModuleFailures(t *testing.T) {
 					"fx.WithLogger", "from:", "Failed",
 				},
 				wantEvents: []string{
-					"Supplied", "Provided", "Provided", "Provided",
+					"Supplied", "Provided", "Provided", "Provided", "Run",
 					"LoggerInitialized", "Provided", "LoggerInitialized",
 				},
 			},