Skip to content

Commit

Permalink
metrics: send metrics to the otel collector endpoint when active
Browse files Browse the repository at this point in the history
Signed-off-by: Jonathan A. Sternberg <[email protected]>
  • Loading branch information
jsternberg committed Dec 28, 2023
1 parent 1cdefbe commit 6a4d2c1
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 0 deletions.
9 changes: 9 additions & 0 deletions commands/bake.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"github.com/docker/buildx/util/confutil"
"github.com/docker/buildx/util/desktop"
"github.com/docker/buildx/util/dockerutil"
"github.com/docker/buildx/util/metrics"
"github.com/docker/buildx/util/progress"
"github.com/docker/buildx/util/tracing"
"github.com/docker/cli/cli/command"
Expand All @@ -45,6 +46,14 @@ type bakeOptions struct {
func runBake(dockerCli command.Cli, targets []string, in bakeOptions, cFlags commonFlags) (err error) {
ctx := appcontext.Context()

mp, report, err := metrics.MeterProvider(dockerCli)
if err != nil {
return err
}
defer report()

recordVersionInfo(mp, "bake")

ctx, end, err := tracing.TraceCurrentCommand(ctx, "bake")
if err != nil {
return err
Expand Down
32 changes: 32 additions & 0 deletions commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ import (
"github.com/docker/buildx/util/buildflags"
"github.com/docker/buildx/util/desktop"
"github.com/docker/buildx/util/ioset"
"github.com/docker/buildx/util/metrics"
"github.com/docker/buildx/util/progress"
"github.com/docker/buildx/util/tracing"
"github.com/docker/buildx/version"
"github.com/docker/cli-docs-tool/annotation"
"github.com/docker/cli/cli"
"github.com/docker/cli/cli/command"
Expand All @@ -51,6 +53,9 @@ import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
"github.com/spf13/pflag"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"google.golang.org/grpc/codes"
)

Expand Down Expand Up @@ -212,6 +217,15 @@ func (o *buildOptions) toDisplayMode() (progressui.DisplayMode, error) {

func runBuild(dockerCli command.Cli, options buildOptions) (err error) {
ctx := appcontext.Context()

mp, report, err := metrics.MeterProvider(dockerCli)
if err != nil {
return err
}
defer report()

recordVersionInfo(mp, "build")

ctx, end, err := tracing.TraceCurrentCommand(ctx, "build")
if err != nil {
return err
Expand Down Expand Up @@ -926,3 +940,21 @@ func maybeJSONArray(v string) []string {
}
return []string{v}
}

func recordVersionInfo(mp metric.MeterProvider, command string) {
meter := mp.Meter("github.com/docker/buildx")

counter, err := meter.Int64Counter("docker.cli.count")
if err != nil {
otel.Handle(err)
}

counter.Add(context.Background(), 1,
metric.WithAttributes(
attribute.String("command", command),
attribute.String("package", version.Package),
attribute.String("version", version.Version),
attribute.String("revision", version.Revision),
),
)
}
108 changes: 108 additions & 0 deletions util/metrics/metrics.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package metrics

import (
"context"
"fmt"
"time"

"github.com/docker/cli/cli/command"
"github.com/moby/buildkit/util/tracing/detect"
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
"go.opentelemetry.io/otel/metric"
sdkmetric "go.opentelemetry.io/otel/sdk/metric"
"go.opentelemetry.io/otel/sdk/metric/metricdata"
)

const (
otelConfigFieldName = "otel"
shutdownTimeout = time.Second
)

// ReportFunc is invoked to signal the metrics should be sent to the
// desired endpoint. It should be invoked on application shutdown.
type ReportFunc func()

// MeterProvider returns a MeterProvider suitable for CLI usage.
// The primary difference between this metric reader and a more typical
// usage is that metric reporting only happens once when ReportFunc
// is invoked.
func MeterProvider(cli command.Cli) (metric.MeterProvider, ReportFunc, error) {
exp, err := dockerOtelExporter(cli)
if exp == nil || err != nil {
return sdkmetric.NewMeterProvider(), func() {}, err
}

reader := sdkmetric.NewManualReader()
mp := sdkmetric.NewMeterProvider(
sdkmetric.WithResource(detect.Resource()),
sdkmetric.WithReader(reader),
)
return mp, reportFunc(reader, exp), nil
}

// reportFunc returns a ReportFunc for collecting ResourceMetrics and then
// exporting them to the configured Exporter.
func reportFunc(reader sdkmetric.Reader, exp sdkmetric.Exporter) ReportFunc {
return func() {
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()

var rm metricdata.ResourceMetrics
if err := reader.Collect(ctx, &rm); err != nil {
// Error when collecting metrics. Do not send any.
return
}
_ = exp.Export(ctx, &rm)
_ = exp.Shutdown(ctx)
}
}

// dockerOtelExporter reads the CLI metadata to determine an OTLP exporter
// endpoint for docker metrics to be sent.
//
// This location, configuration, and usage is hard-coded as part of
// sending usage statistics so this metric reporting is not meant to be
// user facing.
func dockerOtelExporter(cli command.Cli) (sdkmetric.Exporter, error) {
meta, err := cli.ContextStore().GetMetadata(cli.CurrentContext())
if err != nil {
return nil, err
}

var otelCfg interface{}
switch m := meta.Metadata.(type) {
case command.DockerContext:
otelCfg = m.AdditionalFields[otelConfigFieldName]
case map[string]interface{}:
otelCfg = m[otelConfigFieldName]
}

if otelCfg == nil {
return nil, nil
}

otelMap, ok := otelCfg.(map[string]interface{})
if !ok {
return nil, fmt.Errorf(
"unexpected type for field %q: %T (expected: %T)",
otelConfigFieldName,
otelCfg,
otelMap,
)
}

// keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
endpoint, _ := otelMap["OTEL_EXPORTER_OTLP_ENDPOINT"].(string)
if endpoint == "" {
return nil, nil
}

// Hardcoded endpoint from the endpoint.
exp, err := otlpmetricgrpc.New(context.Background(),
otlpmetricgrpc.WithEndpoint(endpoint),
otlpmetricgrpc.WithInsecure())
if err != nil {
return nil, err
}
return exp, nil
}

0 comments on commit 6a4d2c1

Please sign in to comment.