Added some telemetry utils

Signed-off-by: Christopher Petito <chrisjpetito@gmail.com>
This commit is contained in:
Christopher Petito 2024-03-28 16:22:43 +00:00
parent b8d5454963
commit 160f65d9db
2 changed files with 252 additions and 0 deletions

View File

@ -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),
)
}

View File

@ -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)
})
}
}