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
3 changes: 2 additions & 1 deletion baseapp/custom_txhandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"context"
"fmt"

"github.com/tendermint/tendermint/crypto/tmhash"

sdk "github.com/cosmos/cosmos-sdk/types"
"github.com/cosmos/cosmos-sdk/types/tx"
"github.com/tendermint/tendermint/crypto/tmhash"
)

type handlerFun func(ctx sdk.Context, tx sdk.Tx, simulate bool) (newCtx sdk.Context, err error)
Expand Down
38 changes: 35 additions & 3 deletions container/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,13 @@ package container
// Ex:
// var x int
// Build(Provide(func() int { return 1 }), &x)
//
// Build 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 BuildDebug to configure debug behavior.
func Build(containerOption Option, outputs ...interface{}) error {
loc := LocationFromCaller(1)
return build(loc, nil, containerOption, outputs...)
return build(loc, AutoDebug(), containerOption, outputs...)
}

// BuildDebug is a version of Build which takes an optional DebugOption for
Expand All @@ -27,10 +31,38 @@ func build(loc Location, debugOpt DebugOption, option Option, outputs ...interfa
return err
}

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

err = doBuild(cfg, loc, debugOpt, option, outputs...)
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 doBuild(cfg *debugConfig, loc Location, debugOpt DebugOption, option Option, outputs ...interface{}) error {
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 @@ -39,7 +71,7 @@ func build(loc Location, debugOpt DebugOption, option Option, outputs ...interfa
cfg.logf("Registering providers")
cfg.indentLogger()
ctr := newContainer(cfg)
err = option.apply(ctr)
err := option.apply(ctr)
if err != nil {
cfg.logf("Failed registering providers because of: %+v", err)
return err
Expand Down
32 changes: 32 additions & 0 deletions container/container_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -580,3 +580,35 @@ 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
})))

var input TestInput
require.Error(t, container.BuildDebug(
conditionalDebugOpt,
container.Options(),
&input,
))
require.Contains(t, logs, `Initializing logger`)
require.Contains(t, logs, `Registering providers`)
require.Contains(t, logs, `Registering outputs`)
require.False(t, success)

logs = ""
success = false
require.NoError(t, container.BuildDebug(
conditionalDebugOpt,
container.Options(),
))
require.Empty(t, logs)
require.True(t, success)
}
106 changes: 100 additions & 6 deletions container/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,21 +62,93 @@ 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"
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 +166,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 +288,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 +301,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 +340,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
2 changes: 0 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,6 @@ require (
sigs.k8s.io/yaml v1.3.0
)

require github.com/google/uuid v1.3.0

require (
cloud.google.com/go v0.100.2 // indirect
cloud.google.com/go/compute v1.5.0 // indirect
Expand Down