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

guides › report progress from a library

Report progress from a library

A library reports its work as spans in the context it is given. It draws nothing, and costs nothing when no command listens.

A library does not know if a person, a log or nobody watches it. So it draws nothing. It starts spans in the context it was given, and the command that called it decides what is shown.

Procedure#

  1. Take a context.Context as the first argument of each function that does work worth showing.
  2. Start a span for each step with progress.Start, and use the context that it returns for the work below.
  3. Announce the targets of a step as queued before the first runs, so that a display knows all of the work from the start.
  4. Call progress.Span.Run when a target starts, and progress.Span.End with its error when it ends.
  5. End each span that you started, on every path, also on an error.
// reset is library code: it reports its work as spans in ctx, whether a
// command watches or not.
func reset(ctx context.Context, nodes []string) error {
	ctx, step := progress.Start(ctx, progress.KindStep, "reset the machines",
		progress.Flagged(progress.FlagFold), progress.Total(len(nodes)))
	// Every target is announced queued before the first runs, so that a
	// display knows the whole of the work from the start.
	targets := make([]*progress.Span, len(nodes))
	for i, node := range nodes {
		_, targets[i] = progress.Start(ctx, progress.KindTarget, node, progress.Queued(), progress.Node(node))
	}
	var err error
	for i, target := range targets {
		target.Run()
		if nodes[i] == "exe02" {
			failed := errors.New("exe02: no answer")
			target.End(failed)
			err = failed
			continue
		}
		target.End(nil)
	}
	step.End(err)
	return err
}
reset in progress/example_test.go, which the examples of the package call.

The command attaches a Bus to the context and ends the spans that it started. The library’s spans show up below them:

bus := progress.NewBus(progress.BusOptions{Sinks: []progress.Sink{printer{}}})
ctx, command := progress.Start(progress.WithBus(context.Background(), bus), progress.KindCommand, "power reset")
command.End(reset(ctx, []string{"exe01", "exe02"}))
bus.Close()
1 start command "power reset" running
2 start step "reset the machines" running
3 start target "exe01" queued
4 start target "exe02" queued
5 run target "exe01" running
6 end target "exe01" ended ok
7 run target "exe02" running
8 end target "exe02" ended failed (target): exe02: no answer
9 end step "reset the machines" ended failed (target): exe02: no answer
10 end command "power reset" ended failed (target): exe02: no answer
Example in progress/example_test.go. CI runs it and checks its output.

Without a Bus, nothing#

When no command attached a Bus, Start returns the context as it is and a nil span. The methods of a nil span do nothing. The library makes no allocation on this path, and a test of the kit keeps it so.

ctx := context.Background()
got, span := progress.Start(ctx, progress.KindStep, "reset the machines")
span.Run()
span.End(nil)
fmt.Println(got == ctx, span == nil)
true true
ExampleStart_withoutABus in progress/example_test.go. CI runs it and checks its output.
NOTE

A library imports progress, and never display nor cliprogress. progress has no dependency on a terminal, on cobra or on net/http.

Skip and classify#

  • A target that is left out on purpose ends with progress.Skip or an error of progress.ErrSkipped. It is not counted as failed.
  • The class of an error says what kind of failure it is: of the target, of the transport, a timeout, a refused login, a usage error, a cancel. progress.Classify finds it, and a program adds a rule of its own for errors that say none.
byCode := func(err error) progress.Class {
	if exit := (exitError{}); errors.As(err, &exit) && exit.code == 3 {
		return progress.ClassTransport
	}
	return progress.ClassTarget
}
for _, err := range []error{
	nil,
	context.Canceled,
	fmt.Errorf("exe01: %w", context.DeadlineExceeded),
	exitError{3},
	exitError{1},
	os.ErrPermission,
} {
	fmt.Println(progress.Classify(err, byCode))
}
none
canceled
timeout
transport
target
target
ExampleClassify in progress/example_test.go. CI runs it and checks its output.