github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/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/khulnasoft-lab/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 a TracerProvider. This TracerProvider will be configured
    45  	// with the default tracing components for a CLI program along with any options given
    46  	// for the SDK.
    47  	TracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) TracerProvider
    48  
    49  	// MeterProvider returns a MeterProvider. This MeterProvider will be configured
    50  	// with the default metric components for a CLI program along with any options given
    51  	// for the SDK.
    52  	MeterProvider(ctx context.Context, opts ...sdkmetric.Option) MeterProvider
    53  }
    54  
    55  func (cli *DockerCli) Resource() *resource.Resource {
    56  	return cli.res.Get()
    57  }
    58  
    59  func (cli *DockerCli) TracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) TracerProvider {
    60  	allOpts := make([]sdktrace.TracerProviderOption, 0, len(opts)+2)
    61  	allOpts = append(allOpts, sdktrace.WithResource(cli.Resource()))
    62  	allOpts = append(allOpts, dockerSpanExporter(ctx, cli)...)
    63  	allOpts = append(allOpts, opts...)
    64  	return sdktrace.NewTracerProvider(allOpts...)
    65  }
    66  
    67  func (cli *DockerCli) MeterProvider(ctx context.Context, opts ...sdkmetric.Option) MeterProvider {
    68  	allOpts := make([]sdkmetric.Option, 0, len(opts)+2)
    69  	allOpts = append(allOpts, sdkmetric.WithResource(cli.Resource()))
    70  	allOpts = append(allOpts, dockerMetricExporter(ctx, cli)...)
    71  	allOpts = append(allOpts, opts...)
    72  	return sdkmetric.NewMeterProvider(allOpts...)
    73  }
    74  
    75  // WithResourceOptions configures additional options for the default resource. The default
    76  // resource will continue to include its default options.
    77  func WithResourceOptions(opts ...resource.Option) CLIOption {
    78  	return func(cli *DockerCli) error {
    79  		cli.res.AppendOptions(opts...)
    80  		return nil
    81  	}
    82  }
    83  
    84  // WithResource overwrites the default resource and prevents its creation.
    85  func WithResource(res *resource.Resource) CLIOption {
    86  	return func(cli *DockerCli) error {
    87  		cli.res.Set(res)
    88  		return nil
    89  	}
    90  }
    91  
    92  type telemetryResource struct {
    93  	res  *resource.Resource
    94  	opts []resource.Option
    95  	once sync.Once
    96  }
    97  
    98  func (r *telemetryResource) Set(res *resource.Resource) {
    99  	r.res = res
   100  }
   101  
   102  func (r *telemetryResource) Get() *resource.Resource {
   103  	r.once.Do(r.init)
   104  	return r.res
   105  }
   106  
   107  func (r *telemetryResource) init() {
   108  	if r.res != nil {
   109  		r.opts = nil
   110  		return
   111  	}
   112  
   113  	opts := append(r.defaultOptions(), r.opts...)
   114  	res, err := resource.New(context.Background(), opts...)
   115  	if err != nil {
   116  		otel.Handle(err)
   117  	}
   118  	r.res = res
   119  
   120  	// Clear the resource options since they'll never be used again and to allow
   121  	// the garbage collector to retrieve that memory.
   122  	r.opts = nil
   123  }
   124  
   125  func (r *telemetryResource) defaultOptions() []resource.Option {
   126  	return []resource.Option{
   127  		resource.WithDetectors(serviceNameDetector{}),
   128  		resource.WithAttributes(
   129  			// Use a unique instance id so OTEL knows that each invocation
   130  			// of the CLI is its own instance. Without this, downstream
   131  			// OTEL processors may think the same process is restarting
   132  			// continuously.
   133  			semconv.ServiceInstanceID(uuid.Generate().String()),
   134  		),
   135  		resource.WithFromEnv(),
   136  		resource.WithTelemetrySDK(),
   137  	}
   138  }
   139  
   140  func (r *telemetryResource) AppendOptions(opts ...resource.Option) {
   141  	if r.res != nil {
   142  		return
   143  	}
   144  	r.opts = append(r.opts, opts...)
   145  }
   146  
   147  type serviceNameDetector struct{}
   148  
   149  func (serviceNameDetector) Detect(ctx context.Context) (*resource.Resource, error) {
   150  	return resource.StringDetector(
   151  		semconv.SchemaURL,
   152  		semconv.ServiceNameKey,
   153  		func() (string, error) {
   154  			return filepath.Base(os.Args[0]), nil
   155  		},
   156  	).Detect(ctx)
   157  }
   158  
   159  // cliReader is an implementation of Reader that will automatically
   160  // report to a designated Exporter when Shutdown is called.
   161  type cliReader struct {
   162  	sdkmetric.Reader
   163  	exporter sdkmetric.Exporter
   164  }
   165  
   166  func newCLIReader(exp sdkmetric.Exporter) sdkmetric.Reader {
   167  	reader := sdkmetric.NewManualReader(
   168  		sdkmetric.WithTemporalitySelector(deltaTemporality),
   169  	)
   170  	return &cliReader{
   171  		Reader:   reader,
   172  		exporter: exp,
   173  	}
   174  }
   175  
   176  func (r *cliReader) Shutdown(ctx context.Context) error {
   177  	var rm metricdata.ResourceMetrics
   178  	if err := r.Reader.Collect(ctx, &rm); err != nil {
   179  		return err
   180  	}
   181  
   182  	// Place a pretty tight constraint on the actual reporting.
   183  	// We don't want CLI metrics to prevent the CLI from exiting
   184  	// so if there's some kind of issue we need to abort pretty
   185  	// quickly.
   186  	ctx, cancel := context.WithTimeout(ctx, exportTimeout)
   187  	defer cancel()
   188  
   189  	return r.exporter.Export(ctx, &rm)
   190  }
   191  
   192  // deltaTemporality sets the Temporality of every instrument to delta.
   193  //
   194  // This isn't really needed since we create a unique resource on each invocation,
   195  // but it can help with cardinality concerns for downstream processors since they can
   196  // perform aggregation for a time interval and then discard the data once that time
   197  // period has passed. Cumulative temporality would imply to the downstream processor
   198  // that they might receive a successive point and they may unnecessarily keep state
   199  // they really shouldn't.
   200  func deltaTemporality(_ sdkmetric.InstrumentKind) metricdata.Temporality {
   201  	return metricdata.DeltaTemporality
   202  }