@@ -3,6 +3,7 @@ package command
33import (
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+
88139func 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.
118192func 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 )
0 commit comments