mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-29 17:05:46 +08:00
Merge pull request #2224 from jsternberg/otelsdk-package
otel: include service instance id attribute to resource and move to metricutil package
This commit is contained in:
commit
cb856682e9
@ -19,7 +19,6 @@ import (
|
|||||||
"github.com/docker/buildx/util/confutil"
|
"github.com/docker/buildx/util/confutil"
|
||||||
"github.com/docker/buildx/util/desktop"
|
"github.com/docker/buildx/util/desktop"
|
||||||
"github.com/docker/buildx/util/dockerutil"
|
"github.com/docker/buildx/util/dockerutil"
|
||||||
"github.com/docker/buildx/util/metrics"
|
|
||||||
"github.com/docker/buildx/util/progress"
|
"github.com/docker/buildx/util/progress"
|
||||||
"github.com/docker/buildx/util/tracing"
|
"github.com/docker/buildx/util/tracing"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
@ -43,14 +42,6 @@ type bakeOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in bakeOptions, cFlags commonFlags) (err error) {
|
func runBake(ctx context.Context, dockerCli command.Cli, targets []string, in bakeOptions, cFlags commonFlags) (err error) {
|
||||||
mp, report, err := metrics.MeterProvider(dockerCli)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer report()
|
|
||||||
|
|
||||||
recordVersionInfo(mp, "bake")
|
|
||||||
|
|
||||||
ctx, end, err := tracing.TraceCurrentCommand(ctx, "bake")
|
ctx, end, err := tracing.TraceCurrentCommand(ctx, "bake")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -30,10 +30,9 @@ import (
|
|||||||
"github.com/docker/buildx/util/cobrautil"
|
"github.com/docker/buildx/util/cobrautil"
|
||||||
"github.com/docker/buildx/util/desktop"
|
"github.com/docker/buildx/util/desktop"
|
||||||
"github.com/docker/buildx/util/ioset"
|
"github.com/docker/buildx/util/ioset"
|
||||||
"github.com/docker/buildx/util/metrics"
|
"github.com/docker/buildx/util/metricutil"
|
||||||
"github.com/docker/buildx/util/progress"
|
"github.com/docker/buildx/util/progress"
|
||||||
"github.com/docker/buildx/util/tracing"
|
"github.com/docker/buildx/util/tracing"
|
||||||
"github.com/docker/buildx/version"
|
|
||||||
"github.com/docker/cli-docs-tool/annotation"
|
"github.com/docker/cli-docs-tool/annotation"
|
||||||
"github.com/docker/cli/cli"
|
"github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
@ -53,9 +52,6 @@ import (
|
|||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"go.opentelemetry.io/otel"
|
|
||||||
"go.opentelemetry.io/otel/attribute"
|
|
||||||
"go.opentelemetry.io/otel/metric"
|
|
||||||
"google.golang.org/grpc/codes"
|
"google.golang.org/grpc/codes"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -216,13 +212,11 @@ func (o *buildOptions) toDisplayMode() (progressui.DisplayMode, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) (err error) {
|
func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) (err error) {
|
||||||
mp, report, err := metrics.MeterProvider(dockerCli)
|
mp, err := metricutil.NewMeterProvider(ctx, dockerCli)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer report()
|
defer mp.Report(context.Background())
|
||||||
|
|
||||||
recordVersionInfo(mp, "build")
|
|
||||||
|
|
||||||
ctx, end, err := tracing.TraceCurrentCommand(ctx, "build")
|
ctx, end, err := tracing.TraceCurrentCommand(ctx, "build")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -936,30 +930,3 @@ func maybeJSONArray(v string) []string {
|
|||||||
}
|
}
|
||||||
return []string{v}
|
return []string{v}
|
||||||
}
|
}
|
||||||
|
|
||||||
func recordVersionInfo(mp metric.MeterProvider, command string) {
|
|
||||||
// Still in the process of testing/stabilizing these counters.
|
|
||||||
if !isExperimental() {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
meter := mp.Meter("github.com/docker/buildx",
|
|
||||||
metric.WithInstrumentationVersion(version.Version),
|
|
||||||
)
|
|
||||||
|
|
||||||
counter, err := meter.Int64Counter("docker.cli.count",
|
|
||||||
metric.WithDescription("Number of invocations of the docker buildx command."),
|
|
||||||
)
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
2
go.mod
2
go.mod
@ -42,6 +42,7 @@ require (
|
|||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0
|
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0
|
||||||
go.opentelemetry.io/otel/metric v1.19.0
|
go.opentelemetry.io/otel/metric v1.19.0
|
||||||
|
go.opentelemetry.io/otel/sdk v1.19.0
|
||||||
go.opentelemetry.io/otel/sdk/metric v1.19.0
|
go.opentelemetry.io/otel/sdk/metric v1.19.0
|
||||||
go.opentelemetry.io/otel/trace v1.19.0
|
go.opentelemetry.io/otel/trace v1.19.0
|
||||||
golang.org/x/mod v0.13.0
|
golang.org/x/mod v0.13.0
|
||||||
@ -147,7 +148,6 @@ require (
|
|||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
|
||||||
go.opentelemetry.io/otel/exporters/prometheus v0.42.0 // indirect
|
go.opentelemetry.io/otel/exporters/prometheus v0.42.0 // indirect
|
||||||
go.opentelemetry.io/otel/sdk v1.19.0 // indirect
|
|
||||||
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
|
||||||
golang.org/x/crypto v0.17.0 // indirect
|
golang.org/x/crypto v0.17.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
|
golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect
|
||||||
|
15
util/confutil/exp.go
Normal file
15
util/confutil/exp.go
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package confutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsExperimental checks if the experimental flag has been configured.
|
||||||
|
func IsExperimental() bool {
|
||||||
|
if v, ok := os.LookupEnv("BUILDX_EXPERIMENTAL"); ok {
|
||||||
|
vv, _ := strconv.ParseBool(v)
|
||||||
|
return vv
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package metrics
|
package metricutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -7,8 +7,9 @@ import (
|
|||||||
"path"
|
"path"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/docker/buildx/util/confutil"
|
||||||
|
"github.com/docker/buildx/version"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/moby/buildkit/util/tracing/detect"
|
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
|
||||||
"go.opentelemetry.io/otel/metric"
|
"go.opentelemetry.io/otel/metric"
|
||||||
@ -20,75 +21,86 @@ import (
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
otelConfigFieldName = "otel"
|
otelConfigFieldName = "otel"
|
||||||
shutdownTimeout = 2 * time.Second
|
reportTimeout = 2 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
// ReportFunc is invoked to signal the metrics should be sent to the
|
// MeterProvider holds a MeterProvider for metric generation and the configured
|
||||||
// desired endpoint. It should be invoked on application shutdown.
|
// exporters for reporting metrics from the CLI.
|
||||||
type ReportFunc func()
|
type MeterProvider struct {
|
||||||
|
metric.MeterProvider
|
||||||
|
reader *sdkmetric.ManualReader
|
||||||
|
exporters []sdkmetric.Exporter
|
||||||
|
}
|
||||||
|
|
||||||
// MeterProvider returns a MeterProvider suitable for CLI usage.
|
// NewMeterProvider configures a MeterProvider from the CLI context.
|
||||||
// The primary difference between this metric reader and a more typical
|
func NewMeterProvider(ctx context.Context, cli command.Cli) (*MeterProvider, error) {
|
||||||
// usage is that metric reporting only happens once when ReportFunc
|
|
||||||
// is invoked.
|
|
||||||
func MeterProvider(cli command.Cli) (metric.MeterProvider, ReportFunc, error) {
|
|
||||||
var exps []sdkmetric.Exporter
|
var exps []sdkmetric.Exporter
|
||||||
|
|
||||||
if exp, err := dockerOtelExporter(cli); err != nil {
|
// Only metric exporters if the experimental flag is set.
|
||||||
return nil, nil, err
|
if confutil.IsExperimental() {
|
||||||
} else if exp != nil {
|
if exp, err := dockerOtelExporter(cli); err != nil {
|
||||||
exps = append(exps, exp)
|
return nil, err
|
||||||
}
|
} else if exp != nil {
|
||||||
|
exps = append(exps, exp)
|
||||||
|
}
|
||||||
|
|
||||||
if exp, err := detectOtlpExporter(context.Background()); err != nil {
|
if exp, err := detectOtlpExporter(ctx); err != nil {
|
||||||
return nil, nil, err
|
return nil, err
|
||||||
} else if exp != nil {
|
} else if exp != nil {
|
||||||
exps = append(exps, exp)
|
exps = append(exps, exp)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(exps) == 0 {
|
if len(exps) == 0 {
|
||||||
// No exporters are configured so use a noop provider.
|
// No exporters are configured so use a noop provider.
|
||||||
return noop.NewMeterProvider(), func() {}, nil
|
return &MeterProvider{
|
||||||
|
MeterProvider: noop.NewMeterProvider(),
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use delta temporality because, since this is a CLI program, we can never
|
|
||||||
// know the cumulative value.
|
|
||||||
reader := sdkmetric.NewManualReader(
|
reader := sdkmetric.NewManualReader(
|
||||||
sdkmetric.WithTemporalitySelector(deltaTemporality),
|
sdkmetric.WithTemporalitySelector(deltaTemporality),
|
||||||
)
|
)
|
||||||
mp := sdkmetric.NewMeterProvider(
|
mp := sdkmetric.NewMeterProvider(
|
||||||
sdkmetric.WithResource(detect.Resource()),
|
sdkmetric.WithResource(Resource()),
|
||||||
sdkmetric.WithReader(reader),
|
sdkmetric.WithReader(reader),
|
||||||
)
|
)
|
||||||
return mp, reportFunc(reader, exps), nil
|
return &MeterProvider{
|
||||||
|
MeterProvider: mp,
|
||||||
|
reader: reader,
|
||||||
|
exporters: exps,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// reportFunc returns a ReportFunc for collecting ResourceMetrics and then
|
// Report exports metrics to the configured exporter. This should be done before the CLI
|
||||||
// exporting them to the configured Exporter.
|
// exits.
|
||||||
func reportFunc(reader sdkmetric.Reader, exps []sdkmetric.Exporter) ReportFunc {
|
func (m *MeterProvider) Report(ctx context.Context) {
|
||||||
return func() {
|
if m.reader == nil {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
|
// Not configured.
|
||||||
defer cancel()
|
return
|
||||||
|
|
||||||
var rm metricdata.ResourceMetrics
|
|
||||||
if err := reader.Collect(ctx, &rm); err != nil {
|
|
||||||
// Error when collecting metrics. Do not send any.
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var eg errgroup.Group
|
|
||||||
for _, exp := range exps {
|
|
||||||
exp := exp
|
|
||||||
eg.Go(func() error {
|
|
||||||
_ = exp.Export(ctx, &rm)
|
|
||||||
_ = exp.Shutdown(ctx)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Can't report an error because we don't allow it to.
|
|
||||||
_ = eg.Wait()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, reportTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
var rm metricdata.ResourceMetrics
|
||||||
|
if err := m.reader.Collect(ctx, &rm); err != nil {
|
||||||
|
// Error when collecting metrics. Do not send any.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var eg errgroup.Group
|
||||||
|
for _, exp := range m.exporters {
|
||||||
|
exp := exp
|
||||||
|
eg.Go(func() error {
|
||||||
|
_ = exp.Export(ctx, &rm)
|
||||||
|
_ = exp.Shutdown(ctx)
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Can't report an error because we don't allow it to.
|
||||||
|
_ = eg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
// dockerOtelExporter reads the CLI metadata to determine an OTLP exporter
|
// dockerOtelExporter reads the CLI metadata to determine an OTLP exporter
|
||||||
@ -184,6 +196,20 @@ func otelExporterOtlpEndpoint(cli command.Cli) (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// deltaTemporality sets the Temporality of every instrument to delta.
|
// deltaTemporality sets the Temporality of every instrument to delta.
|
||||||
|
//
|
||||||
|
// This isn't really needed since we create a unique resource on each invocation,
|
||||||
|
// but it can help with cardinality concerns for downstream processors since they can
|
||||||
|
// perform aggregation for a time interval and then discard the data once that time
|
||||||
|
// period has passed. Cumulative temporality would imply to the downstream processor
|
||||||
|
// that they might receive a successive point and they may unnecessarily keep state
|
||||||
|
// they really shouldn't.
|
||||||
func deltaTemporality(_ sdkmetric.InstrumentKind) metricdata.Temporality {
|
func deltaTemporality(_ sdkmetric.InstrumentKind) metricdata.Temporality {
|
||||||
return metricdata.DeltaTemporality
|
return metricdata.DeltaTemporality
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Meter returns a Meter from the MetricProvider that indicates the measurement
|
||||||
|
// comes from buildx with the appropriate version.
|
||||||
|
func Meter(mp metric.MeterProvider) metric.Meter {
|
||||||
|
return mp.Meter(version.Package,
|
||||||
|
metric.WithInstrumentationVersion(version.Version))
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
package metrics
|
package metricutil
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -35,13 +35,9 @@ func detectOtlpExporter(ctx context.Context) (sdkmetric.Exporter, error) {
|
|||||||
|
|
||||||
switch proto {
|
switch proto {
|
||||||
case "grpc":
|
case "grpc":
|
||||||
return otlpmetricgrpc.New(ctx,
|
return otlpmetricgrpc.New(ctx)
|
||||||
otlpmetricgrpc.WithTemporalitySelector(deltaTemporality),
|
|
||||||
)
|
|
||||||
case "http/protobuf":
|
case "http/protobuf":
|
||||||
return otlpmetrichttp.New(ctx,
|
return otlpmetrichttp.New(ctx)
|
||||||
otlpmetrichttp.WithTemporalitySelector(deltaTemporality),
|
|
||||||
)
|
|
||||||
// case "http/json": // unsupported by library
|
// case "http/json": // unsupported by library
|
||||||
default:
|
default:
|
||||||
return nil, errors.Errorf("unsupported otlp protocol %v", proto)
|
return nil, errors.Errorf("unsupported otlp protocol %v", proto)
|
53
util/metricutil/resource.go
Normal file
53
util/metricutil/resource.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package metricutil
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"go.opentelemetry.io/otel"
|
||||||
|
"go.opentelemetry.io/otel/sdk/resource"
|
||||||
|
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
res *resource.Resource
|
||||||
|
resOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resource retrieves the OTEL resource for the buildx CLI.
|
||||||
|
func Resource() *resource.Resource {
|
||||||
|
resOnce.Do(func() {
|
||||||
|
var err error
|
||||||
|
res, err = resource.New(context.Background(),
|
||||||
|
resource.WithDetectors(serviceNameDetector{}),
|
||||||
|
resource.WithAttributes(
|
||||||
|
// Use a unique instance id so OTEL knows that each invocation
|
||||||
|
// of the CLI is its own instance. Without this, downstream
|
||||||
|
// OTEL processors may think the same process is restarting
|
||||||
|
// continuously and reset the metric counters.
|
||||||
|
semconv.ServiceInstanceID(uuid.New().String()),
|
||||||
|
),
|
||||||
|
resource.WithFromEnv(),
|
||||||
|
resource.WithTelemetrySDK(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
otel.Handle(err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type serviceNameDetector struct{}
|
||||||
|
|
||||||
|
func (serviceNameDetector) Detect(ctx context.Context) (*resource.Resource, error) {
|
||||||
|
return resource.StringDetector(
|
||||||
|
semconv.SchemaURL,
|
||||||
|
semconv.ServiceNameKey,
|
||||||
|
func() (string, error) {
|
||||||
|
return filepath.Base(os.Args[0]), nil
|
||||||
|
},
|
||||||
|
).Detect(ctx)
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user