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 }