mirror of
https://gitea.com/Lydanne/buildx.git
synced 2025-05-18 00:47:48 +08:00

Add the service instance id to the resource attributes to prevent downstream OTEL processors and exporters from thinking that the CLI invocations are a single process that keeps restarting. The unique id can be removed through downstream aggregation to prevent cardinality issues, but we need some way to tell OTEL that it shouldn't reset the counters. Move the check for the experimental flag to its own package and then use that invocation to prevent creating exporters so metrics are disabled completely. This makes it so we don't have to check for the experimental flag in every place we add metrics until we decide to make metrics stable in general. This also moves the OTEL initialization to a `util/metricutil` package to be more consistent with the existing util naming and to differentiate it from the upstream `metric` name. Using both `metrics` and `metric` as import names was confusing since `metric` was an upstream dependency and `metrics` was a local utility. `metricutil` matches with the existing utilities and makes clear that it isn't a spelling mistake. The record version metric has been removed since we weren't planning on keeping that metric anyway and most of the information is now included in the instrumentation library name and version. That function is included as a utility in the `otel/sdk/metric` package to retrieve the appropriate meter from the meter provider. Signed-off-by: Jonathan A. Sternberg <jonathan.sternberg@docker.com>
216 lines
6.2 KiB
Go
216 lines
6.2 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/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
|
|
|
|
// Only metric exporters if the experimental flag is set.
|
|
if confutil.IsExperimental() {
|
|
if exp, err := dockerOtelExporter(cli); err != nil {
|
|
return nil, err
|
|
} else if exp != nil {
|
|
exps = append(exps, exp)
|
|
}
|
|
|
|
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.
|
|
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
|
|
// 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))
|
|
}
|