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

metrics: measure context transfers for local source operations #2235

Merged
merged 1 commit into from
Feb 23, 2024
Merged
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
3 changes: 2 additions & 1 deletion commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
if _, err := console.ConsoleFromFile(os.Stderr); err == nil {
term = true
}
attributes := buildMetricAttributes(dockerCli, b, &options)

ctx2, cancel := context.WithCancel(context.TODO())
defer cancel()
Expand All @@ -325,6 +326,7 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
fmt.Sprintf("building with %q instance using %s driver", b.Name, b.Driver),
fmt.Sprintf("%s:%s", b.Driver, b.Name),
),
progress.WithMetrics(mp, attributes),
progress.WithOnClose(func() {
printWarnings(os.Stderr, printer.Warnings(), progressMode)
}),
Expand All @@ -333,7 +335,6 @@ func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions)
return err
}

attributes := buildMetricAttributes(dockerCli, b, &options)
done := timeBuildCommand(mp, attributes)
var resp *client.SolveResponse
var retErr error
Expand Down
151 changes: 151 additions & 0 deletions util/progress/metricwriter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
package progress

import (
"context"
"regexp"
"strings"
"time"

"github.com/docker/buildx/util/metricutil"
"github.com/moby/buildkit/client"
"github.com/opencontainers/go-digest"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)

type metricWriter struct {
recorders []metricRecorder
attrs attribute.Set
}

func newMetrics(mp metric.MeterProvider, attrs attribute.Set) *metricWriter {
meter := metricutil.Meter(mp)
return &metricWriter{
recorders: []metricRecorder{
newLocalSourceTransferMetricRecorder(meter, attrs),
},
attrs: attrs,
}
}

func (mw *metricWriter) Write(ss *client.SolveStatus) {
for _, recorder := range mw.recorders {
recorder.Record(ss)
}
}

type metricRecorder interface {
Record(ss *client.SolveStatus)
}

type (
localSourceTransferState struct {
// Attributes holds the attributes specific to this context transfer.
Attributes attribute.Set

// LastTransferSize contains the last byte count for the transfer.
LastTransferSize int64
}
localSourceTransferMetricRecorder struct {
// BaseAttributes holds the set of base attributes for all metrics produced.
BaseAttributes attribute.Set

// State contains the state for individual digests that are being processed.
State map[digest.Digest]*localSourceTransferState

// TransferSize holds the metric for the number of bytes transferred.
TransferSize metric.Int64Counter

// Duration holds the metric for the total time taken to perform the transfer.
Duration metric.Float64Counter
}
)

func newLocalSourceTransferMetricRecorder(meter metric.Meter, attrs attribute.Set) *localSourceTransferMetricRecorder {
mr := &localSourceTransferMetricRecorder{
BaseAttributes: attrs,
State: make(map[digest.Digest]*localSourceTransferState),
}
mr.TransferSize, _ = meter.Int64Counter("source.local.transfer.io",
metric.WithDescription("Measures the number of bytes transferred between the client and server for the context."),
metric.WithUnit("By"))

mr.Duration, _ = meter.Float64Counter("source.local.transfer.time",
metric.WithDescription("Measures the length of time spent transferring the context."),
metric.WithUnit("ms"))
Comment on lines +69 to +75
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These names are based on this section of the instrument naming document for OTEL: https://opentelemetry.io/docs/specs/semconv/general/metrics/#instrument-naming

return mr
}

func (mr *localSourceTransferMetricRecorder) Record(ss *client.SolveStatus) {
for _, v := range ss.Vertexes {
state, ok := mr.State[v.Digest]
if !ok {
attr := detectLocalSourceType(v.Name)
if !attr.Valid() {
// Not a context transfer operation so just ignore.
continue
}

state = &localSourceTransferState{
Attributes: attribute.NewSet(attr),
}
mr.State[v.Digest] = state
}

if v.Started != nil && v.Completed != nil {
dur := float64(v.Completed.Sub(*v.Started)) / float64(time.Millisecond)
mr.Duration.Add(context.Background(), dur,
metric.WithAttributeSet(mr.BaseAttributes),
metric.WithAttributeSet(state.Attributes),
)
}
}

for _, status := range ss.Statuses {
state, ok := mr.State[status.Vertex]
if !ok {
continue
}

if strings.HasPrefix(status.Name, "transferring") {
diff := status.Current - state.LastTransferSize
if diff > 0 {
mr.TransferSize.Add(context.Background(), diff,
metric.WithAttributeSet(mr.BaseAttributes),
metric.WithAttributeSet(state.Attributes),
)
}
}
}
}

var reLocalSourceType = regexp.MustCompile(
strings.Join([]string{
`(?P<context>\[internal] load build context)`,
`(?P<dockerfile>load build definition)`,
`(?P<dockerignore>load \.dockerignore)`,
`(?P<namedcontext>\[context .+] load from client)`,
}, "|"),
)

func detectLocalSourceType(vertexName string) attribute.KeyValue {
match := reLocalSourceType.FindStringSubmatch(vertexName)
if match == nil {
return attribute.KeyValue{}
}

for i, source := range reLocalSourceType.SubexpNames() {
if len(source) == 0 {
// Not a subexpression.
continue
}

// Did we find a match for this subexpression?
if len(match[i]) > 0 {
// Use the match name which corresponds to the name of the source.
return attribute.String("source.local.type", source)
}
}
// No matches found.
return attribute.KeyValue{}
}
19 changes: 18 additions & 1 deletion util/progress/printer.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@ import (
"sync"

"github.com/containerd/console"
"github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/logutil"
"github.com/moby/buildkit/client"
"github.com/moby/buildkit/util/progress/progressui"
"github.com/opencontainers/go-digest"
"github.com/sirupsen/logrus"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
)

type Printer struct {
Expand All @@ -24,6 +27,7 @@ type Printer struct {
warnings []client.VertexWarning
logMu sync.Mutex
logSourceMap map[digest.Digest]interface{}
metrics *metricWriter

// TODO: remove once we can use result context to pass build ref
// see https://github.com/docker/buildx/pull/1861
Expand All @@ -49,6 +53,9 @@ func (p *Printer) Unpause() {

func (p *Printer) Write(s *client.SolveStatus) {
p.status <- s
if p.metrics != nil {
p.metrics.Write(s)
}
}

func (p *Printer) Warnings() []client.VertexWarning {
Expand Down Expand Up @@ -96,7 +103,8 @@ func NewPrinter(ctx context.Context, out console.File, mode progressui.DisplayMo
}

pw := &Printer{
ready: make(chan struct{}),
ready: make(chan struct{}),
metrics: opt.mw,
}
go func() {
for {
Expand Down Expand Up @@ -147,6 +155,7 @@ func (p *Printer) BuildRefs() map[string]string {

type printerOpts struct {
displayOpts []progressui.DisplayOpt
mw *metricWriter

onclose func()
}
Expand All @@ -165,6 +174,14 @@ func WithDesc(text string, console string) PrinterOpt {
}
}

func WithMetrics(mp metric.MeterProvider, attrs attribute.Set) PrinterOpt {
return func(opt *printerOpts) {
if confutil.IsExperimental() {
opt.mw = newMetrics(mp, attrs)
}
}
}

func WithOnClose(onclose func()) PrinterOpt {
return func(opt *printerOpts) {
opt.onclose = onclose
Expand Down