❯ go-clikit
v0.10.0 pkg.go.dev ↗

guides › follow a child process

Follow a child process

Hand a child program the trace of your span, and read its event log to show or forward its progress.

A program can run another program that uses the kit, such as its own command line from a page of its web UI, or a tool call run as a child. The child writes its own event log. The parent reads it, and the two logs name one trace.

parentspan.TraceContext()eventlog.NewTail(path,Reducer | Forwarder)childParseTraceContext(…)progress.NewLog(f, …)progress.jsonlone event a lineTRACEPARENT
The parent hands the child its trace, and reads the child’s event log.

Procedure#

  1. In the parent, put the Traceparent of progress.Span.TraceContext in the child’s environment, as TRACEPARENT. Give the child an event log of its own, in a file of the parent’s, by the flag or the variable that the child takes for it.
  2. In the child, read TRACEPARENT and parse it with progress.ParseTraceContext. Give it to the Bus as BusOptions.Trace, or to cliprogress.Options.Trace.
  3. In the parent, follow the log with eventlog.NewTail. Poll it while the child runs, and once after it ended.
  4. Feed the lines to a eventlog.Reducer, to show the runs in a view, or to a eventlog.Forwarder, to send the events of your trace on to a sink.
dir, err := os.MkdirTemp("", "child")
if err != nil {
	panic(err)
}
defer func() { _ = os.RemoveAll(dir) }()
path := filepath.Join(dir, "progress.jsonl")

// The server, in the span of the call, hands the child its trace.
server := progress.NewBus(progress.BusOptions{Program: "server"})
_, call := progress.Start(progress.WithBus(context.Background(), server), progress.KindCall, "tool call")
traceparent := call.TraceContext().Traceparent()

// The child, run with its event log in path and TRACEPARENT set.
trace, _ := progress.ParseTraceContext(traceparent, "")
f, err := os.Create(path)
if err != nil {
	panic(err)
}
log := progress.NewLog(f, progress.LogOptions{})
bus := progress.NewBus(progress.BusOptions{Sinks: []progress.Sink{log}, Program: "prog", Trace: trace})
fanout.Map(progress.WithBus(context.Background(), bus), []string{"exe1", "exe2", "exe3"},
	fanout.MapOptions[string]{Step: "probe the nodes"},
	func(context.Context, string) (struct{}, error) { return struct{}{}, nil })
bus.Close()
if err := log.Close(); err != nil {
	panic(err)
}
_ = f.Close()

// The server again; Send would send the notification on the call's
// session, and here keeps the last.
var last string
n := mcpprogress.New(mcpprogress.Options{
	Program: "server",
	Send: func(_ context.Context, progress, total float64, message string) error {
		last = fmt.Sprintf("%v/%v %s", progress, total, message)
		return nil
	},
})
tail := eventlog.NewTail(path, eventlog.NewForwarder(n, server.TraceID()))
if _, err := tail.Poll(); err != nil {
	panic(err)
}
if err := n.Close(context.Background()); err != nil {
	panic(err)
}
call.End(nil)
server.Close()
fmt.Println(last)
3/3 probe the nodes: 3/3 done
ExampleNewForwarder in progress/eventlog/example_test.go. CI runs it and checks its output.
NOTE

The event log has version 1. Keys can be added in a release, and are never renamed or removed. A reader passes over the keys and the values that it does not know. See the event log.