diff --git a/hkt/main.go b/hkt/main.go index 20c3a60..cda2eb9 100644 --- a/hkt/main.go +++ b/hkt/main.go @@ -68,11 +68,10 @@ func newRunCommand(ctx context.Context, app *command.App) *cobra.Command { } s, err := app.Engine.Run(ctx, files[0]) - if err != nil { - return err - } - return app.Render(s) + app.Render(s.Events) + + return err }, Version: version, SilenceUsage: true, diff --git a/pkg/check/check.go b/pkg/check/check.go index 39b497f..b0d2d59 100644 --- a/pkg/check/check.go +++ b/pkg/check/check.go @@ -1,10 +1,20 @@ package check // import "hookt.dev/cmd/pkg/check" -import "sync" +import ( + "context" + "fmt" + "strconv" + "sync" + + "github.com/itchyny/gojq" + "hookt.dev/cmd/pkg/trace" +) type S struct { mu sync.Mutex + Events Events + Steps struct { OK int Fail int @@ -22,3 +32,75 @@ func (s *S) Fail() { s.Steps.Fail++ s.mu.Unlock() } + +func (s *S) Trace() trace.PatternTrace { + return trace.PatternTrace{ + ParseKey: func(ctx context.Context, q *gojq.Query, err error) { + if err != nil { + return + } + + n, _ := strconv.Atoi(trace.Get(ctx, "step-index")) + desc := trace.Get(ctx, "step-desc") + group := trace.Get(ctx, "pattern-group") + pattern := trace.Get(ctx, "pattern") + + s.mu.Lock() + defer s.mu.Unlock() + + if n >= len(s.Events) { + for i := len(s.Events); i <= n; i++ { + s.Events = append(s.Events, &Event{}) + } + } + + s.Events[n].Desc = desc + s.Events[n].MarkPattern(group, pattern, false) + }, + EqualMatch: func(ctx context.Context, a, b any, eq bool) { + if !eq { + return + } + + n, _ := strconv.Atoi(trace.Get(ctx, "step-index")) + group := trace.Get(ctx, "pattern-group") + pattern := trace.Get(ctx, "pattern") + + s.mu.Lock() + defer s.mu.Unlock() + + s.Events[n].MarkPattern(group, pattern, eq) + }, + } +} + +func (e *Event) MarkPattern(group, pattern string, ok bool) { + switch group { + case "match": + if e.Match == nil { + e.Match = make(map[string]bool) + } + e.Match[pattern] = ok + case "pass": + if e.Pass == nil { + e.Pass = make(map[string]bool) + } + e.Pass[pattern] = ok + case "fail": + if e.Fail == nil { + e.Fail = make(map[string]bool) + } + e.Fail[pattern] = ok + default: + panic(fmt.Errorf("unknown group: %q (pattern=%q, ok=%v)", group, pattern, ok)) + } +} + +type Event struct { + Desc string `json:"desc,omitempty"` + Match map[string]bool `json:"match"` + Pass map[string]bool `json:"pass,omitempty"` + Fail map[string]bool `json:"fail,omitempty"` +} + +type Events []*Event diff --git a/pkg/hookt/engine.go b/pkg/hookt/engine.go index 3c8b369..44b0807 100644 --- a/pkg/hookt/engine.go +++ b/pkg/hookt/engine.go @@ -4,6 +4,7 @@ import ( "context" "log/slog" "os" + "strconv" "github.com/lmittmann/tint" "golang.org/x/sync/errgroup" @@ -11,6 +12,7 @@ import ( "hookt.dev/cmd/pkg/errors" "hookt.dev/cmd/pkg/plugin/builtin" "hookt.dev/cmd/pkg/proto" + "hookt.dev/cmd/pkg/trace" ) var plugins []proto.Interface @@ -43,17 +45,24 @@ func (e *Engine) Run(ctx context.Context, file string) (*check.S, error) { return nil, errors.New("failed to read file: %w", err) } + var s check.S + + ctx = trace.WithPattern(ctx, trace.ContextPattern(ctx).Join(s.Trace())) + w, err := e.p.Parse(ctx, p) if err != nil { return nil, errors.New("failed to parse file: %w", err) } - var s check.S - var g errgroup.Group - for _, job := range w.Jobs { - for _, step := range job.Steps { + for i, job := range w.Jobs { + ctx := trace.With(ctx, "job", job.ID) + ctx = trace.With(ctx, "job-index", strconv.Itoa(i)) + for j, step := range job.Steps { + ctx := trace.With(ctx, "step", step.ID) + ctx = trace.With(ctx, "step-desc", step.Desc) + ctx = trace.With(ctx, "step-index", strconv.Itoa(j)) g.Go(func() error { r, ok := step.With.(proto.Runner) if !ok { @@ -80,5 +89,16 @@ func (e *Engine) Run(ctx context.Context, file string) (*check.S, error) { } } - return &s, g.Wait() + done := make(chan error) + + go func() { + done <- g.Wait() + }() + + select { + case <-ctx.Done(): + return &s, ctx.Err() + case err := <-done: + return &s, err + } } diff --git a/pkg/hookt/reflow.go b/pkg/hookt/reflow.go deleted file mode 100644 index 11ef98c..0000000 --- a/pkg/hookt/reflow.go +++ /dev/null @@ -1 +0,0 @@ -package hookt // import "hookt.dev/cmd/pkg/hookt" diff --git a/pkg/id/id.go b/pkg/id/id.go new file mode 100644 index 0000000..d6fb252 --- /dev/null +++ b/pkg/id/id.go @@ -0,0 +1,35 @@ +package id + +import ( + "math/rand" + "strings" +) + +const ( + all = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" +) + +func Gen(n int) string { + return gen(all, 6, 1<<6-1, 63/6, n) +} + +// source: https://stackoverflow.com/questions/22892120/ +func gen(chars string, idxBits uint, idxMask int64, idxMax int, n int) string { + var sb strings.Builder + + sb.Grow(n) + + for i, cache, remain := n-1, rand.Int63(), idxMax; i >= 0; { + if remain == 0 { + cache, remain = rand.Int63(), idxMax + } + if idx := int(cache & idxMask); idx < len(chars) { + sb.WriteByte(chars[idx]) + i-- + } + cache >>= idxBits + remain-- + } + + return sb.String() +} diff --git a/pkg/plugin/builtin/event/event.go b/pkg/plugin/builtin/event/event.go index 06674ff..44360dc 100644 --- a/pkg/plugin/builtin/event/event.go +++ b/pkg/plugin/builtin/event/event.go @@ -2,7 +2,6 @@ package event // import "hookt.dev/cmd/pkg/plugin/builtin/event" import ( "context" - "encoding/json" "log/slog" "time" @@ -10,6 +9,7 @@ import ( "hookt.dev/cmd/pkg/errors" "hookt.dev/cmd/pkg/plugin/builtin/event/wire" "hookt.dev/cmd/pkg/proto" + "hookt.dev/cmd/pkg/trace" ) type Plugin struct { @@ -94,7 +94,7 @@ func (p *Plugin) process() { } } -func (p *Plugin) Step(context.Context) any { +func (p *Plugin) Step(ctx context.Context) any { c := make(chan proto.Message) p.c[c] = struct{}{} it, _ := time.ParseDuration(p.Config.InactiveTimeout) @@ -113,32 +113,30 @@ type Step struct { it time.Duration } -func str(v any) string { - if v == nil { - return "" - } - p, _ := json.Marshal(v) - return string(p) +func group(ctx context.Context, name string) context.Context { + return trace.With(ctx, "pattern-group", name) } func (s *Step) Run(ctx context.Context, c *check.S) error { slog.Debug("event: run", - "match", str(s.Match), - "pass", str(s.Pass), - "fail", str(s.Fail), + "match", s.Match, + "pass", s.Pass, + "fail", s.Fail, ) - match, err := s.p.p.Pattern(ctx, s.Match) + tr := trace.ContextPattern(ctx) + + match, err := s.p.p.Patterns(group(ctx, "match"), s.Match) if err != nil { return errors.New("failed to parse match pattern: %w", err) } - pass, err := s.p.p.Pattern(ctx, s.Pass) + pass, err := s.p.p.Patterns(group(ctx, "pass"), s.Pass) if err != nil { return errors.New("failed to parse pass pattern: %w", err) } - fail, err := s.p.p.Pattern(ctx, s.Fail) + fail, err := s.p.p.Patterns(group(ctx, "fail"), s.Fail) if err != nil { return errors.New("failed to parse fail pattern: %w", err) } @@ -150,6 +148,7 @@ func (s *Step) Run(ctx context.Context, c *check.S) error { select { case <-inactive.C: c.Fail() + tr.MatchTimeout(ctx) return errors.New("step has timed out after %v", s.it) case msg := <-s.c: if !inactive.Stop() { @@ -159,7 +158,7 @@ func (s *Step) Run(ctx context.Context, c *check.S) error { obj := msg.Object() - match, err := match.Match(ctx, obj) + match, err := match.Match(group(ctx, "match"), obj) if err != nil { return errors.New("failed to match on pattern: %w", err) } @@ -168,7 +167,7 @@ func (s *Step) Run(ctx context.Context, c *check.S) error { continue } - fail, err := fail.Match(ctx, obj) + fail, err := fail.Match(group(ctx, "fail"), obj) if err != nil { return errors.New("failed to match fail pattern: %w", err) } @@ -177,7 +176,7 @@ func (s *Step) Run(ctx context.Context, c *check.S) error { return errors.New("failure pattern matched") } - ok, err := pass.Match(ctx, obj) + ok, err := pass.Match(group(ctx, "match"), obj) if err != nil { return errors.New("failed to match ok pattern: %w", err) } diff --git a/pkg/proto/pattern.go b/pkg/proto/pattern.go index 5080711..88a5ea2 100644 --- a/pkg/proto/pattern.go +++ b/pkg/proto/pattern.go @@ -4,7 +4,7 @@ import ( "context" "fmt" "log/slog" - "text/template" + "sort" "hookt.dev/cmd/pkg/errors" "hookt.dev/cmd/pkg/proto/wire" @@ -15,14 +15,21 @@ import ( "sigs.k8s.io/yaml" ) -type Pattern map[*gojq.Query]func(context.Context, any) (bool, error) +type Pattern struct { + Key *gojq.Query + Match func(context.Context, any) (bool, error) +} + +type Patterns []*Pattern + +func (p Patterns) Match(ctx context.Context, obj any) (bool, error) { + for _, p := range p { + ctx := pattern(ctx, p.Key.String()) -func (p Pattern) Match(ctx context.Context, obj any) (bool, error) { - for q, fn := range p { - it := q.RunWithContext(ctx, obj) + it := p.Key.RunWithContext(ctx, obj) slog.Debug("pattern", - "query", q.String(), + "query", p.Key.String(), ) // TODO: Handle multiple results? @@ -31,9 +38,9 @@ func (p Pattern) Match(ctx context.Context, obj any) (bool, error) { return false, nil } - ok, err := fn(ctx, v) + ok, err := p.Match(ctx, v) if err != nil { - return false, errors.New("failed to match jq %q: %w", q.String(), err) + return false, errors.New("failed to match jq %q: %w", p.Key.String(), err) } if !ok { return false, nil @@ -43,16 +50,27 @@ func (p Pattern) Match(ctx context.Context, obj any) (bool, error) { return len(p) != 0, nil } -func (p *P) Pattern(ctx context.Context, obj wire.Object) (Pattern, error) { +func pattern(ctx context.Context, name string) context.Context { + return trace.With(ctx, "pattern", name) +} + +func (p *P) Patterns(ctx context.Context, obj wire.Object) (Patterns, error) { var ( - pt = make(Pattern) + pt = make(Patterns, 0, len(obj)) tr = trace.ContextPattern(ctx) err error ) for k, raw := range obj { - q, e := gojq.Parse(k) - tr.ParseKey(k, q, e) + var ( + e error + q Pattern + ) + + ctx := pattern(ctx, k) + + q.Key, e = gojq.Parse(k) + tr.ParseKey(ctx, q.Key, e) if e != nil { err = errors.Join( err, @@ -64,7 +82,7 @@ func (p *P) Pattern(ctx context.Context, obj wire.Object) (Pattern, error) { var want any e = yaml.Unmarshal(raw, &want) - tr.UnmarshalValue(k, raw, want, e) + tr.UnmarshalValue(ctx, raw, want, e) if e != nil { err = errors.Join( err, @@ -80,20 +98,24 @@ func (p *P) Pattern(ctx context.Context, obj wire.Object) (Pattern, error) { switch want := want.(type) { case bool: - pt[q] = func(_ context.Context, got any) (bool, error) { return want || got == nil, nil } + q.Match = func(_ context.Context, got any) (bool, error) { return want || got == nil, nil } case string: - tv := tr.TemplateValue - tr.TemplateValue = func(_, v string, t *template.Template, e error) { tv(k, v, t, e) } - pt[q] = p.t.Match(trace.WithPattern(ctx, tr), want) + q.Match = p.t.Match(ctx, want) default: - pt[q] = func(_ context.Context, got any) (bool, error) { + q.Match = func(_ context.Context, got any) (bool, error) { ok := cmpEqual(want, got) - tr.EqualMatch(k, want, got, ok) + tr.EqualMatch(ctx, want, got, ok) return ok, nil } } + + pt = append(pt, &q) } + sort.Slice(pt, func(i, j int) bool { + return pt[i].Key.String() < pt[j].Key.String() + }) + return pt, err } diff --git a/pkg/proto/proto.go b/pkg/proto/proto.go index 54cd65b..5493920 100644 --- a/pkg/proto/proto.go +++ b/pkg/proto/proto.go @@ -4,6 +4,8 @@ import ( "context" "encoding/json" "log/slog" + "strconv" + "strings" "hookt.dev/cmd/pkg/errors" "hookt.dev/cmd/pkg/proto/wire" @@ -17,6 +19,7 @@ type Workflow struct { } type Job struct { + ID string Plugins []Plugin Steps []Step } @@ -66,9 +69,15 @@ func (p *P) Parse(ctx context.Context, q []byte) (*Workflow, error) { w.Jobs = make([]Job, len(raw.Jobs)) + uniq := make(map[string]struct{}) + for i, job := range raw.Jobs { j := &w.Jobs[i] + if strings.HasPrefix(job.ID, "#") { + return nil, errors.New("#job-%d: error reading job: id cannot start with #", i) + } + slog.Debug("wiring jobs", "index", i, "job", job.ID, @@ -76,8 +85,17 @@ func (p *P) Parse(ctx context.Context, q []byte) (*Workflow, error) { tr.WireJob(i, &job) + j.ID = nonempty(job.ID, "#job-"+strconv.Itoa(i)) j.Plugins = make([]Plugin, len(job.Plugins)) + if _, ok := uniq[j.ID]; ok { + return nil, errors.New("#job-%d: error reading job: duplicate id %q", i, j.ID) + } + + uniq[j.ID] = struct{}{} + + ctx := trace.With(ctx, "job", j.ID) + for k, plugin := range job.Plugins { iface, ok := p.m[plugin.Uses] if !ok { @@ -104,29 +122,41 @@ func (p *P) Parse(ctx context.Context, q []byte) (*Workflow, error) { j.Steps = make([]Step, len(job.Steps)) + uniq := make(map[string]struct{}) + for k, step := range job.Steps { iface, ok := p.m[step.Uses] if !ok { - return nil, errors.New("error reading plugin %q step: not found", step.Uses) + return nil, errors.New("#step-%d: error reading plugin %q step: not found", k, step.Uses) } - slog.Debug("wiring steps", - "index", k, - "step", step.Uses, - "with", string(step.With), - ) + if strings.HasPrefix(step.ID, "#") { + return nil, errors.New("#step-%d: error reading plugin %q step: id cannot start with #", k, step.Uses) + } s := &j.Steps[k] s.Uses = step.Uses - s.ID = step.ID + s.ID = nonempty(step.ID, "#step-"+strconv.Itoa(k)) s.Desc = step.Desc - s.With = iface.Step(ctx) + s.With = iface.Step(trace.With(ctx, "step", s.ID)) + + if _, ok := uniq[s.ID]; ok { + return nil, errors.New("error reading plugin %q step: duplicate id %q", step.Uses, s.ID) + } + + uniq[s.ID] = struct{}{} if err := yaml.Unmarshal(step.With, s.With); err != nil { - return nil, errors.New("error reading plugin %q step: %w", step.Uses, err) + return nil, errors.New("%s: error reading plugin %q step: %w", s.ID, step.Uses, err) } + slog.Debug("wiring steps", + "id", s.ID, + "step", step.Uses, + "with", string(step.With), + ) + tr.WireStep(k, &step, s.With) } @@ -151,3 +181,13 @@ func (p *P) Parse(ctx context.Context, q []byte) (*Workflow, error) { return &w, nil } + +func nonempty[T comparable](t ...T) T { + var zero T + for _, v := range t { + if v != zero { + return v + } + } + return zero +} diff --git a/pkg/proto/template.go b/pkg/proto/template.go index 02b727e..2624625 100644 --- a/pkg/proto/template.go +++ b/pkg/proto/template.go @@ -66,10 +66,11 @@ func (t *T) funcs() template.FuncMap { func (t *T) Match(ctx context.Context, data string) func(context.Context, any) (bool, error) { tr := trace.ContextPattern(ctx) tmpl, err := t.Parse("", data) - tr.TemplateValue("", data, tmpl, err) + tr.TemplateValue(ctx, data, tmpl, err) if err != nil { return func(context.Context, any) (bool, error) { return false, err } } + return func(ctx context.Context, got any) (bool, error) { var ( buf bytes.Buffer @@ -77,7 +78,7 @@ func (t *T) Match(ctx context.Context, data string) func(context.Context, any) ( ) err := tmpl.Execute(&buf, got) - tr.ExecuteMatch("", buf.Bytes(), err) + tr.ExecuteMatch(ctx, buf.Bytes(), err) if err != nil { return false, errors.New("failed to evaluate %q: %w", data, err) } @@ -85,7 +86,7 @@ func (t *T) Match(ctx context.Context, data string) func(context.Context, any) ( var want any err = yaml.Unmarshal(buf.Bytes(), &want) - tr.UnmarshalMatch("", buf.Bytes(), want, err) + tr.UnmarshalMatch(ctx, buf.Bytes(), want, err) if err != nil { return false, errors.New("failed to parse result: %w", err) } @@ -95,7 +96,7 @@ func (t *T) Match(ctx context.Context, data string) func(context.Context, any) ( return want, nil default: ok := cmpEqual(want, got) - tr.EqualMatch("", want, got, ok) + tr.EqualMatch(ctx, want, got, ok) return ok, nil } } diff --git a/pkg/proto/wire/model.go b/pkg/proto/wire/model.go index 62f11f7..06152df 100644 --- a/pkg/proto/wire/model.go +++ b/pkg/proto/wire/model.go @@ -2,6 +2,7 @@ package wire import ( "encoding/json" + "log/slog" ) type Workflow struct { @@ -9,7 +10,7 @@ type Workflow struct { } type Job struct { - ID string `json:"id"` + ID string `json:"id,omitempty"` Plugins []Plugin `json:"plugins"` Steps []Step `json:"steps"` } @@ -36,6 +37,14 @@ type Generic = json.RawMessage type Object map[string]Generic +func (o Object) LogValue() slog.Value { + p, err := json.Marshal(o) + if err != nil { + panic("unexpected error: " + err.Error()) + } + return slog.StringValue(string(p)) +} + type Step struct { Uses string `json:"uses"` ID string `json:"id,omitempty"` diff --git a/pkg/trace/trace.go b/pkg/trace/trace.go index f900eea..2423b6a 100644 --- a/pkg/trace/trace.go +++ b/pkg/trace/trace.go @@ -20,14 +20,22 @@ var ( RunStep: func() {}, MatchStep: func() {}, TapMessage: func() {}, + Step: func(string) StepTrace { return nopStep }, + } + nopStep = StepTrace{ + PatternGroup: func(string, string) PatternGroupTrace { return nopPatternGroup }, + } + nopPatternGroup = PatternGroupTrace{ + Pattern: func(string) PatternTrace { return nopPattern }, } nopPattern = PatternTrace{ - ParseKey: func(string, *gojq.Query, error) {}, - UnmarshalValue: func(string, []byte, any, error) {}, - TemplateValue: func(string, string, *template.Template, error) {}, - ExecuteMatch: func(string, []byte, error) {}, - UnmarshalMatch: func(string, []byte, any, error) {}, - EqualMatch: func(string, any, any, bool) {}, + ParseKey: func(context.Context, *gojq.Query, error) {}, + UnmarshalValue: func(context.Context, []byte, any, error) {}, + TemplateValue: func(context.Context, string, *template.Template, error) {}, + ExecuteMatch: func(context.Context, []byte, error) {}, + UnmarshalMatch: func(context.Context, []byte, any, error) {}, + EqualMatch: func(context.Context, any, any, bool) {}, + MatchTimeout: func(context.Context) {}, } ) @@ -44,94 +52,96 @@ func LogJob() JobTrace { func LogPattern() PatternTrace { return PatternTrace{ - ParseKey: func(key string, q *gojq.Query, err error) { + ParseKey: func(ctx context.Context, q *gojq.Query, err error) { + tags := append(attrs(ctx)) if err != nil { - slog.Error("trace: ParseKey", - "key", key, - tint.Err(err), - ) + tags = append(tags, tint.Err(err)) + slog.Error("trace: ParseKey", tags...) } else { - slog.Info("trace: ParseKey", - "key", key, - ) + slog.Info("trace: ParseKey", tags...) } }, - UnmarshalValue: func(key string, p []byte, v any, err error) { + UnmarshalValue: func(ctx context.Context, p []byte, v any, err error) { + tags := append(attrs(ctx), + "raw", string(p), + "value", v, + ) if err != nil { - slog.Error("trace: UnmarshalValue", - "key", key, - "raw", string(p), - "value", v, - tint.Err(err), - ) + tags = append(tags, tint.Err(err)) + slog.Error("trace: UnmarshalValue", tags...) } else { - slog.Info("trace: UnmarshalValue", - "key", key, - "raw", string(p), - "value", v, - ) + slog.Info("trace: UnmarshalValue", tags...) } }, - TemplateValue: func(key string, value string, t *template.Template, err error) { + TemplateValue: func(ctx context.Context, value string, t *template.Template, err error) { + tags := append(attrs(ctx), + "value", value, + ) if err != nil { - slog.Error("trace: TemplateValue", - "key", key, - "value", value, - tint.Err(err), - ) + tags = append(tags, tint.Err(err)) + slog.Error("trace: TemplateValue", tags...) } else { - slog.Info("trace: TemplateValue", - "key", key, - "value", value, - ) + slog.Info("trace: TemplateValue", tags...) } }, - ExecuteMatch: func(key string, p []byte, err error) { + ExecuteMatch: func(ctx context.Context, p []byte, err error) { + tags := append(attrs(ctx), + "raw", string(p), + ) if err != nil { - slog.Error("trace: ExecuteMatch", - "key", key, - "raw", string(p), - tint.Err(err), - ) + tags = append(tags, tint.Err(err)) + slog.Error("trace: ExecuteMatch", tags...) } else { - slog.Info("trace: ExecuteMatch", - "key", key, - "raw", string(p), - ) + slog.Info("trace: ExecuteMatch", tags...) } }, - UnmarshalMatch: func(key string, p []byte, v any, err error) { + UnmarshalMatch: func(ctx context.Context, p []byte, v any, err error) { + tags := append(attrs(ctx), + "raw", string(p), + "value", v, + ) if err != nil { - slog.Error("trace: UnmarshalMatch", - "key", key, - "raw", string(p), - "value", v, - tint.Err(err), - ) + tags = append(tags, tint.Err(err)) + slog.Error("trace: UnmarshalMatch", tags...) } else { - slog.Info("trace: UnmarshalMatch", - "key", key, - "raw", string(p), - "value", v, - ) + slog.Info("trace: UnmarshalMatch", tags...) } }, - EqualMatch: func(key string, want any, got any, ok bool) { + EqualMatch: func(ctx context.Context, want any, got any, ok bool) { + tags := append(attrs(ctx), + "want", fmt.Sprintf("%+[1]v (%[1]T)", want), + "got", fmt.Sprintf("%+[1]v (%[1]T)", got), + ) if !ok { - slog.Error("trace: EqualMatch", - "key", key, - "want", fmt.Sprintf("%+[1]v (%[1]T)", want), - "got", fmt.Sprintf("%+[1]v (%[1]T)", got), - ) + slog.Error("trace: EqualMatch", tags...) } else { - slog.Info("trace: EqualMatch", - "key", key, - "want", fmt.Sprintf("%+[1]v (%[1]T)", want), - "got", fmt.Sprintf("%+[1]v (%[1]T)", got), - ) + slog.Info("trace: EqualMatch", tags...) } }, + MatchTimeout: func(ctx context.Context) { + tags := append(attrs(ctx)) + slog.Error("trace: MatchTimeout", tags...) + }, + } +} + +func attrs(ctx context.Context) []any { + var attrs []any + + if job := Get(ctx, "job"); job != "" { + attrs = append(attrs, "job", job) + } + if step := Get(ctx, "step"); step != "" { + attrs = append(attrs, "step", step) + } + if group := Get(ctx, "pattern-group"); group != "" { + attrs = append(attrs, "pattern-group", group) } + if pattern := Get(ctx, "pattern"); pattern != "" { + attrs = append(attrs, "pattern", pattern) + } + + return attrs } func WithJob(ctx context.Context, trace JobTrace) context.Context { @@ -145,6 +155,28 @@ func ContextJob(ctx context.Context) JobTrace { return nopJob } +func WithStep(ctx context.Context, trace StepTrace) context.Context { + return with(ctx, &trace) +} + +func ContextStep(ctx context.Context) StepTrace { + if trace := from[StepTrace](ctx); trace != nil { + return *trace + } + return nopStep +} + +func WithPatternGroup(ctx context.Context, trace PatternGroupTrace) context.Context { + return with(ctx, &trace) +} + +func ContextPatternGroup(ctx context.Context) PatternGroupTrace { + if trace := from[PatternGroupTrace](ctx); trace != nil { + return *trace + } + return nopPatternGroup +} + func WithPattern(ctx context.Context, trace PatternTrace) context.Context { return with(ctx, &trace) } @@ -156,6 +188,19 @@ func ContextPattern(ctx context.Context) PatternTrace { return nopPattern } +type traceKey struct{ string } + +func With(ctx context.Context, key, value string) context.Context { + return context.WithValue(ctx, traceKey{key}, value) +} + +func Get(ctx context.Context, key string) string { + value, _ := ctx.Value(traceKey{key}).(string) + return value +} + +type Trace struct{} + type JobTrace struct { WireJob func(index int, job *wire.Job) WirePlugin func(index int, plugin *wire.Plugin, impl any) @@ -164,15 +209,79 @@ type JobTrace struct { MatchStep func() TapMessage func() + + Step func(id string) StepTrace +} + +type StepTrace struct { + PatternGroup func(step, name string) PatternGroupTrace +} + +type PatternGroupTrace struct { + Pattern func(key string) PatternTrace } type PatternTrace struct { - ParseKey func(string, *gojq.Query, error) - UnmarshalValue func(string, []byte, any, error) - TemplateValue func(string, string, *template.Template, error) - ExecuteMatch func(string, []byte, error) - UnmarshalMatch func(string, []byte, any, error) - EqualMatch func(string, any, any, bool) + ParseKey func(context.Context, *gojq.Query, error) + UnmarshalValue func(context.Context, []byte, any, error) + TemplateValue func(context.Context, string, *template.Template, error) + ExecuteMatch func(context.Context, []byte, error) + UnmarshalMatch func(context.Context, []byte, any, error) + EqualMatch func(context.Context, any, any, bool) + MatchTimeout func(context.Context) +} + +func (pt PatternTrace) Join(extra PatternTrace) PatternTrace { + if extra.ParseKey != nil { + fn := pt.ParseKey + pt.ParseKey = func(ctx context.Context, q *gojq.Query, err error) { + fn(ctx, q, err) + extra.ParseKey(ctx, q, err) + } + } + if extra.UnmarshalValue != nil { + fn := pt.UnmarshalValue + pt.UnmarshalValue = func(ctx context.Context, p []byte, v any, err error) { + fn(ctx, p, v, err) + extra.UnmarshalValue(ctx, p, v, err) + } + } + if extra.TemplateValue != nil { + fn := pt.TemplateValue + pt.TemplateValue = func(ctx context.Context, value string, t *template.Template, err error) { + fn(ctx, value, t, err) + extra.TemplateValue(ctx, value, t, err) + } + } + if extra.ExecuteMatch != nil { + fn := pt.ExecuteMatch + pt.ExecuteMatch = func(ctx context.Context, p []byte, err error) { + fn(ctx, p, err) + extra.ExecuteMatch(ctx, p, err) + } + } + if extra.UnmarshalMatch != nil { + fn := pt.UnmarshalMatch + pt.UnmarshalMatch = func(ctx context.Context, p []byte, v any, err error) { + fn(ctx, p, v, err) + extra.UnmarshalMatch(ctx, p, v, err) + } + } + if extra.EqualMatch != nil { + fn := pt.EqualMatch + pt.EqualMatch = func(ctx context.Context, want any, got any, ok bool) { + fn(ctx, want, got, ok) + extra.EqualMatch(ctx, want, got, ok) + } + } + if extra.MatchTimeout != nil { + fn := pt.MatchTimeout + pt.MatchTimeout = func(ctx context.Context) { + fn(ctx) + extra.MatchTimeout(ctx) + } + } + return pt } type EventInfo struct{}