From 02e391953953f2329d62c6692108c8d5eeacdfd6 Mon Sep 17 00:00:00 2001 From: Aaron Craelius Date: Thu, 14 Oct 2021 12:49:50 -0400 Subject: [PATCH] refactor(container)!: move logging and visualization options to DebugOption (#10302) ## Description --- ### Author Checklist *All items are required. Please add a note to the item if the item is not applicable and please add links to any relevant follow up issues.* I have... - [x] included the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [x] added `!` to the type prefix if API or client breaking change - [x] targeted the correct branch (see [PR Targeting](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#pr-targeting)) - [ ] provided a link to the relevant issue or specification - [ ] followed the guidelines for [building modules](https://github.com/cosmos/cosmos-sdk/blob/master/docs/building-modules) - [ ] included the necessary unit and integration [tests](https://github.com/cosmos/cosmos-sdk/blob/master/CONTRIBUTING.md#testing) - [ ] added a changelog entry to `CHANGELOG.md` - [x] included comments for [documenting Go code](https://blog.golang.org/godoc) - [ ] updated the relevant documentation or specification - [x] reviewed "Files changed" and left comments if necessary - [ ] confirmed all CI checks have passed ### Reviewers Checklist *All items are required. Please add a note if the item is not applicable and please add your handle next to the items reviewed if you only reviewed selected items.* I have... - [ ] confirmed the correct [type prefix](https://github.com/commitizen/conventional-commit-types/blob/v3.0.0/index.json) in the PR title - [ ] confirmed `!` in the type prefix if API or client breaking change - [ ] confirmed all author checklist items have been addressed - [ ] reviewed state machine logic - [ ] reviewed API design and naming - [ ] reviewed documentation is accurate - [ ] reviewed tests and test coverage - [ ] manually tested (if applicable) --- container/config.go | 173 ------------------------ container/container.go | 6 +- container/container_test.go | 22 +-- container/debug.go | 259 ++++++++++++++++++++++++++++++++++++ container/option.go | 114 ++-------------- container/run.go | 18 ++- 6 files changed, 299 insertions(+), 293 deletions(-) delete mode 100644 container/config.go create mode 100644 container/debug.go diff --git a/container/config.go b/container/config.go deleted file mode 100644 index 12437d7c009f..000000000000 --- a/container/config.go +++ /dev/null @@ -1,173 +0,0 @@ -package container - -import ( - "bytes" - "fmt" - "path/filepath" - "reflect" - - "github.com/pkg/errors" - - "github.com/goccy/go-graphviz" - "github.com/goccy/go-graphviz/cgraph" -) - -type config struct { - // logging - loggers []func(string) - indentStr string - - // graphing - graphviz *graphviz.Graphviz - graph *cgraph.Graph - visualizers []func(string) - logVisualizer bool -} - -func newConfig() (*config, error) { - g := graphviz.New() - graph, err := g.Graph() - if err != nil { - return nil, errors.Wrap(err, "error initializing graph") - } - - return &config{ - graphviz: g, - graph: graph, - }, nil -} - -func (c *config) indentLogger() { - c.indentStr = c.indentStr + " " -} - -func (c *config) dedentLogger() { - if len(c.indentStr) > 0 { - c.indentStr = c.indentStr[1:] - } -} - -func (c config) logf(format string, args ...interface{}) { - s := fmt.Sprintf(c.indentStr+format, args...) - for _, logger := range c.loggers { - logger(s) - } -} - -func (c *config) generateGraph() { - buf := &bytes.Buffer{} - err := c.graphviz.Render(c.graph, graphviz.XDOT, buf) - if err != nil { - c.logf("Error rendering DOT graph: %+v", err) - return - } - - dot := buf.String() - if c.logVisualizer { - c.logf("DOT Graph: %s", dot) - } - - for _, v := range c.visualizers { - v(dot) - } - - err = c.graph.Close() - if err != nil { - c.logf("Error closing graph: %+v", err) - return - } - - err = c.graphviz.Close() - if err != nil { - c.logf("Error closing graphviz: %+v", err) - } -} - -func (c *config) addFuncVisualizer(f func(string)) { - c.visualizers = append(c.visualizers, func(dot string) { - f(dot) - }) -} - -func (c *config) enableLogVisualizer() { - c.logVisualizer = true -} - -func (c *config) addFileVisualizer(filename string, format string) { - c.visualizers = append(c.visualizers, func(_ string) { - err := c.graphviz.RenderFilename(c.graph, graphviz.Format(format), filename) - if err != nil { - c.logf("Error saving graphviz file %s with format %s: %+v", filename, format, err) - } else { - path, err := filepath.Abs(filename) - if err == nil { - c.logf("Saved graph of container to %s", path) - } - } - }) -} - -func (c *config) locationGraphNode(location Location, scope Scope) (*cgraph.Node, error) { - graph := c.scopeSubGraph(scope) - node, found, err := c.findOrCreateGraphNode(graph, location.Name()) - if err != nil { - return nil, err - } - - if found { - return node, nil - } - - node.SetShape(cgraph.BoxShape) - node.SetColor("lightgrey") - return node, nil -} - -func (c *config) typeGraphNode(typ reflect.Type) (*cgraph.Node, error) { - node, found, err := c.findOrCreateGraphNode(c.graph, typ.String()) - if err != nil { - return nil, err - } - - if found { - return node, nil - } - - node.SetColor("lightgrey") - return node, err -} - -func (c *config) findOrCreateGraphNode(subGraph *cgraph.Graph, name string) (node *cgraph.Node, found bool, err error) { - node, err = c.graph.Node(name) - if err != nil { - return nil, false, errors.Wrapf(err, "error finding graph node %s", name) - } - - if node != nil { - return node, true, nil - } - - node, err = subGraph.CreateNode(name) - if err != nil { - return nil, false, errors.Wrapf(err, "error creating graph node %s", name) - } - - return node, false, nil -} - -func (c *config) scopeSubGraph(scope Scope) *cgraph.Graph { - graph := c.graph - if scope != nil { - gname := fmt.Sprintf("cluster_%s", scope.Name()) - graph = c.graph.SubGraph(gname, 1) - graph.SetLabel(fmt.Sprintf("Scope: %s", scope.Name())) - } - return graph -} - -func (c *config) addGraphEdge(from *cgraph.Node, to *cgraph.Node) { - _, err := c.graph.CreateEdge("", from, to) - if err != nil { - c.logf("error creating graph edge") - } -} diff --git a/container/container.go b/container/container.go index e528726d60c2..8b4cfe0da97e 100644 --- a/container/container.go +++ b/container/container.go @@ -10,7 +10,7 @@ import ( ) type container struct { - *config + *debugConfig resolvers map[reflect.Type]resolver @@ -26,9 +26,9 @@ type resolveFrame struct { typ reflect.Type } -func newContainer(cfg *config) *container { +func newContainer(cfg *debugConfig) *container { return &container{ - config: cfg, + debugConfig: cfg, resolvers: map[reflect.Type]resolver{}, scopes: map[string]Scope{}, callerStack: nil, diff --git a/container/container_test.go b/container/container_test.go index 42f1ab0accbe..c9aa3a4d8970 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -516,17 +516,19 @@ func TestLogging(t *testing.T) { require.NoError(t, err) defer os.Remove(graphfile.Name()) - require.NoError(t, container.Run( + require.NoError(t, container.RunDebug( func() {}, - container.Logger(func(s string) { - logOut += s - }), - container.Visualizer(func(g string) { - dotGraph = g - }), - container.LogVisualizer(), - container.FileVisualizer(graphfile.Name(), "svg"), - container.StdoutLogger(), + container.DebugOptions( + container.Logger(func(s string) { + logOut += s + }), + container.Visualizer(func(g string) { + dotGraph = g + }), + container.LogVisualizer(), + container.FileVisualizer(graphfile.Name(), "svg"), + container.StdoutLogger(), + ), )) require.Contains(t, logOut, "digraph") diff --git a/container/debug.go b/container/debug.go new file mode 100644 index 000000000000..a743b84cef29 --- /dev/null +++ b/container/debug.go @@ -0,0 +1,259 @@ +package container + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "reflect" + + "github.com/pkg/errors" + + "github.com/goccy/go-graphviz" + "github.com/goccy/go-graphviz/cgraph" +) + +// DebugOption is a functional option for running a container that controls +// debug logging and visualization output. +type DebugOption interface { + applyConfig(*debugConfig) error +} + +// StdoutLogger is a debug option which routes logging output to stdout. +func StdoutLogger() DebugOption { + return Logger(func(s string) { + _, _ = fmt.Fprintln(os.Stdout, s) + }) +} + +// Visualizer creates an option which provides a visualizer function which +// will receive a rendering of the container in the Graphiz DOT format +// whenever the container finishes building or fails due to an error. The +// graph is color-coded to aid debugging. +func Visualizer(visualizer func(dotGraph string)) DebugOption { + return debugOption(func(c *debugConfig) error { + c.addFuncVisualizer(visualizer) + return nil + }) +} + +// LogVisualizer is a debug option which dumps a graphviz DOT rendering of +// the container to the log. +func LogVisualizer() DebugOption { + return debugOption(func(c *debugConfig) error { + c.enableLogVisualizer() + return nil + }) +} + +// FileVisualizer is a debug option which dumps a graphviz rendering of +// the container to the specified file with the specified format. Currently +// supported formats are: dot, svg, png and jpg. +func FileVisualizer(filename, format string) DebugOption { + return debugOption(func(c *debugConfig) error { + c.addFileVisualizer(filename, format) + return nil + }) +} + +// Logger creates an option which provides a logger function which will +// receive all log messages from the container. +func Logger(logger func(string)) DebugOption { + return debugOption(func(c *debugConfig) error { + logger("Initializing logger") + c.loggers = append(c.loggers, logger) + return nil + }) +} + +// 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. +func Debug() DebugOption { + return DebugOptions( + StdoutLogger(), + LogVisualizer(), + FileVisualizer("container_dump.svg", "svg"), + ) +} + +// DebugOptions creates a debug option which bundles together other debug options. +func DebugOptions(options ...DebugOption) DebugOption { + return debugOption(func(c *debugConfig) error { + for _, opt := range options { + err := opt.applyConfig(c) + if err != nil { + return err + } + } + return nil + }) +} + +type debugConfig struct { + // logging + loggers []func(string) + indentStr string + + // graphing + graphviz *graphviz.Graphviz + graph *cgraph.Graph + visualizers []func(string) + logVisualizer bool +} + +type debugOption func(*debugConfig) error + +func (c debugOption) applyConfig(ctr *debugConfig) error { + return c(ctr) +} + +var _ DebugOption = (*debugOption)(nil) + +func newDebugConfig() (*debugConfig, error) { + g := graphviz.New() + graph, err := g.Graph() + if err != nil { + return nil, errors.Wrap(err, "error initializing graph") + } + + return &debugConfig{ + graphviz: g, + graph: graph, + }, nil +} + +func (c *debugConfig) indentLogger() { + c.indentStr = c.indentStr + " " +} + +func (c *debugConfig) dedentLogger() { + if len(c.indentStr) > 0 { + c.indentStr = c.indentStr[1:] + } +} + +func (c debugConfig) logf(format string, args ...interface{}) { + s := fmt.Sprintf(c.indentStr+format, args...) + for _, logger := range c.loggers { + logger(s) + } +} + +func (c *debugConfig) generateGraph() { + buf := &bytes.Buffer{} + err := c.graphviz.Render(c.graph, graphviz.XDOT, buf) + if err != nil { + c.logf("Error rendering DOT graph: %+v", err) + return + } + + dot := buf.String() + if c.logVisualizer { + c.logf("DOT Graph: %s", dot) + } + + for _, v := range c.visualizers { + v(dot) + } + + err = c.graph.Close() + if err != nil { + c.logf("Error closing graph: %+v", err) + return + } + + err = c.graphviz.Close() + if err != nil { + c.logf("Error closing graphviz: %+v", err) + } +} + +func (c *debugConfig) addFuncVisualizer(f func(string)) { + c.visualizers = append(c.visualizers, func(dot string) { + f(dot) + }) +} + +func (c *debugConfig) enableLogVisualizer() { + c.logVisualizer = true +} + +func (c *debugConfig) addFileVisualizer(filename string, format string) { + c.visualizers = append(c.visualizers, func(_ string) { + err := c.graphviz.RenderFilename(c.graph, graphviz.Format(format), filename) + if err != nil { + c.logf("Error saving graphviz file %s with format %s: %+v", filename, format, err) + } else { + path, err := filepath.Abs(filename) + if err == nil { + c.logf("Saved graph of container to %s", path) + } + } + }) +} + +func (c *debugConfig) locationGraphNode(location Location, scope Scope) (*cgraph.Node, error) { + graph := c.scopeSubGraph(scope) + node, found, err := c.findOrCreateGraphNode(graph, location.Name()) + if err != nil { + return nil, err + } + + if found { + return node, nil + } + + node.SetShape(cgraph.BoxShape) + node.SetColor("lightgrey") + return node, nil +} + +func (c *debugConfig) typeGraphNode(typ reflect.Type) (*cgraph.Node, error) { + node, found, err := c.findOrCreateGraphNode(c.graph, typ.String()) + if err != nil { + return nil, err + } + + if found { + return node, nil + } + + node.SetColor("lightgrey") + return node, err +} + +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 { + return nil, false, errors.Wrapf(err, "error finding graph node %s", name) + } + + if node != nil { + return node, true, nil + } + + node, err = subGraph.CreateNode(name) + if err != nil { + return nil, false, errors.Wrapf(err, "error creating graph node %s", name) + } + + return node, false, nil +} + +func (c *debugConfig) scopeSubGraph(scope Scope) *cgraph.Graph { + graph := c.graph + if scope != nil { + gname := fmt.Sprintf("cluster_%s", scope.Name()) + graph = c.graph.SubGraph(gname, 1) + graph.SetLabel(fmt.Sprintf("Scope: %s", scope.Name())) + } + return graph +} + +func (c *debugConfig) addGraphEdge(from *cgraph.Node, to *cgraph.Node) { + _, err := c.graph.CreateEdge("", from, to) + if err != nil { + c.logf("error creating graph edge") + } +} diff --git a/container/option.go b/container/option.go index 9388ab6f64a9..e327bc3055cb 100644 --- a/container/option.go +++ b/container/option.go @@ -1,8 +1,6 @@ package container import ( - "fmt" - "os" "reflect" "github.com/pkg/errors" @@ -10,8 +8,7 @@ import ( // Option is a functional option for a container. type Option interface { - applyConfig(*config) error - applyContainer(*container) error + apply(*container) error } // Provide creates a container option which registers the provided dependency @@ -64,118 +61,31 @@ func Supply(values ...interface{}) Option { }) } -// Logger creates an option which provides a logger function which will -// receive all log messages from the container. -func Logger(logger func(string)) Option { - return configOption(func(c *config) error { - logger("Initializing logger") - c.loggers = append(c.loggers, logger) - return nil - }) -} - -func StdoutLogger() Option { - return Logger(func(s string) { - _, _ = fmt.Fprintln(os.Stdout, s) - }) -} - -// Visualizer creates an option which provides a visualizer function which -// will receive a rendering of the container in the Graphiz DOT format -// whenever the container finishes building or fails due to an error. The -// graph is color-coded to aid debugging. -func Visualizer(visualizer func(dotGraph string)) Option { - return configOption(func(c *config) error { - c.addFuncVisualizer(visualizer) - return nil - }) -} - -func LogVisualizer() Option { - return configOption(func(c *config) error { - c.enableLogVisualizer() - return nil - }) -} - -func FileVisualizer(filename, format string) Option { - return configOption(func(c *config) error { - c.addFileVisualizer(filename, format) - return nil - }) -} - -func Debug() Option { - return Options( - StdoutLogger(), - LogVisualizer(), - FileVisualizer("container_dump.svg", "svg"), - ) -} - // Error creates an option which causes the dependency injection container to // fail immediately. func Error(err error) Option { - return configOption(func(*config) error { + return containerOption(func(*container) error { return errors.WithStack(err) }) } // Options creates an option which bundles together other options. func Options(opts ...Option) Option { - return option{ - configOption: func(cfg *config) error { - for _, opt := range opts { - err := opt.applyConfig(cfg) - if err != nil { - return errors.WithStack(err) - } - } - return nil - }, - containerOption: func(ctr *container) error { - for _, opt := range opts { - err := opt.applyContainer(ctr) - if err != nil { - return errors.WithStack(err) - } + return containerOption(func(ctr *container) error { + for _, opt := range opts { + err := opt.apply(ctr) + if err != nil { + return errors.WithStack(err) } - return nil - }, - } -} - -type configOption func(*config) error - -func (c configOption) applyConfig(cfg *config) error { - return c(cfg) -} - -func (c configOption) applyContainer(*container) error { - return nil + } + return nil + }) } type containerOption func(*container) error -func (c containerOption) applyConfig(*config) error { - return nil -} - -func (c containerOption) applyContainer(ctr *container) error { +func (c containerOption) apply(ctr *container) error { return c(ctr) } -type option struct { - configOption - containerOption -} - -func (o option) applyConfig(c *config) error { - return o.configOption(c) -} - -func (o option) applyContainer(c *container) error { - return o.containerOption(c) -} - -var _, _, _ Option = (*configOption)(nil), (*containerOption)(nil), option{} +var _ Option = (*containerOption)(nil) diff --git a/container/run.go b/container/run.go index 0e680d45fbee..12d1b9e2f113 100644 --- a/container/run.go +++ b/container/run.go @@ -8,24 +8,32 @@ package container // Ex: // Run(func (x int) error { println(x) }, Provide(func() int { return 1 })) func Run(invoker interface{}, opts ...Option) error { + return RunDebug(invoker, nil, 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 := newConfig() + cfg, err := newDebugConfig() if err != nil { return err } defer cfg.generateGraph() // always generate graph on exit - err = opt.applyConfig(cfg) - if err != nil { - return err + if debugOpt != nil { + err = debugOpt.applyConfig(cfg) + if err != nil { + return err + } } cfg.logf("Registering providers") cfg.indentLogger() ctr := newContainer(cfg) - err = opt.applyContainer(ctr) + err = opt.apply(ctr) if err != nil { cfg.logf("Failed registering providers because of: %+v", err) return err