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 }