mirror of https://github.com/docker/cli.git
Added some telemetry utils
Signed-off-by: Christopher Petito <chrisjpetito@gmail.com>
This commit is contained in:
parent
b8d5454963
commit
160f65d9db
|
@ -0,0 +1,158 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/version"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
// BaseMetricAttributes returns an attribute.Set containing attributes to attach to metrics/traces
|
||||
func BaseMetricAttributes(cmd *cobra.Command) attribute.Set {
|
||||
attrList := []attribute.KeyValue{
|
||||
attribute.String("command.name", getCommandName(cmd)),
|
||||
}
|
||||
return attribute.NewSet(attrList...)
|
||||
}
|
||||
|
||||
// InstrumentCobraCommands wraps all cobra commands' RunE funcs to set a command duration metric using otel.
|
||||
//
|
||||
// Note: this should be the last func to wrap/modify the PersistentRunE/RunE funcs before command execution.
|
||||
//
|
||||
// can also be used for spans!
|
||||
func InstrumentCobraCommands(cmd *cobra.Command, mp metric.MeterProvider) {
|
||||
meter := getDefaultMeter(mp)
|
||||
// If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default
|
||||
ogPersistentPreRunE := cmd.PersistentPreRunE
|
||||
if ogPersistentPreRunE == nil {
|
||||
ogPersistentPreRun := cmd.PersistentPreRun
|
||||
//nolint:unparam // necessary because error will always be nil here
|
||||
ogPersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
ogPersistentPreRun(cmd, args)
|
||||
return nil
|
||||
}
|
||||
cmd.PersistentPreRun = nil
|
||||
}
|
||||
|
||||
// wrap RunE in PersistentPreRunE so that this operation gets executed on all children commands
|
||||
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
// If RunE is nil, make it execute Run and return nil by default
|
||||
ogRunE := cmd.RunE
|
||||
if ogRunE == nil {
|
||||
ogRun := cmd.Run
|
||||
//nolint:unparam // necessary because error will always be nil here
|
||||
ogRunE = func(cmd *cobra.Command, args []string) error {
|
||||
ogRun(cmd, args)
|
||||
return nil
|
||||
}
|
||||
cmd.Run = nil
|
||||
}
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
// start the timer as the first step of every cobra command
|
||||
stopCobraCmdTimer := startCobraCommandTimer(cmd, meter)
|
||||
cmdErr := ogRunE(cmd, args)
|
||||
stopCobraCmdTimer(cmdErr)
|
||||
return cmdErr
|
||||
}
|
||||
|
||||
return ogPersistentPreRunE(cmd, args)
|
||||
}
|
||||
}
|
||||
|
||||
func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter) func(err error) {
|
||||
ctx := cmd.Context()
|
||||
baseAttrs := BaseMetricAttributes(cmd)
|
||||
durationCounter, _ := meter.Float64Counter(
|
||||
"command.time",
|
||||
metric.WithDescription("Measures the duration of the cobra command"),
|
||||
metric.WithUnit("ms"),
|
||||
)
|
||||
start := time.Now()
|
||||
|
||||
return func(err error) {
|
||||
duration := float64(time.Since(start)) / float64(time.Millisecond)
|
||||
cmdStatusAttrs := attributesFromError(err)
|
||||
durationCounter.Add(ctx, duration,
|
||||
metric.WithAttributeSet(baseAttrs),
|
||||
metric.WithAttributeSet(attribute.NewSet(cmdStatusAttrs...)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func attributesFromError(err error) []attribute.KeyValue {
|
||||
attrs := []attribute.KeyValue{}
|
||||
exitCode := 0
|
||||
if err != nil {
|
||||
exitCode = 1
|
||||
if stderr, ok := err.(statusError); ok {
|
||||
// StatusError should only be used for errors, and all errors should
|
||||
// have a non-zero exit status, so only set this here if this value isn't 0
|
||||
if stderr.StatusCode != 0 {
|
||||
exitCode = stderr.StatusCode
|
||||
}
|
||||
}
|
||||
attrs = append(attrs, attribute.String("command.error.type", otelErrorType(err)))
|
||||
}
|
||||
attrs = append(attrs, attribute.String("command.status.code", strconv.Itoa(exitCode)))
|
||||
|
||||
return attrs
|
||||
}
|
||||
|
||||
// otelErrorType returns an attribute for the error type based on the error category.
|
||||
func otelErrorType(err error) string {
|
||||
name := "generic"
|
||||
if errors.Is(err, context.Canceled) {
|
||||
name = "canceled"
|
||||
}
|
||||
return name
|
||||
}
|
||||
|
||||
// statusError reports an unsuccessful exit by a command.
|
||||
type statusError struct {
|
||||
Status string
|
||||
StatusCode int
|
||||
}
|
||||
|
||||
func (e statusError) Error() string {
|
||||
return fmt.Sprintf("Status: %s, Code: %d", e.Status, e.StatusCode)
|
||||
}
|
||||
|
||||
// getCommandName gets the cobra command name in the format
|
||||
// `... parentCommandName commandName` by traversing it's parent commands recursively.
|
||||
// until the root command is reached.
|
||||
//
|
||||
// Note: The root command's name is excluded. If cmd is the root cmd, return ""
|
||||
func getCommandName(cmd *cobra.Command) string {
|
||||
fullCmdName := getFullCommandName(cmd)
|
||||
i := strings.Index(fullCmdName, " ")
|
||||
if i == -1 {
|
||||
return ""
|
||||
}
|
||||
return fullCmdName[i+1:]
|
||||
}
|
||||
|
||||
// getFullCommandName gets the full cobra command name in the format
|
||||
// `... parentCommandName commandName` by traversing it's parent commands recursively
|
||||
// until the root command is reached.
|
||||
func getFullCommandName(cmd *cobra.Command) string {
|
||||
if cmd.HasParent() {
|
||||
return fmt.Sprintf("%s %s", getFullCommandName(cmd.Parent()), cmd.Name())
|
||||
}
|
||||
return cmd.Name()
|
||||
}
|
||||
|
||||
// getDefaultMeter gets the default metric.Meter for the application
|
||||
// using the given metric.MeterProvider
|
||||
func getDefaultMeter(mp metric.MeterProvider) metric.Meter {
|
||||
return mp.Meter(
|
||||
"github.com/docker/cli",
|
||||
metric.WithInstrumentationVersion(version.Version),
|
||||
)
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
func setupCobraCommands() (*cobra.Command, *cobra.Command, *cobra.Command) {
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "root [OPTIONS] COMMAND [ARG...]",
|
||||
}
|
||||
childCmd := &cobra.Command{
|
||||
Use: "child [OPTIONS] COMMAND [ARG...]",
|
||||
}
|
||||
grandchildCmd := &cobra.Command{
|
||||
Use: "grandchild [OPTIONS] COMMAND [ARG...]",
|
||||
}
|
||||
childCmd.AddCommand(grandchildCmd)
|
||||
rootCmd.AddCommand(childCmd)
|
||||
|
||||
return rootCmd, childCmd, grandchildCmd
|
||||
}
|
||||
|
||||
func TestGetFullCommandName(t *testing.T) {
|
||||
rootCmd, childCmd, grandchildCmd := setupCobraCommands()
|
||||
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
testName string
|
||||
cmd *cobra.Command
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
testName: "rootCmd",
|
||||
cmd: rootCmd,
|
||||
expected: "root",
|
||||
},
|
||||
{
|
||||
testName: "childCmd",
|
||||
cmd: childCmd,
|
||||
expected: "root child",
|
||||
},
|
||||
{
|
||||
testName: "grandChild",
|
||||
cmd: grandchildCmd,
|
||||
expected: "root child grandchild",
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := getFullCommandName(tc.cmd)
|
||||
assert.Equal(t, actual, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCommandName(t *testing.T) {
|
||||
rootCmd, childCmd, grandchildCmd := setupCobraCommands()
|
||||
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
testName string
|
||||
cmd *cobra.Command
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
testName: "rootCmd",
|
||||
cmd: rootCmd,
|
||||
expected: "",
|
||||
},
|
||||
{
|
||||
testName: "childCmd",
|
||||
cmd: childCmd,
|
||||
expected: "child",
|
||||
},
|
||||
{
|
||||
testName: "grandchildCmd",
|
||||
cmd: grandchildCmd,
|
||||
expected: "child grandchild",
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := getCommandName(tc.cmd)
|
||||
assert.Equal(t, actual, tc.expected)
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue