From d5a66d4a9586bc40e8bfdcde7efc13774a14053a Mon Sep 17 00:00:00 2001 From: nikita-gf Date: Fri, 20 Feb 2026 12:00:18 +0000 Subject: [PATCH 1/2] Accept custom logger + log binding errors --- graph.go | 49 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/graph.go b/graph.go index 516a161..415cc32 100644 --- a/graph.go +++ b/graph.go @@ -122,6 +122,7 @@ type graphNode struct { dependents []*graphNode dependentsByKey map[ID][]*graphNode tracer trace.Tracer + logger Logger } const ( @@ -150,8 +151,8 @@ func (gn *graphNode) execute(ctx context.Context, rs *runState) (err error) { tCtx, span := gn.tracer.Start(ctx, gn.task.Name()) defer span.End() - log.Debugf("Starting task %s", gn.task.Name()) - defer log.Debugf("Finished task %s", gn.task.Name()) + gn.logger.Debugf("Starting task %s", gn.task.Name()) + defer gn.logger.Debugf("Finished task %s", gn.task.Name()) bindings, err := gn.task.Execute(tCtx, rs) if err != nil { @@ -169,20 +170,28 @@ func (gn *graphNode) execute(ctx context.Context, rs *runState) (err error) { } } var extra []string + errors := []string{} providesSet := set.NewSet[ID](gn.task.Provides()...) for _, binding := range bindings { if !providesSet.Contains(binding.ID()) { extra = append(extra, binding.ID().String()) } + if binding.Status() == Absent { + err := binding.Error() + if err != nil { + errors = append(errors, fmt.Sprintf("[%s: %s]", binding.ID().String(), err)) + } + span.SetAttributes( attribute.String( traceTaskgraphAbsentKeysPrefix+binding.ID().String(), - fmt.Sprintf("%v", binding.Error()), + fmt.Sprintf("%v", err), ), ) } } + if len(extra) > 0 || len(missing) > 0 { return wrapStackErrorf( "task %s: mismatch between task Provides declaration and returned bindings: missing bindings [%s], got extra bindings [%s]", @@ -192,8 +201,12 @@ func (gn *graphNode) execute(ctx context.Context, rs *runState) (err error) { ) } + if len(errors) > 0 { + gn.logger.Debugf("task %s has binding errors: %s", gn.task.Name(), strings.Join(errors, ", ")) + } + for _, dependent := range gn.dependents { - log.Debugf("task %s signalling dependent %s\n", gn.task.Name(), dependent.task.Name()) + gn.logger.Debugf("task %s signalling dependent %s\n", gn.task.Name(), dependent.task.Name()) if err := rs.signal(tCtx, dependent.id); err != nil { return err } @@ -212,10 +225,10 @@ func (gn *graphNode) canExecute(b Binder) bool { func (gn *graphNode) runFunc(ctx context.Context, rs *runState) func() error { return func() error { if gn.canExecute(rs) { - log.Debugf("task %s starting immediately\n", gn.task.Name()) + gn.logger.Debugf("task %s starting immediately\n", gn.task.Name()) return gn.execute(ctx, rs) } - log.Debugf("task %s has dependencies missing; cannot start immediately\n", gn.task.Name()) + gn.logger.Debugf("task %s has dependencies missing; cannot start immediately\n", gn.task.Name()) signal, ok := rs.signals[gn.id] if !ok { return wrapStackErrorf("signal channel missing for id %q", gn.id) @@ -224,10 +237,10 @@ func (gn *graphNode) runFunc(ctx context.Context, rs *runState) func() error { select { case <-signal: if gn.canExecute(rs) { - log.Debugf("task %s starting\n", gn.task.Name()) + gn.logger.Debugf("task %s starting\n", gn.task.Name()) return gn.execute(ctx, rs) } - log.Debugf("task %s still has dependencies missing\n", gn.task.Name()) + gn.logger.Debugf("task %s still has dependencies missing\n", gn.task.Name()) case <-ctx.Done(): return nil } @@ -241,6 +254,7 @@ type graph struct { allDependencies, allProvided set.Set[ID] nodes []*graphNode tracer trace.Tracer + logger Logger } func (g *graph) buildInputBinder(inputs ...Binding) (Binder, error) { @@ -429,9 +443,14 @@ func (g *graph) Graphviz(includeInputs bool) string { return buf.String() } +type Logger interface { + Debugf(format string, args ...interface{}) +} + type graphOptions struct { tasks []Task tracer trace.Tracer + logger Logger } // A GraphOption is used to configure a new Graph. @@ -459,6 +478,14 @@ func WithTracer(tracer trace.Tracer) GraphOption { } } +func WithLogger(logger Logger) GraphOption { + return func(opts *graphOptions) error { + opts.logger = logger + + return nil + } +} + // New creates a new Graph. Exactly one WithTasks option should be passed. // // Ideally, Graphs should be created on program startup, rather than creating them dynamically. @@ -473,12 +500,17 @@ func New(name string, opts ...GraphOption) (Graph, error) { } } + if o.logger == nil { + o.logger = log + } + g := &graph{ name: name, tasks: o.tasks, allDependencies: set.NewSet[ID](), allProvided: set.NewSet[ID](), tracer: o.tracer, + logger: o.logger, } provideTasks := map[string][]string{} @@ -498,6 +530,7 @@ func New(name string, opts ...GraphOption) (Graph, error) { task: t, dependentsByKey: map[ID][]*graphNode{}, tracer: g.tracer, + logger: g.logger, } g.nodes = append(g.nodes, node) From fb56365c784f215e77491074b5dc6138dae6098f Mon Sep 17 00:00:00 2001 From: nikita-gf Date: Fri, 20 Feb 2026 16:12:56 +0000 Subject: [PATCH 2/2] Lint --- graph.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/graph.go b/graph.go index 415cc32..b5763e2 100644 --- a/graph.go +++ b/graph.go @@ -202,7 +202,11 @@ func (gn *graphNode) execute(ctx context.Context, rs *runState) (err error) { } if len(errors) > 0 { - gn.logger.Debugf("task %s has binding errors: %s", gn.task.Name(), strings.Join(errors, ", ")) + gn.logger.Debugf( + "task %s has binding errors: %s", + gn.task.Name(), + strings.Join(errors, ", "), + ) } for _, dependent := range gn.dependents { @@ -443,6 +447,7 @@ func (g *graph) Graphviz(includeInputs bool) string { return buf.String() } +// Logger logger interface for the graph. type Logger interface { Debugf(format string, args ...interface{}) } @@ -478,6 +483,7 @@ func WithTracer(tracer trace.Tracer) GraphOption { } } +// WithLogger sets a logger for the graph. func WithLogger(logger Logger) GraphOption { return func(opts *graphOptions) error { opts.logger = logger