github.com/khulnasoft/cli@v0.0.0-20240402070845-01bcad7beefa/cli/command/telemetry_utils.go (about) 1 package command 2 3 import ( 4 "context" 5 "fmt" 6 "strconv" 7 "strings" 8 "time" 9 10 "github.com/khulnasoft/cli/cli/version" 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 // BaseMetricAttributes returns an attribute.Set containing attributes to attach to metrics/traces 18 func BaseMetricAttributes(cmd *cobra.Command) attribute.Set { 19 attrList := []attribute.KeyValue{ 20 attribute.String("command.name", getCommandName(cmd)), 21 } 22 return attribute.NewSet(attrList...) 23 } 24 25 // InstrumentCobraCommands wraps all cobra commands' RunE funcs to set a command duration metric using otel. 26 // 27 // Note: this should be the last func to wrap/modify the PersistentRunE/RunE funcs before command execution. 28 // 29 // can also be used for spans! 30 func InstrumentCobraCommands(cmd *cobra.Command, mp metric.MeterProvider) { 31 meter := getDefaultMeter(mp) 32 // If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default 33 ogPersistentPreRunE := cmd.PersistentPreRunE 34 if ogPersistentPreRunE == nil { 35 ogPersistentPreRun := cmd.PersistentPreRun 36 //nolint:unparam // necessary because error will always be nil here 37 ogPersistentPreRunE = func(cmd *cobra.Command, args []string) error { 38 ogPersistentPreRun(cmd, args) 39 return nil 40 } 41 cmd.PersistentPreRun = nil 42 } 43 44 // wrap RunE in PersistentPreRunE so that this operation gets executed on all children commands 45 cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { 46 // If RunE is nil, make it execute Run and return nil by default 47 ogRunE := cmd.RunE 48 if ogRunE == nil { 49 ogRun := cmd.Run 50 //nolint:unparam // necessary because error will always be nil here 51 ogRunE = func(cmd *cobra.Command, args []string) error { 52 ogRun(cmd, args) 53 return nil 54 } 55 cmd.Run = nil 56 } 57 cmd.RunE = func(cmd *cobra.Command, args []string) error { 58 // start the timer as the first step of every cobra command 59 stopCobraCmdTimer := startCobraCommandTimer(cmd, meter) 60 cmdErr := ogRunE(cmd, args) 61 stopCobraCmdTimer(cmdErr) 62 return cmdErr 63 } 64 65 return ogPersistentPreRunE(cmd, args) 66 } 67 } 68 69 func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter) func(err error) { 70 ctx := cmd.Context() 71 baseAttrs := BaseMetricAttributes(cmd) 72 durationCounter, _ := meter.Float64Counter( 73 "command.time", 74 metric.WithDescription("Measures the duration of the cobra command"), 75 metric.WithUnit("ms"), 76 ) 77 start := time.Now() 78 79 return func(err error) { 80 duration := float64(time.Since(start)) / float64(time.Millisecond) 81 cmdStatusAttrs := attributesFromError(err) 82 durationCounter.Add(ctx, duration, 83 metric.WithAttributeSet(baseAttrs), 84 metric.WithAttributeSet(attribute.NewSet(cmdStatusAttrs...)), 85 ) 86 } 87 } 88 89 func attributesFromError(err error) []attribute.KeyValue { 90 attrs := []attribute.KeyValue{} 91 exitCode := 0 92 if err != nil { 93 exitCode = 1 94 if stderr, ok := err.(statusError); ok { 95 // StatusError should only be used for errors, and all errors should 96 // have a non-zero exit status, so only set this here if this value isn't 0 97 if stderr.StatusCode != 0 { 98 exitCode = stderr.StatusCode 99 } 100 } 101 attrs = append(attrs, attribute.String("command.error.type", otelErrorType(err))) 102 } 103 attrs = append(attrs, attribute.String("command.status.code", strconv.Itoa(exitCode))) 104 105 return attrs 106 } 107 108 // otelErrorType returns an attribute for the error type based on the error category. 109 func otelErrorType(err error) string { 110 name := "generic" 111 if errors.Is(err, context.Canceled) { 112 name = "canceled" 113 } 114 return name 115 } 116 117 // statusError reports an unsuccessful exit by a command. 118 type statusError struct { 119 Status string 120 StatusCode int 121 } 122 123 func (e statusError) Error() string { 124 return fmt.Sprintf("Status: %s, Code: %d", e.Status, e.StatusCode) 125 } 126 127 // getCommandName gets the cobra command name in the format 128 // `... parentCommandName commandName` by traversing it's parent commands recursively. 129 // until the root command is reached. 130 // 131 // Note: The root command's name is excluded. If cmd is the root cmd, return "" 132 func getCommandName(cmd *cobra.Command) string { 133 fullCmdName := getFullCommandName(cmd) 134 i := strings.Index(fullCmdName, " ") 135 if i == -1 { 136 return "" 137 } 138 return fullCmdName[i+1:] 139 } 140 141 // getFullCommandName gets the full cobra command name in the format 142 // `... parentCommandName commandName` by traversing it's parent commands recursively 143 // until the root command is reached. 144 func getFullCommandName(cmd *cobra.Command) string { 145 if cmd.HasParent() { 146 return fmt.Sprintf("%s %s", getFullCommandName(cmd.Parent()), cmd.Name()) 147 } 148 return cmd.Name() 149 } 150 151 // getDefaultMeter gets the default metric.Meter for the application 152 // using the given metric.MeterProvider 153 func getDefaultMeter(mp metric.MeterProvider) metric.Meter { 154 return mp.Meter( 155 "github.com/khulnasoft/cli", 156 metric.WithInstrumentationVersion(version.Version), 157 ) 158 }