mirror of
				https://gitea.com/Lydanne/buildx.git
				synced 2025-11-04 18:13:42 +08:00 
			
		
		
		
	The `BUILDX_EXPERIMENTAL` check is removed from the docker otel collector. We'll send metrics to the OTLP endpoint for docker desktop if it is present and enabled regardless of experimental status. The user-facing `OTEL` endpoints for enabling the metric reporting for external use is still hidden behind the experimental flag. We'll likely remove the experimental flag for this feature for v0.14. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
		
			
				
	
	
		
			220 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			220 lines
		
	
	
		
			6.3 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package metricutil
 | 
						|
 | 
						|
import (
 | 
						|
	"context"
 | 
						|
	"fmt"
 | 
						|
	"net/url"
 | 
						|
	"path"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/docker/buildx/util/confutil"
 | 
						|
	"github.com/docker/buildx/version"
 | 
						|
	"github.com/docker/cli/cli/command"
 | 
						|
	"github.com/pkg/errors"
 | 
						|
	"go.opentelemetry.io/otel"
 | 
						|
	"go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc"
 | 
						|
	"go.opentelemetry.io/otel/metric"
 | 
						|
	"go.opentelemetry.io/otel/metric/noop"
 | 
						|
	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
 | 
						|
	"go.opentelemetry.io/otel/sdk/metric/metricdata"
 | 
						|
	"golang.org/x/sync/errgroup"
 | 
						|
)
 | 
						|
 | 
						|
const (
 | 
						|
	otelConfigFieldName = "otel"
 | 
						|
	reportTimeout       = 2 * time.Second
 | 
						|
)
 | 
						|
 | 
						|
// MeterProvider holds a MeterProvider for metric generation and the configured
 | 
						|
// exporters for reporting metrics from the CLI.
 | 
						|
type MeterProvider struct {
 | 
						|
	metric.MeterProvider
 | 
						|
	reader    *sdkmetric.ManualReader
 | 
						|
	exporters []sdkmetric.Exporter
 | 
						|
}
 | 
						|
 | 
						|
// NewMeterProvider configures a MeterProvider from the CLI context.
 | 
						|
func NewMeterProvider(ctx context.Context, cli command.Cli) (*MeterProvider, error) {
 | 
						|
	var exps []sdkmetric.Exporter
 | 
						|
 | 
						|
	if exp, err := dockerOtelExporter(cli); err != nil {
 | 
						|
		return nil, err
 | 
						|
	} else if exp != nil {
 | 
						|
		exps = append(exps, exp)
 | 
						|
	}
 | 
						|
 | 
						|
	if confutil.IsExperimental() {
 | 
						|
		// Expose the user-facing metric exporter only if the experimental flag is set.
 | 
						|
		if exp, err := detectOtlpExporter(ctx); err != nil {
 | 
						|
			return nil, err
 | 
						|
		} else if exp != nil {
 | 
						|
			exps = append(exps, exp)
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	if len(exps) == 0 {
 | 
						|
		// No exporters are configured so use a noop provider.
 | 
						|
		return &MeterProvider{
 | 
						|
			MeterProvider: noop.NewMeterProvider(),
 | 
						|
		}, nil
 | 
						|
	}
 | 
						|
 | 
						|
	reader := sdkmetric.NewManualReader(
 | 
						|
		sdkmetric.WithTemporalitySelector(deltaTemporality),
 | 
						|
	)
 | 
						|
	mp := sdkmetric.NewMeterProvider(
 | 
						|
		sdkmetric.WithResource(Resource()),
 | 
						|
		sdkmetric.WithReader(reader),
 | 
						|
	)
 | 
						|
	return &MeterProvider{
 | 
						|
		MeterProvider: mp,
 | 
						|
		reader:        reader,
 | 
						|
		exporters:     exps,
 | 
						|
	}, nil
 | 
						|
}
 | 
						|
 | 
						|
// Report exports metrics to the configured exporter. This should be done before the CLI
 | 
						|
// exits.
 | 
						|
func (m *MeterProvider) Report(ctx context.Context) {
 | 
						|
	if m.reader == nil {
 | 
						|
		// Not configured.
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	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.
 | 
						|
		otel.Handle(err)
 | 
						|
		return
 | 
						|
	}
 | 
						|
 | 
						|
	var eg errgroup.Group
 | 
						|
	for _, exp := range m.exporters {
 | 
						|
		exp := exp
 | 
						|
		eg.Go(func() error {
 | 
						|
			if err := exp.Export(ctx, &rm); err != nil {
 | 
						|
				otel.Handle(err)
 | 
						|
			}
 | 
						|
			_ = 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
 | 
						|
// 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) {
 | 
						|
	endpoint, err := otelExporterOtlpEndpoint(cli)
 | 
						|
	if endpoint == "" || err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	// Parse the endpoint. The docker config expects the endpoint to be
 | 
						|
	// in the form of a URL to match the environment variable, but this
 | 
						|
	// option doesn't correspond directly to WithEndpoint.
 | 
						|
	//
 | 
						|
	// We pretend we're the same as the environment reader.
 | 
						|
	u, err := url.Parse(endpoint)
 | 
						|
	if err != nil {
 | 
						|
		return nil, errors.Errorf("docker otel endpoint is invalid: %s", err)
 | 
						|
	}
 | 
						|
 | 
						|
	var opts []otlpmetricgrpc.Option
 | 
						|
	switch u.Scheme {
 | 
						|
	case "unix":
 | 
						|
		// Unix sockets are a bit weird. OTEL seems to imply they
 | 
						|
		// can be used as an environment variable and are handled properly,
 | 
						|
		// but they don't seem to be as the behavior of the environment variable
 | 
						|
		// is to strip the scheme from the endpoint, but the underlying implementation
 | 
						|
		// needs the scheme to use the correct resolver.
 | 
						|
		//
 | 
						|
		// We'll just handle this in a special way and add the unix:// back to the endpoint.
 | 
						|
		opts = []otlpmetricgrpc.Option{
 | 
						|
			otlpmetricgrpc.WithEndpoint(fmt.Sprintf("unix://%s", path.Join(u.Host, u.Path))),
 | 
						|
			otlpmetricgrpc.WithInsecure(),
 | 
						|
		}
 | 
						|
	case "http":
 | 
						|
		opts = []otlpmetricgrpc.Option{
 | 
						|
			// Omit the scheme when using http or https.
 | 
						|
			otlpmetricgrpc.WithEndpoint(path.Join(u.Host, u.Path)),
 | 
						|
			otlpmetricgrpc.WithInsecure(),
 | 
						|
		}
 | 
						|
	default:
 | 
						|
		opts = []otlpmetricgrpc.Option{
 | 
						|
			// Omit the scheme when using http or https.
 | 
						|
			otlpmetricgrpc.WithEndpoint(path.Join(u.Host, u.Path)),
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	// Hardcoded endpoint from the endpoint.
 | 
						|
	exp, err := otlpmetricgrpc.New(context.Background(), opts...)
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
	return exp, nil
 | 
						|
}
 | 
						|
 | 
						|
// otelExporterOtlpEndpoint retrieves the OTLP endpoint used for the docker reporter
 | 
						|
// from the current context.
 | 
						|
func otelExporterOtlpEndpoint(cli command.Cli) (string, error) {
 | 
						|
	meta, err := cli.ContextStore().GetMetadata(cli.CurrentContext())
 | 
						|
	if err != nil {
 | 
						|
		return "", 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
 | 
						|
	}
 | 
						|
 | 
						|
	otelMap, ok := otelCfg.(map[string]interface{})
 | 
						|
	if !ok {
 | 
						|
		return "", errors.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)
 | 
						|
	return endpoint, nil
 | 
						|
}
 | 
						|
 | 
						|
// 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 {
 | 
						|
	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))
 | 
						|
}
 |