github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/telemetry.go (about)

     1  package command
     2  
     3  import (
     4  	"context"
     5  	"os"
     6  	"path/filepath"
     7  	"sync"
     8  	"time"
     9  
    10  	"github.com/docker/distribution/uuid"
    11  	"go.opentelemetry.io/otel"
    12  	"go.opentelemetry.io/otel/metric"
    13  	sdkmetric "go.opentelemetry.io/otel/sdk/metric"
    14  	"go.opentelemetry.io/otel/sdk/metric/metricdata"
    15  	"go.opentelemetry.io/otel/sdk/resource"
    16  	sdktrace "go.opentelemetry.io/otel/sdk/trace"
    17  	semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
    18  	"go.opentelemetry.io/otel/trace"
    19  )
    20  
    21  const exportTimeout = 50 * time.Millisecond
    22  
    23  // TracerProvider is an extension of the trace.TracerProvider interface for CLI programs.
    24  type TracerProvider interface {
    25  	trace.TracerProvider
    26  	ForceFlush(ctx context.Context) error
    27  	Shutdown(ctx context.Context) error
    28  }
    29  
    30  // MeterProvider is an extension of the metric.MeterProvider interface for CLI programs.
    31  type MeterProvider interface {
    32  	metric.MeterProvider
    33  	ForceFlush(ctx context.Context) error
    34  	Shutdown(ctx context.Context) error
    35  }
    36  
    37  // TelemetryClient provides the methods for using OTEL tracing or metrics.
    38  type TelemetryClient interface {
    39  	// Resource returns the OTEL Resource configured with this TelemetryClient.
    40  	// This resource may be created lazily, but the resource should be the same
    41  	// each time this function is invoked.
    42  	Resource() *resource.Resource
    43  
    44  	// TracerProvider returns the currently initialized TracerProvider. This TracerProvider will be configured
    45  	// with the default tracing components for a CLI program
    46  	TracerProvider() trace.TracerProvider
    47  
    48  	// MeterProvider returns the currently initialized MeterProvider. This MeterProvider will be configured
    49  	// with the default metric components for a CLI program
    50  	MeterProvider() metric.MeterProvider
    51  }
    52  
    53  func (cli *DockerCli) Resource() *resource.Resource {
    54  	return cli.res.Get()
    55  }
    56  
    57  func (cli *DockerCli) TracerProvider() trace.TracerProvider {
    58  	return otel.GetTracerProvider()
    59  }
    60  
    61  func (cli *DockerCli) MeterProvider() metric.MeterProvider {
    62  	return otel.GetMeterProvider()
    63  }
    64  
    65  // WithResourceOptions configures additional options for the default resource. The default
    66  // resource will continue to include its default options.
    67  func WithResourceOptions(opts ...resource.Option) CLIOption {
    68  	return func(cli *DockerCli) error {
    69  		cli.res.AppendOptions(opts...)
    70  		return nil
    71  	}
    72  }
    73  
    74  // WithResource overwrites the default resource and prevents its creation.
    75  func WithResource(res *resource.Resource) CLIOption {
    76  	return func(cli *DockerCli) error {
    77  		cli.res.Set(res)
    78  		return nil
    79  	}
    80  }
    81  
    82  type telemetryResource struct {
    83  	res  *resource.Resource
    84  	opts []resource.Option
    85  	once sync.Once
    86  }
    87  
    88  func (r *telemetryResource) Set(res *resource.Resource) {
    89  	r.res = res
    90  }
    91  
    92  func (r *telemetryResource) Get() *resource.Resource {
    93  	r.once.Do(r.init)
    94  	return r.res
    95  }
    96  
    97  func (r *telemetryResource) init() {
    98  	if r.res != nil {
    99  		r.opts = nil
   100  		return
   101  	}
   102  
   103  	opts := append(defaultResourceOptions(), r.opts...)
   104  	res, err := resource.New(context.Background(), opts...)
   105  	if err != nil {
   106  		otel.Handle(err)
   107  	}
   108  	r.res = res
   109  
   110  	// Clear the resource options since they'll never be used again and to allow
   111  	// the garbage collector to retrieve that memory.
   112  	r.opts = nil
   113  }
   114  
   115  // createGlobalMeterProvider creates a new MeterProvider from the initialized DockerCli struct
   116  // with the given options and sets it as the global meter provider
   117  func (cli *DockerCli) createGlobalMeterProvider(ctx context.Context, opts ...sdkmetric.Option) {
   118  	allOpts := make([]sdkmetric.Option, 0, len(opts)+2)
   119  	allOpts = append(allOpts, sdkmetric.WithResource(cli.Resource()))
   120  	allOpts = append(allOpts, dockerMetricExporter(ctx, cli)...)
   121  	allOpts = append(allOpts, opts...)
   122  	mp := sdkmetric.NewMeterProvider(allOpts...)
   123  	otel.SetMeterProvider(mp)
   124  }
   125  
   126  // createGlobalTracerProvider creates a new TracerProvider from the initialized DockerCli struct
   127  // with the given options and sets it as the global tracer provider
   128  func (cli *DockerCli) createGlobalTracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) {
   129  	allOpts := make([]sdktrace.TracerProviderOption, 0, len(opts)+2)
   130  	allOpts = append(allOpts, sdktrace.WithResource(cli.Resource()))
   131  	allOpts = append(allOpts, dockerSpanExporter(ctx, cli)...)
   132  	allOpts = append(allOpts, opts...)
   133  	tp := sdktrace.NewTracerProvider(allOpts...)
   134  	otel.SetTracerProvider(tp)
   135  }
   136  
   137  func defaultResourceOptions() []resource.Option {
   138  	return []resource.Option{
   139  		resource.WithDetectors(serviceNameDetector{}),
   140  		resource.WithAttributes(
   141  			// Use a unique instance id so OTEL knows that each invocation
   142  			// of the CLI is its own instance. Without this, downstream
   143  			// OTEL processors may think the same process is restarting
   144  			// continuously.
   145  			semconv.ServiceInstanceID(uuid.Generate().String()),
   146  		),
   147  		resource.WithFromEnv(),
   148  		resource.WithTelemetrySDK(),
   149  	}
   150  }
   151  
   152  func (r *telemetryResource) AppendOptions(opts ...resource.Option) {
   153  	if r.res != nil {
   154  		return
   155  	}
   156  	r.opts = append(r.opts, opts...)
   157  }
   158  
   159  type serviceNameDetector struct{}
   160  
   161  func (serviceNameDetector) Detect(ctx context.Context) (*resource.Resource, error) {
   162  	return resource.StringDetector(
   163  		semconv.SchemaURL,
   164  		semconv.ServiceNameKey,
   165  		func() (string, error) {
   166  			return filepath.Base(os.Args[0]), nil
   167  		},
   168  	).Detect(ctx)
   169  }
   170  
   171  // cliReader is an implementation of Reader that will automatically
   172  // report to a designated Exporter when Shutdown is called.
   173  type cliReader struct {
   174  	sdkmetric.Reader
   175  	exporter sdkmetric.Exporter
   176  }
   177  
   178  func newCLIReader(exp sdkmetric.Exporter) sdkmetric.Reader {
   179  	reader := sdkmetric.NewManualReader(
   180  		sdkmetric.WithTemporalitySelector(deltaTemporality),
   181  	)
   182  	return &cliReader{
   183  		Reader:   reader,
   184  		exporter: exp,
   185  	}
   186  }
   187  
   188  func (r *cliReader) Shutdown(ctx context.Context) error {
   189  	// Place a pretty tight constraint on the actual reporting.
   190  	// We don't want CLI metrics to prevent the CLI from exiting
   191  	// so if there's some kind of issue we need to abort pretty
   192  	// quickly.
   193  	ctx, cancel := context.WithTimeout(ctx, exportTimeout)
   194  	defer cancel()
   195  
   196  	return r.ForceFlush(ctx)
   197  }
   198  
   199  func (r *cliReader) ForceFlush(ctx context.Context) error {
   200  	var rm metricdata.ResourceMetrics
   201  	if err := r.Reader.Collect(ctx, &rm); err != nil {
   202  		return err
   203  	}
   204  
   205  	return r.exporter.Export(ctx, &rm)
   206  }
   207  
   208  // deltaTemporality sets the Temporality of every instrument to delta.
   209  //
   210  // This isn't really needed since we create a unique resource on each invocation,
   211  // but it can help with cardinality concerns for downstream processors since they can
   212  // perform aggregation for a time interval and then discard the data once that time
   213  // period has passed. Cumulative temporality would imply to the downstream processor
   214  // that they might receive a successive point and they may unnecessarily keep state
   215  // they really shouldn't.
   216  func deltaTemporality(_ sdkmetric.InstrumentKind) metricdata.Temporality {
   217  	return metricdata.DeltaTemporality
   218  }