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

feat(container): automatic debugging on errors #11915

Merged
merged 9 commits into from
May 10, 2022
30 changes: 30 additions & 0 deletions container/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -542,3 +542,33 @@ func TestLogging(t *testing.T) {
require.NoError(t, err)
require.Contains(t, string(graphfileContents), "<svg")
}

func TestConditionalDebugging(t *testing.T) {
logs := ""
success := false
conditionalDebugOpt := container.DebugOptions(
container.OnError(container.Logger(func(s string) {
logs += s + "\n"
})),
container.OnSuccess(container.DebugCleanup(func() {
success = true
})))

require.Error(t, container.RunDebug(
func(input TestInput) {},
conditionalDebugOpt,
))
require.Contains(t, logs, `Initializing logger`)
require.Contains(t, logs, `Registering providers`)
require.Contains(t, logs, `Registering invoker`)
require.False(t, success)

logs = ""
success = false
require.NoError(t, container.RunDebug(
func() {},
conditionalDebugOpt,
))
require.Empty(t, logs)
require.True(t, success)
}
104 changes: 98 additions & 6 deletions container/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,91 @@ func Logger(logger func(string)) DebugOption {
return debugOption(func(c *debugConfig) error {
logger("Initializing logger")
c.loggers = append(c.loggers, logger)

// send conditional log messages batched for onError/onSuccess cases
if c.logBuf != nil {
for _, s := range *c.logBuf {
logger(s)
}
}

return nil
})
}

const debugContainerSvg = "debug_container.svg"
const debugContainerDot = "debug_container.dot"

// Debug is a default debug option which sends log output to stdout, dumps
// the container in the graphviz DOT format to stdout, and to the file
// container_dump.svg.
// the container in the graphviz DOT and SVG formats to debug_container.dot
// and debug_container.svg respectively.
func Debug() DebugOption {
return DebugOptions(
StdoutLogger(),
LogVisualizer(),
FileVisualizer("container_dump.svg", "svg"),
FileVisualizer(debugContainerSvg, "svg"),
FileVisualizer(debugContainerDot, "dot"),
)
}

func (d *debugConfig) initLogBuf() {
if d.logBuf == nil {
d.logBuf = &[]string{}
d.loggers = append(d.loggers, func(s string) {
*d.logBuf = append(*d.logBuf, s)
})
}
}

// OnError is a debug option that allows setting debug options that are
// conditional on an error happening. Any loggers added error will
// receive the full dump of logs since the start of container processing.
func OnError(option DebugOption) DebugOption {
return debugOption(func(config *debugConfig) error {
config.initLogBuf()
config.onError = option
return nil
})
}

// OnSuccess is a debug option that allows setting debug options that are
// conditional on successful container resolution. Any loggers added on success
// will receive the full dump of logs since the start of container processing.
func OnSuccess(option DebugOption) DebugOption {
return debugOption(func(config *debugConfig) error {
config.initLogBuf()
config.onSuccess = option
return nil
})
}

// DebugCleanup specifies a clean-up function to be called at the end of
// processing to clean up any resources that may be used during debugging.
func DebugCleanup(cleanup func()) DebugOption {
return debugOption(func(config *debugConfig) error {
config.cleanup = append(config.cleanup, cleanup)
return nil
})
}

// AutoDebug does the same thing as Debug when there is an error and deletes
// the debug_container.dot and debug_container.dot if they exist when there
// is no error. This is the default debug mode of Run.
func AutoDebug() DebugOption {
return DebugOptions(
OnError(Debug()),
OnSuccess(DebugCleanup(func() {
deleteIfExists(debugContainerSvg)
deleteIfExists(debugContainerDot)
})),
)
}

func deleteIfExists(filename string) {
if _, err := os.Stat(filename); err == nil {
_ = os.Remove(filename)
}
}

// DebugOptions creates a debug option which bundles together other debug options.
func DebugOptions(options ...DebugOption) DebugOption {
return debugOption(func(c *debugConfig) error {
Expand All @@ -94,12 +164,18 @@ type debugConfig struct {
// logging
loggers []func(string)
indentStr string
logBuf *[]string // a log buffer for onError/onSuccess processing

// graphing
graphviz *graphviz.Graphviz
graph *cgraph.Graph
visualizers []func(string)
logVisualizer bool

// extra processing
onError DebugOption
onSuccess DebugOption
cleanup []func()
}

type debugOption func(*debugConfig) error
Expand Down Expand Up @@ -210,7 +286,7 @@ func (c *debugConfig) locationGraphNode(location Location, key *moduleKey) (*cgr
}

func (c *debugConfig) typeGraphNode(typ reflect.Type) (*cgraph.Node, error) {
node, found, err := c.findOrCreateGraphNode(c.graph, typ.String())
node, found, err := c.findOrCreateGraphNode(c.graph, moreUsefulTypeString(typ))
if err != nil {
return nil, err
}
Expand All @@ -223,6 +299,22 @@ func (c *debugConfig) typeGraphNode(typ reflect.Type) (*cgraph.Node, error) {
return node, err
}

// moreUsefulTypeString is more useful than reflect.Type.String()
func moreUsefulTypeString(ty reflect.Type) string {
switch ty.Kind() {
case reflect.Struct, reflect.Interface:
return fmt.Sprintf("%s.%s", ty.PkgPath(), ty.Name())
case reflect.Pointer:
return fmt.Sprintf("*%s", moreUsefulTypeString(ty.Elem()))
case reflect.Map:
return fmt.Sprintf("map[%s]%s", moreUsefulTypeString(ty.Key()), moreUsefulTypeString(ty.Elem()))
case reflect.Slice:
return fmt.Sprintf("[]%s", moreUsefulTypeString(ty.Elem()))
default:
return ty.String()
}
}

func (c *debugConfig) findOrCreateGraphNode(subGraph *cgraph.Graph, name string) (node *cgraph.Node, found bool, err error) {
node, err = c.graph.Node(name)
if err != nil {
Expand All @@ -246,7 +338,7 @@ func (c *debugConfig) moduleSubGraph(key *moduleKey) *cgraph.Graph {
if key != nil {
gname := fmt.Sprintf("cluster_%s", key.name)
graph = c.graph.SubGraph(gname, 1)
graph.SetLabel(fmt.Sprintf("ModuleKey: %s", key.name))
graph.SetLabel(fmt.Sprintf("Module: %s", key.name))
}
return graph
}
Expand Down
42 changes: 37 additions & 5 deletions container/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,56 @@ package container
//
// Ex:
// Run(func (x int) error { println(x) }, Provide(func() int { return 1 }))
//
// Run uses the debug mode provided by AutoDebug which means there will be
// verbose debugging information if there is an error and nothing upon success.
// Use RunDebug to configure behavior with more control.
func Run(invoker interface{}, opts ...Option) error {
return RunDebug(invoker, nil, opts...)
return RunDebug(invoker, AutoDebug(), opts...)
}

// RunDebug is a version of Run which takes an optional DebugOption for
// logging and visualization.
func RunDebug(invoker interface{}, debugOpt DebugOption, opts ...Option) error {
opt := Options(opts...)

cfg, err := newDebugConfig()
if err != nil {
return err
}

// debug cleanup
defer func() {
for _, f := range cfg.cleanup {
f()
}
}()

err = run(cfg, invoker, debugOpt, opts...)
if err != nil {
if cfg.onError != nil {
err2 := cfg.onError.applyConfig(cfg)
if err2 != nil {
return err2
}
}
return err
} else {
if cfg.onSuccess != nil {
err2 := cfg.onSuccess.applyConfig(cfg)
if err2 != nil {
return err2
}
}
return nil
}
}

func run(cfg *debugConfig, invoker interface{}, debugOpt DebugOption, opts ...Option) error {
opt := Options(opts...)

defer cfg.generateGraph() // always generate graph on exit

if debugOpt != nil {
err = debugOpt.applyConfig(cfg)
err := debugOpt.applyConfig(cfg)
if err != nil {
return err
}
Expand All @@ -33,7 +65,7 @@ func RunDebug(invoker interface{}, debugOpt DebugOption, opts ...Option) error {
cfg.logf("Registering providers")
cfg.indentLogger()
ctr := newContainer(cfg)
err = opt.apply(ctr)
err := opt.apply(ctr)
if err != nil {
cfg.logf("Failed registering providers because of: %+v", err)
return err
Expand Down