Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add history trace command #2904

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions commands/history/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ func RootCmd(rootcmd *cobra.Command, dockerCli command.Cli, opts RootOptions) *c
logsCmd(dockerCli, opts),
inspectCmd(dockerCli, opts),
openCmd(dockerCli, opts),
traceCmd(dockerCli, opts),
)

return cmd
Expand Down
260 changes: 260 additions & 0 deletions commands/history/trace.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
package history

import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net"
"os"
"slices"
"strconv"
"strings"
"time"

"github.com/containerd/console"
"github.com/containerd/containerd/v2/core/content/proxy"
"github.com/docker/buildx/builder"
"github.com/docker/buildx/util/cobrautil/completion"
"github.com/docker/buildx/util/otelutil"
"github.com/docker/buildx/util/otelutil/jaeger"
"github.com/docker/cli/cli/command"
controlapi "github.com/moby/buildkit/api/services/control"
"github.com/opencontainers/go-digest"
ocispecs "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/pkg/browser"
"github.com/pkg/errors"
"github.com/spf13/cobra"
jaegerui "github.com/tonistiigi/jaeger-ui-rest"
)

type traceOptions struct {
builder string
ref string
addr string
compare string
}

func loadTrace(ctx context.Context, ref string, nodes []builder.Node) (string, []byte, error) {
var offset *int
if strings.HasPrefix(ref, "^") {
off, err := strconv.Atoi(ref[1:])
if err != nil {
return "", nil, errors.Wrapf(err, "invalid offset %q", ref)
}
offset = &off
ref = ""
}

recs, err := queryRecords(ctx, ref, nodes)
if err != nil {
return "", nil, err
}

var rec *historyRecord

if ref == "" {
slices.SortFunc(recs, func(a, b historyRecord) int {
return b.CreatedAt.AsTime().Compare(a.CreatedAt.AsTime())
})
for _, r := range recs {
if r.CompletedAt != nil {
if offset != nil {
if *offset > 0 {
*offset--
continue
}
}
rec = &r
break
}
}
if offset != nil && *offset > 0 {
return "", nil, errors.Errorf("no completed build found with offset %d", *offset)
}
} else {
rec = &recs[0]
}
if rec == nil {
if ref == "" {
return "", nil, errors.New("no records found")
}
return "", nil, errors.Errorf("no record found for ref %q", ref)
}

if rec.CompletedAt == nil {
return "", nil, errors.Errorf("build %q is not completed, only completed builds can be traced", rec.Ref)
}

if rec.Trace == nil {
// build is complete but no trace yet. try to finalize the trace
time.Sleep(1 * time.Second) // give some extra time for last parts of trace to be written

c, err := rec.node.Driver.Client(ctx)
if err != nil {
return "", nil, err
}
_, err = c.ControlClient().UpdateBuildHistory(ctx, &controlapi.UpdateBuildHistoryRequest{
Ref: rec.Ref,
Finalize: true,
})
if err != nil {
return "", nil, err
}

recs, err := queryRecords(ctx, rec.Ref, []builder.Node{*rec.node})
if err != nil {
return "", nil, err
}

if len(recs) == 0 {
return "", nil, errors.Errorf("build record %q was deleted", rec.Ref)
}

rec = &recs[0]
if rec.Trace == nil {
return "", nil, errors.Errorf("build record %q is missing a trace", rec.Ref)
}
}

c, err := rec.node.Driver.Client(ctx)
if err != nil {
return "", nil, err
}

store := proxy.NewContentStore(c.ContentClient())

ra, err := store.ReaderAt(ctx, ocispecs.Descriptor{
Digest: digest.Digest(rec.Trace.Digest),
MediaType: rec.Trace.MediaType,
Size: rec.Trace.Size,
})
if err != nil {
return "", nil, err
}

spans, err := otelutil.ParseSpanStubs(io.NewSectionReader(ra, 0, ra.Size()))
if err != nil {
return "", nil, err
}

wrapper := struct {
Data []jaeger.Trace `json:"data"`
}{
Data: spans.JaegerData().Data,
}

if len(wrapper.Data) == 0 {
return "", nil, errors.New("no trace data")
}

buf := &bytes.Buffer{}
enc := json.NewEncoder(buf)
enc.SetIndent("", " ")
if err := enc.Encode(wrapper); err != nil {
return "", nil, err
}

return string(wrapper.Data[0].TraceID), buf.Bytes(), nil
}

func runTrace(ctx context.Context, dockerCli command.Cli, opts traceOptions) error {
b, err := builder.New(dockerCli, builder.WithName(opts.builder))
if err != nil {
return err
}

nodes, err := b.LoadNodes(ctx)
if err != nil {
return err
}
for _, node := range nodes {
if node.Err != nil {
return node.Err
}
}

traceID, data, err := loadTrace(ctx, opts.ref, nodes)
if err != nil {
return err
}
srv := jaegerui.NewServer(jaegerui.Config{})
if err := srv.AddTrace(traceID, bytes.NewReader(data)); err != nil {
return err
}
url := "/trace/" + traceID

if opts.compare != "" {
traceIDcomp, data, err := loadTrace(ctx, opts.compare, nodes)
if err != nil {
return errors.Wrapf(err, "failed to load trace for %s", opts.compare)
}
if err := srv.AddTrace(traceIDcomp, bytes.NewReader(data)); err != nil {
return err
}
url = "/trace/" + traceIDcomp + "..." + traceID
}

var term bool
if _, err := console.ConsoleFromFile(os.Stdout); err == nil {
term = true
}

if !term && opts.compare == "" {
fmt.Fprintln(dockerCli.Out(), string(data))
return nil
}

ln, err := net.Listen("tcp", opts.addr)
if err != nil {
return err
}

go func() {
time.Sleep(100 * time.Millisecond)
browser.OpenURL(url)
}()

url = "http://" + ln.Addr().String() + url
fmt.Fprintf(dockerCli.Err(), "Trace available at %s\n", url)

go func() {
<-ctx.Done()
ln.Close()
}()

err = srv.Serve(ln)
if err != nil {
select {
case <-ctx.Done():
return nil
default:
}
}
return err
}

func traceCmd(dockerCli command.Cli, rootOpts RootOptions) *cobra.Command {
var options traceOptions

cmd := &cobra.Command{
Use: "trace [OPTIONS] [REF]",
Short: "Show the OpenTelemetry trace of a build record",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if len(args) > 0 {
options.ref = args[0]
}
options.builder = *rootOpts.Builder
return runTrace(cmd.Context(), dockerCli, options)
},
ValidArgsFunction: completion.Disable,
}

flags := cmd.Flags()
flags.StringVar(&options.addr, "addr", "127.0.0.1:0", "Address to bind the UI server")
flags.StringVar(&options.compare, "compare", "", "Compare with another build reference")

return cmd
}
15 changes: 8 additions & 7 deletions docs/reference/buildx_history.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,14 @@ Commands to work on build records

### Subcommands

| Name | Description |
|:---------------------------------------|:-------------------------------|
| [`inspect`](buildx_history_inspect.md) | Inspect a build |
| [`logs`](buildx_history_logs.md) | Print the logs of a build |
| [`ls`](buildx_history_ls.md) | List build records |
| [`open`](buildx_history_open.md) | Open a build in Docker Desktop |
| [`rm`](buildx_history_rm.md) | Remove build records |
| Name | Description |
|:---------------------------------------|:-----------------------------------------------|
| [`inspect`](buildx_history_inspect.md) | Inspect a build |
| [`logs`](buildx_history_logs.md) | Print the logs of a build |
| [`ls`](buildx_history_ls.md) | List build records |
| [`open`](buildx_history_open.md) | Open a build in Docker Desktop |
| [`rm`](buildx_history_rm.md) | Remove build records |
| [`trace`](buildx_history_trace.md) | Show the OpenTelemetry trace of a build record |


### Options
Expand Down
17 changes: 17 additions & 0 deletions docs/reference/buildx_history_trace.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# docker buildx history trace

<!---MARKER_GEN_START-->
Show the OpenTelemetry trace of a build record

### Options

| Name | Type | Default | Description |
|:----------------|:---------|:--------------|:-----------------------------------------|
| `--addr` | `string` | `127.0.0.1:0` | Address to bind the UI server |
| `--builder` | `string` | | Override the configured builder instance |
| `--compare` | `string` | | Compare with another build reference |
| `-D`, `--debug` | `bool` | | Enable debug logging |


<!---MARKER_GEN_END-->

4 changes: 3 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/docker/buildx

go 1.22.0
go 1.23.1

require (
github.com/Masterminds/semver/v3 v3.2.1
Expand Down Expand Up @@ -46,8 +46,10 @@ require (
github.com/stretchr/testify v1.10.0
github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250206065519-bb2801509720
github.com/zclconf/go-cty v1.16.0
go.opentelemetry.io/otel v1.31.0
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0
go.opentelemetry.io/otel/metric v1.31.0
go.opentelemetry.io/otel/sdk v1.31.0
go.opentelemetry.io/otel/trace v1.31.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,8 @@ github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a h1:EfGw4G0x/8qXW
github.com/tonistiigi/fsutil v0.0.0-20250113203817-b14e27f4135a/go.mod h1:Dl/9oEjK7IqnjAm21Okx/XIxUCFJzvh+XdVHUlBwXTw=
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4 h1:7I5c2Ig/5FgqkYOh/N87NzoyI9U15qUPXhDD8uCupv8=
github.com/tonistiigi/go-csvvalue v0.0.0-20240710180619-ddb21b71c0b4/go.mod h1:278M4p8WsNh3n4a1eqiFcV2FGk7wE5fwUpUom9mK9lE=
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250206065519-bb2801509720 h1:feVEZxSTNtC2GrzidXn4tCwBrSp2MHWGRIwmWVGRQIg=
github.com/tonistiigi/jaeger-ui-rest v0.0.0-20250206065519-bb2801509720/go.mod h1:MD2HGVy33cP0VcvmXMvAPiidkQHKz/04Ja9iYQnhZso=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea h1:SXhTLE6pb6eld/v/cCndK0AMpt1wiVFb/YYmqB3/QG0=
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea/go.mod h1:WPnis/6cRcDZSUvVmezrxJPkiO87ThFYsoUiMwWNDJk=
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab h1:H6aJ0yKQ0gF49Qb2z5hI1UHxSQt4JMyxebFR15KnApw=
Expand Down Expand Up @@ -490,6 +492,8 @@ go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0 h1:FFeLy
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.31.0/go.mod h1:TMu73/k1CP8nBUpDLc71Wj/Kf7ZS9FK5b53VapRsP9o=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0 h1:lUsI2TYsQw2r1IASwoROaCnjdj2cvC2+Jbxvk6nHnWU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.31.0/go.mod h1:2HpZxxQurfGxJlJDblybejHB6RX6pmExPNe517hREw4=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0 h1:UGZ1QwZWY67Z6BmckTU+9Rxn04m2bD3gD6Mk0OIOCPk=
go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.31.0/go.mod h1:fcwWuDuaObkkChiDlhEpSq9+X1C0omv+s5mBtToAQ64=
go.opentelemetry.io/otel/metric v1.31.0 h1:FSErL0ATQAmYHUIzSezZibnyVlft1ybhy4ozRPcF2fE=
go.opentelemetry.io/otel/metric v1.31.0/go.mod h1:C3dEloVbLuYoX41KpmAhOqNriGbA+qqH6PQ5E5mUfnY=
go.opentelemetry.io/otel/sdk v1.31.0 h1:xLY3abVHYZ5HSfOg3l2E5LUj2Cwva5Y7yGxnSW9H5Gk=
Expand Down
Loading
Loading