github.1git.de/docker/cli@v26.1.3+incompatible/cli/command/telemetry_utils.go (about) 1 package command 2 3 import ( 4 "context" 5 "fmt" 6 "strings" 7 "time" 8 9 "github.com/docker/cli/cli/version" 10 "github.com/moby/term" 11 "github.com/pkg/errors" 12 "github.com/spf13/cobra" 13 "go.opentelemetry.io/otel/attribute" 14 "go.opentelemetry.io/otel/metric" 15 ) 16 17 // BaseCommandAttributes returns an attribute.Set containing attributes to attach to metrics/traces 18 func BaseCommandAttributes(cmd *cobra.Command, streams Streams) []attribute.KeyValue { 19 return append([]attribute.KeyValue{ 20 attribute.String("command.name", getCommandName(cmd)), 21 }, stdioAttributes(streams)...) 22 } 23 24 // InstrumentCobraCommands wraps all cobra commands' RunE funcs to set a command duration metric using otel. 25 // 26 // Note: this should be the last func to wrap/modify the PersistentRunE/RunE funcs before command execution. 27 // 28 // can also be used for spans! 29 func (cli *DockerCli) InstrumentCobraCommands(ctx context.Context, cmd *cobra.Command) { 30 // If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default 31 ogPersistentPreRunE := cmd.PersistentPreRunE 32 if ogPersistentPreRunE == nil { 33 ogPersistentPreRun := cmd.PersistentPreRun 34 //nolint:unparam // necessary because error will always be nil here 35 ogPersistentPreRunE = func(cmd *cobra.Command, args []string) error { 36 ogPersistentPreRun(cmd, args) 37 return nil 38 } 39 cmd.PersistentPreRun = nil 40 } 41 42 // wrap RunE in PersistentPreRunE so that this operation gets executed on all children commands 43 cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { 44 // If RunE is nil, make it execute Run and return nil by default 45 ogRunE := cmd.RunE 46 if ogRunE == nil { 47 ogRun := cmd.Run 48 //nolint:unparam // necessary because error will always be nil here 49 ogRunE = func(cmd *cobra.Command, args []string) error { 50 ogRun(cmd, args) 51 return nil 52 } 53 cmd.Run = nil 54 } 55 cmd.RunE = func(cmd *cobra.Command, args []string) error { 56 // start the timer as the first step of every cobra command 57 stopInstrumentation := cli.StartInstrumentation(cmd) 58 cmdErr := ogRunE(cmd, args) 59 stopInstrumentation(cmdErr) 60 return cmdErr 61 } 62 63 return ogPersistentPreRunE(cmd, args) 64 } 65 } 66 67 // StartInstrumentation instruments CLI commands with the individual metrics and spans configured. 68 // It's the main command OTel utility, and new command-related metrics should be added to it. 69 // It should be called immediately before command execution, and returns a stopInstrumentation function 70 // that must be called with the error resulting from the command execution. 71 func (cli *DockerCli) StartInstrumentation(cmd *cobra.Command) (stopInstrumentation func(error)) { 72 baseAttrs := BaseCommandAttributes(cmd, cli) 73 return startCobraCommandTimer(cli.MeterProvider(), baseAttrs) 74 } 75 76 func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue) func(err error) { 77 meter := getDefaultMeter(mp) 78 durationCounter, _ := meter.Float64Counter( 79 "command.time", 80 metric.WithDescription("Measures the duration of the cobra command"), 81 metric.WithUnit("ms"), 82 ) 83 start := time.Now() 84 85 return func(err error) { 86 // Use a new context for the export so that the command being cancelled 87 // doesn't affect the metrics, and we get metrics for cancelled commands. 88 ctx, cancel := context.WithTimeout(context.Background(), exportTimeout) 89 defer cancel() 90 91 duration := float64(time.Since(start)) / float64(time.Millisecond) 92 cmdStatusAttrs := attributesFromError(err) 93 durationCounter.Add(ctx, duration, 94 metric.WithAttributes(attrs...), 95 metric.WithAttributes(cmdStatusAttrs...), 96 ) 97 if mp, ok := mp.(MeterProvider); ok { 98 mp.ForceFlush(ctx) 99 } 100 } 101 } 102 103 func stdioAttributes(streams Streams) []attribute.KeyValue { 104 // we don't wrap stderr, but we do wrap in/out 105 _, stderrTty := term.GetFdInfo(streams.Err()) 106 return []attribute.KeyValue{ 107 attribute.Bool("command.stdin.isatty", streams.In().IsTerminal()), 108 attribute.Bool("command.stdout.isatty", streams.Out().IsTerminal()), 109 attribute.Bool("command.stderr.isatty", stderrTty), 110 } 111 } 112 113 func attributesFromError(err error) []attribute.KeyValue { 114 attrs := []attribute.KeyValue{} 115 exitCode := 0 116 if err != nil { 117 exitCode = 1 118 if stderr, ok := err.(statusError); ok { 119 // StatusError should only be used for errors, and all errors should 120 // have a non-zero exit status, so only set this here if this value isn't 0 121 if stderr.StatusCode != 0 { 122 exitCode = stderr.StatusCode 123 } 124 } 125 attrs = append(attrs, attribute.String("command.error.type", otelErrorType(err))) 126 } 127 attrs = append(attrs, attribute.Int("command.status.code", exitCode)) 128 129 return attrs 130 } 131 132 // otelErrorType returns an attribute for the error type based on the error category. 133 func otelErrorType(err error) string { 134 name := "generic" 135 if errors.Is(err, context.Canceled) { 136 name = "canceled" 137 } 138 return name 139 } 140 141 // statusError reports an unsuccessful exit by a command. 142 type statusError struct { 143 Status string 144 StatusCode int 145 } 146 147 func (e statusError) Error() string { 148 return fmt.Sprintf("Status: %s, Code: %d", e.Status, e.StatusCode) 149 } 150 151 // getCommandName gets the cobra command name in the format 152 // `... parentCommandName commandName` by traversing it's parent commands recursively. 153 // until the root command is reached. 154 // 155 // Note: The root command's name is excluded. If cmd is the root cmd, return "" 156 func getCommandName(cmd *cobra.Command) string { 157 fullCmdName := getFullCommandName(cmd) 158 i := strings.Index(fullCmdName, " ") 159 if i == -1 { 160 return "" 161 } 162 return fullCmdName[i+1:] 163 } 164 165 // getFullCommandName gets the full cobra command name in the format 166 // `... parentCommandName commandName` by traversing it's parent commands recursively 167 // until the root command is reached. 168 func getFullCommandName(cmd *cobra.Command) string { 169 if cmd.HasParent() { 170 return fmt.Sprintf("%s %s", getFullCommandName(cmd.Parent()), cmd.Name()) 171 } 172 return cmd.Name() 173 } 174 175 // getDefaultMeter gets the default metric.Meter for the application 176 // using the given metric.MeterProvider 177 func getDefaultMeter(mp metric.MeterProvider) metric.Meter { 178 return mp.Meter( 179 "github.com/docker/cli", 180 metric.WithInstrumentationVersion(version.Version), 181 ) 182 }