Skip to content

Commit 8931b01

Browse files
committed
Instrument plugin command executions
Signed-off-by: Christopher Petito <[email protected]>
1 parent 871f1b3 commit 8931b01

File tree

4 files changed

+108
-28
lines changed

4 files changed

+108
-28
lines changed

cli-plugins/manager/cobra.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,12 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
9090
cargs = append(cargs, args...)
9191
cargs = append(cargs, toComplete)
9292
os.Args = cargs
93-
runCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd)
93+
pluginRunCommand, runErr := PluginRunCommand(dockerCli, p.Name, cmd)
9494
if runErr != nil {
9595
return nil, cobra.ShellCompDirectiveError
9696
}
97-
runErr = runCommand.Run()
97+
runCommand := dockerCli.(*command.DockerCli).InstrumentPluginCommand(pluginRunCommand)
98+
runErr = runCommand.TimedRun(cmd.Context())
9899
if runErr == nil {
99100
os.Exit(0) // plugin already rendered complete data
100101
}

cli/command/telemetry_utils.go

Lines changed: 90 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package command
33
import (
44
"context"
55
"fmt"
6+
"os/exec"
67
"strings"
78
"time"
89

@@ -14,20 +15,18 @@ import (
1415
"go.opentelemetry.io/otel/metric"
1516
)
1617

17-
// BaseCommandAttributes returns an attribute.Set containing attributes to attach to metrics/traces
18-
func BaseCommandAttributes(cmd *cobra.Command, streams Streams) []attribute.KeyValue {
18+
// baseCommandAttributes returns an attribute.Set containing attributes to attach to metrics/traces
19+
func baseCommandAttributes(cmd *cobra.Command, streams Streams) []attribute.KeyValue {
1920
return append([]attribute.KeyValue{
2021
attribute.String("command.name", getCommandName(cmd)),
2122
}, stdioAttributes(streams)...)
2223
}
2324

2425
// InstrumentCobraCommands wraps all cobra commands' RunE funcs to set a command duration metric using otel.
2526
//
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(cmd *cobra.Command, mp metric.MeterProvider) {
30-
meter := getDefaultMeter(mp)
27+
// Note: this should be the last func to wrap/modify the PersistentRunE/RunE funcs
28+
// before command execution for more accurate measurements.
29+
func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command) {
3130
// If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default
3231
ogPersistentPreRunE := cmd.PersistentPreRunE
3332
if ogPersistentPreRunE == nil {
@@ -55,8 +54,8 @@ func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command, mp metric.Mete
5554
}
5655
cmd.RunE = func(cmd *cobra.Command, args []string) error {
5756
// start the timer as the first step of every cobra command
58-
baseAttrs := BaseCommandAttributes(cmd, cli)
59-
stopCobraCmdTimer := startCobraCommandTimer(cmd, meter, baseAttrs)
57+
baseAttrs := baseCommandAttributes(cmd, cli)
58+
stopCobraCmdTimer := cli.startCobraCommandTimer(cmd, baseAttrs)
6059
cmdErr := ogRunE(cmd, args)
6160
stopCobraCmdTimer(cmdErr)
6261
return cmdErr
@@ -66,9 +65,9 @@ func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command, mp metric.Mete
6665
}
6766
}
6867

69-
func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter, attrs []attribute.KeyValue) func(err error) {
68+
func (cli *DockerCli) startCobraCommandTimer(cmd *cobra.Command, attrs []attribute.KeyValue) func(err error) {
7069
ctx := cmd.Context()
71-
durationCounter, _ := meter.Float64Counter(
70+
durationCounter, _ := cli.getDefaultMeter().Float64Counter(
7271
"command.time",
7372
metric.WithDescription("Measures the duration of the cobra command"),
7473
metric.WithUnit("ms"),
@@ -77,14 +76,66 @@ func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter, attrs []attr
7776

7877
return func(err error) {
7978
duration := float64(time.Since(start)) / float64(time.Millisecond)
80-
cmdStatusAttrs := attributesFromError(err)
79+
cmdStatusAttrs := attributesFromCommandError(err)
8180
durationCounter.Add(ctx, duration,
8281
metric.WithAttributes(attrs...),
8382
metric.WithAttributes(cmdStatusAttrs...),
8483
)
8584
}
8685
}
8786

87+
// basePluginCommandAttributes returns a slice of attribute.KeyValue to attach to metrics/traces
88+
func basePluginCommandAttributes(plugincmd *exec.Cmd, streams Streams) []attribute.KeyValue {
89+
pluginPath := strings.Split(plugincmd.Path, "-")
90+
pluginName := pluginPath[len(pluginPath)-1]
91+
return append([]attribute.KeyValue{
92+
attribute.String("plugin.name", pluginName),
93+
}, stdioAttributes(streams)...)
94+
}
95+
96+
// wrappedCmd is used to wrap an exec.Cmd in order to instrument the
97+
// command with otel by using the TimedRun() func
98+
type wrappedCmd struct {
99+
*exec.Cmd
100+
101+
baseAttrs []attribute.KeyValue
102+
cli *DockerCli
103+
}
104+
105+
// TimedRun measures the duration of the command execution using and otel meter
106+
func (c *wrappedCmd) TimedRun(ctx context.Context) error {
107+
stopPluginCommandTimer := c.cli.startPluginCommandTimer(ctx, c.baseAttrs)
108+
err := c.Cmd.Run()
109+
stopPluginCommandTimer(err)
110+
return err
111+
}
112+
113+
// InstrumentPluginCommand instruments the plugin's exec.Cmd to measure it's execution time
114+
// Execute the returned command with TimedRun() to record the execution time.
115+
func (cli *DockerCli) InstrumentPluginCommand(plugincmd *exec.Cmd) *wrappedCmd {
116+
baseAttrs := basePluginCommandAttributes(plugincmd, cli)
117+
newCmd := &wrappedCmd{Cmd: plugincmd, baseAttrs: baseAttrs, cli: cli}
118+
return newCmd
119+
}
120+
121+
func (cli *DockerCli) startPluginCommandTimer(ctx context.Context, attrs []attribute.KeyValue) func(err error) {
122+
durationCounter, _ := cli.getDefaultMeter().Float64Counter(
123+
"plugin.command.time",
124+
metric.WithDescription("Measures the duration of the plugin execution"),
125+
metric.WithUnit("ms"),
126+
)
127+
start := time.Now()
128+
129+
return func(err error) {
130+
duration := float64(time.Since(start)) / float64(time.Millisecond)
131+
pluginStatusAttrs := attributesFromPluginError(err)
132+
durationCounter.Add(ctx, duration,
133+
metric.WithAttributes(attrs...),
134+
metric.WithAttributes(pluginStatusAttrs...),
135+
)
136+
}
137+
}
138+
88139
func stdioAttributes(streams Streams) []attribute.KeyValue {
89140
// we don't wrap stderr, but we do wrap in/out
90141
_, stderrTty := term.GetFdInfo(streams.Err())
@@ -95,7 +146,9 @@ func stdioAttributes(streams Streams) []attribute.KeyValue {
95146
}
96147
}
97148

98-
func attributesFromError(err error) []attribute.KeyValue {
149+
// Used to create attributes from an error.
150+
// The error is expected to be returned from the execution of a cobra command
151+
func attributesFromCommandError(err error) []attribute.KeyValue {
99152
attrs := []attribute.KeyValue{}
100153
exitCode := 0
101154
if err != nil {
@@ -114,6 +167,27 @@ func attributesFromError(err error) []attribute.KeyValue {
114167
return attrs
115168
}
116169

170+
// Used to create attributes from an error.
171+
// The error is expected to be returned from the execution of a plugin
172+
func attributesFromPluginError(err error) []attribute.KeyValue {
173+
attrs := []attribute.KeyValue{}
174+
exitCode := 0
175+
if err != nil {
176+
exitCode = 1
177+
if stderr, ok := err.(statusError); ok {
178+
// StatusError should only be used for errors, and all errors should
179+
// have a non-zero exit status, so only set this here if this value isn't 0
180+
if stderr.StatusCode != 0 {
181+
exitCode = stderr.StatusCode
182+
}
183+
}
184+
attrs = append(attrs, attribute.String("plugin.error.type", otelErrorType(err)))
185+
}
186+
attrs = append(attrs, attribute.Int("plugin.status.code", exitCode))
187+
188+
return attrs
189+
}
190+
117191
// otelErrorType returns an attribute for the error type based on the error category.
118192
func otelErrorType(err error) string {
119193
name := "generic"
@@ -158,9 +232,9 @@ func getFullCommandName(cmd *cobra.Command) string {
158232
}
159233

160234
// getDefaultMeter gets the default metric.Meter for the application
161-
// using the given metric.MeterProvider
162-
func getDefaultMeter(mp metric.MeterProvider) metric.Meter {
163-
return mp.Meter(
235+
// using the global metric.MeterProvider
236+
func (cli *DockerCli) getDefaultMeter() metric.Meter {
237+
return cli.MeterProvider().Meter(
164238
"github.com/docker/cli",
165239
metric.WithInstrumentationVersion(version.Version),
166240
)

cli/command/telemetry_utils_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ func TestAttributesFromError(t *testing.T) {
182182
tc := tc
183183
t.Run(tc.testName, func(t *testing.T) {
184184
t.Parallel()
185-
actual := attributesFromError(tc.err)
185+
actual := attributesFromCommandError(tc.err)
186186
assert.Check(t, reflect.DeepEqual(actual, tc.expected))
187187
})
188188
}

cmd/docker/docker.go

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -136,9 +136,10 @@ func setupHelpCommand(dockerCli command.Cli, rootCmd, helpCmd *cobra.Command) {
136136
helpCmd.Run = nil
137137
helpCmd.RunE = func(c *cobra.Command, args []string) error {
138138
if len(args) > 0 {
139-
helpcmd, err := pluginmanager.PluginRunCommand(dockerCli, args[0], rootCmd)
139+
helpRunCmd, err := pluginmanager.PluginRunCommand(dockerCli, args[0], rootCmd)
140140
if err == nil {
141-
return helpcmd.Run()
141+
helpcmd := dockerCli.(*command.DockerCli).InstrumentPluginCommand(helpRunCmd)
142+
return helpcmd.TimedRun(c.Context())
142143
}
143144
if !pluginmanager.IsNotFound(err) {
144145
return errors.Errorf("unknown help topic: %v", strings.Join(args, " "))
@@ -159,11 +160,12 @@ func tryRunPluginHelp(dockerCli command.Cli, ccmd *cobra.Command, cargs []string
159160
if err != nil {
160161
return err
161162
}
162-
helpcmd, err := pluginmanager.PluginRunCommand(dockerCli, cmd.Name(), root)
163+
helpRunCmd, err := pluginmanager.PluginRunCommand(dockerCli, cmd.Name(), root)
163164
if err != nil {
164165
return err
165166
}
166-
return helpcmd.Run()
167+
helpcmd := dockerCli.(*command.DockerCli).InstrumentPluginCommand(helpRunCmd)
168+
return helpcmd.TimedRun(ccmd.Context())
167169
}
168170

169171
func setHelpFunc(dockerCli command.Cli, cmd *cobra.Command) {
@@ -225,10 +227,11 @@ func setValidateArgs(dockerCli command.Cli, cmd *cobra.Command) {
225227
}
226228

227229
func tryPluginRun(dockerCli command.Cli, cmd *cobra.Command, subcommand string, envs []string) error {
228-
plugincmd, err := pluginmanager.PluginRunCommand(dockerCli, subcommand, cmd)
230+
pluginRunCmd, err := pluginmanager.PluginRunCommand(dockerCli, subcommand, cmd)
229231
if err != nil {
230232
return err
231233
}
234+
plugincmd := dockerCli.(*command.DockerCli).InstrumentPluginCommand(pluginRunCmd)
232235

233236
// Establish the plugin socket, adding it to the environment under a
234237
// well-known key if successful.
@@ -279,7 +282,7 @@ func tryPluginRun(dockerCli command.Cli, cmd *cobra.Command, subcommand string,
279282
}
280283
}()
281284

282-
if err := plugincmd.Run(); err != nil {
285+
if err := plugincmd.TimedRun(cmd.Context()); err != nil {
283286
statusCode := 1
284287
exitErr, ok := err.(*exec.ExitError)
285288
if !ok {
@@ -308,9 +311,11 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
308311
return err
309312
}
310313

311-
mp := dockerCli.MeterProvider(ctx)
314+
mp := dockerCli.MeterProvider()
312315
defer mp.(command.MeterProvider).Shutdown(ctx)
313-
dockerCli.InstrumentCobraCommands(cmd, mp)
316+
dockerCli.InstrumentCobraCommands(cmd)
317+
318+
cmd.SetContext(ctx)
314319

315320
var envs []string
316321
args, os.Args, envs, err = processAliases(dockerCli, cmd, args, os.Args)
@@ -352,7 +357,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
352357
// We've parsed global args already, so reset args to those
353358
// which remain.
354359
cmd.SetArgs(args)
355-
err = cmd.ExecuteContext(ctx)
360+
err = cmd.Execute()
356361

357362
// If the command is being executed in an interactive terminal
358363
// and hook are enabled, run the plugin hooks.

0 commit comments

Comments
 (0)