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  }