Skip to content

Commit

Permalink
Adds a simple JSON output as an additional progress indicator
Browse files Browse the repository at this point in the history
A simple JSON output is added as an additional progress indicator to the
progressui package. The JSON output is presently a direct translation of
the raw text with the index separated into its own field and a formatted
message field as another field.

In the future, we might want to expand the printing to include more
structured logging to aid automated applications in reading the output.
This change doesn't have any opinions about what those extra fields
should be or if they should exist and the new interfaces are not exposed
to ensure that future changes can be made backwards compatible if or
when they are added.

Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
  • Loading branch information
jsternberg committed Aug 7, 2023
1 parent 3197395 commit a105c98
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 19 deletions.
14 changes: 13 additions & 1 deletion util/progress/progressui/display.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ type displaySolveStatusOpts struct {
phase string
textDesc string
consoleDesc string
textPrinter textPrinter
}

type DisplaySolveStatusOpt func(b *displaySolveStatusOpts)
Expand All @@ -42,6 +43,13 @@ func WithDesc(text string, console string) DisplaySolveStatusOpt {
}
}

// WithJsonPrinter prints each line of output as a JSON stream instead of as raw text.
func WithJsonPrinter() DisplaySolveStatusOpt {
return func(b *displaySolveStatusOpts) {
b.textPrinter = (*jsonPrinter)(nil)
}
}

func DisplaySolveStatus(ctx context.Context, c console.Console, w io.Writer, ch chan *client.SolveStatus, opts ...DisplaySolveStatusOpt) ([]client.VertexWarning, error) {
modeConsole := c != nil

Expand All @@ -50,8 +58,12 @@ func DisplaySolveStatus(ctx context.Context, c console.Console, w io.Writer, ch
opt(dsso)
}

if dsso.textPrinter == nil {
dsso.textPrinter = (*rawPrinter)(nil)
}

disp := &display{c: c, phase: dsso.phase, desc: dsso.consoleDesc}
printer := &textMux{w: w, desc: dsso.textDesc}
printer := &textMux{w: w, printer: dsso.textPrinter, desc: dsso.textDesc}

if disp.phase == "" {
disp.phase = "Building"
Expand Down
100 changes: 82 additions & 18 deletions util/progress/progressui/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package progressui
import (
"container/ring"
"context"
"encoding/json"
"fmt"
"io"
"os"
Expand All @@ -28,6 +29,7 @@ type lastStatus struct {

type textMux struct {
w io.Writer
printer textPrinter
current digest.Digest
last map[string]lastStatus
notFirst bool
Expand All @@ -54,34 +56,35 @@ func (p *textMux) printVtx(t *trace, dgst digest.Digest) {
if p.current != "" {
old := t.byDigest[p.current]
if old.logsPartial {
fmt.Fprintln(p.w, "")
p.newline()
}
old.logsOffset = 0
old.count = 0
fmt.Fprintf(p.w, "#%d ...\n", old.index)
p.log(old.index, "...")
}

if p.notFirst {
fmt.Fprintln(p.w, "")
p.newline()
} else {
if p.desc != "" {
fmt.Fprintf(p.w, "#0 %s\n\n", p.desc)
p.log(0, p.desc)
p.newline()
}
p.notFirst = true
}

name := v.Name
if os.Getenv("PROGRESS_NO_TRUNC") == "0" {
fmt.Fprintf(p.w, "#%d %s\n", v.index, limitString(v.Name, 72))
} else {
fmt.Fprintf(p.w, "#%d %s\n", v.index, v.Name)
name = limitString(v.Name, 72)
}
p.log(v.index, name)
}

if len(v.events) != 0 {
v.logsOffset = 0
}
for _, ev := range v.events {
fmt.Fprintf(p.w, "#%d %s\n", v.index, ev)
p.log(v.index, ev)
}
v.events = v.events[:0]

Expand Down Expand Up @@ -132,26 +135,26 @@ func (p *textMux) printVtx(t *trace, dgst digest.Digest) {
} else {
isOpenStatus = true
}
fmt.Fprintf(p.w, "#%d %s%s%s\n", v.index, s.ID, bytes, tm)
p.logf(v.index, "%s%s%s", s.ID, bytes, tm)
}
}
v.statusUpdates = map[string]struct{}{}

for _, w := range v.warnings[v.warningIdx:] {
fmt.Fprintf(p.w, "#%d WARN: %s\n", v.index, w.Short)
p.logf(v.index, "WARN: %s", w.Short)
v.warningIdx++
}

for i, l := range v.logs {
if i == 0 && v.logsOffset != 0 { // index has already been printed
l = l[v.logsOffset:]
fmt.Fprintf(p.w, "%s", l)
p.logf(-1, "%s", l)
} else {
fmt.Fprintf(p.w, "#%d %s", v.index, []byte(l))
p.logf(v.index, "%s", []byte(l))
}

if i != len(v.logs)-1 || !v.logsPartial {
fmt.Fprintln(p.w, "")
p.newline()
}
if v.logsBuffer == nil {
v.logsBuffer = ring.New(logsBufferSize)
Expand All @@ -178,16 +181,16 @@ func (p *textMux) printVtx(t *trace, dgst digest.Digest) {
v.count = 0

if v.logsPartial {
fmt.Fprintln(p.w, "")
p.newline()
}
if v.Error != "" {
if strings.HasSuffix(v.Error, context.Canceled.Error()) {
fmt.Fprintf(p.w, "#%d CANCELED\n", v.index)
p.log(v.index, "CANCELED")
} else {
fmt.Fprintf(p.w, "#%d ERROR: %s\n", v.index, v.Error)
p.logf(v.index, "ERROR: %s", v.Error)
}
} else if v.Cached {
fmt.Fprintf(p.w, "#%d CACHED\n", v.index)
p.log(v.index, "CACHED")
} else {
tm := ""
var ivals []interval
Expand All @@ -202,7 +205,7 @@ func (p *textMux) printVtx(t *trace, dgst digest.Digest) {
}
tm = fmt.Sprintf(" %.1fs", dt)
}
fmt.Fprintf(p.w, "#%d DONE%s\n", v.index, tm)
p.logf(v.index, "DONE%s", tm)
}
}

Expand Down Expand Up @@ -325,6 +328,18 @@ func (p *textMux) print(t *trace) {
}
}

func (p *textMux) log(index int, msg string) {
p.printer.Print(p.w, index, msg)
}

func (p *textMux) logf(index int, format string, a ...any) {
p.log(index, fmt.Sprintf(format, a...))
}

func (p *textMux) newline() {
p.log(-1, "")
}

type vtxStat struct {
blockTime time.Duration
speed float64
Expand All @@ -338,3 +353,52 @@ func limitString(s string, l int) string {
}
return s
}

// textPrinter prints the progress to a raw [io.Writer].
type textPrinter interface {
// Print will print a message for the given build step at the index.
//
// If no index is associated with the message, a negative number will
// be used.
Print(w io.Writer, index int, msg string) error
}

// rawPrinter prints the text directly to the [io.Writer] with minor formatting.
type rawPrinter struct{}

func (*rawPrinter) Print(w io.Writer, index int, msg string) error {
if index < 0 {
_, err := fmt.Fprintf(w, "%s\n", msg)
return err
}
_, err := fmt.Fprintf(w, "#%d %s\n", index, msg)
return err
}

// jsonPrinter prints the text in JSON format.
type jsonPrinter struct{}

func (*jsonPrinter) Print(w io.Writer, index int, msg string) error {
if index < 0 && len(msg) == 0 {
// Skip empty messages for the json printer.
return nil
}

v := jsonEntry{
Index: index,
Message: msg,
}
out, err := json.Marshal(v)
if err != nil {
return err
}

_, err = fmt.Fprintf(w, "%s\n", out)
return err
}

// jsonEntry is a single JSON message emitted by the jsonPrinter.
type jsonEntry struct {
Index int `json:"index"`
Message string `json:"msg"`
}

0 comments on commit a105c98

Please sign in to comment.