@@ -3,6 +3,7 @@ package command
3
3
import (
4
4
"context"
5
5
"fmt"
6
+ "os/exec"
6
7
"strings"
7
8
"time"
8
9
@@ -14,20 +15,18 @@ import (
14
15
"go.opentelemetry.io/otel/metric"
15
16
)
16
17
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 {
19
20
return append ([]attribute.KeyValue {
20
21
attribute .String ("command.name" , getCommandName (cmd )),
21
22
}, stdioAttributes (streams )... )
22
23
}
23
24
24
25
// InstrumentCobraCommands wraps all cobra commands' RunE funcs to set a command duration metric using otel.
25
26
//
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 ) {
31
30
// If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default
32
31
ogPersistentPreRunE := cmd .PersistentPreRunE
33
32
if ogPersistentPreRunE == nil {
@@ -55,8 +54,8 @@ func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command, mp metric.Mete
55
54
}
56
55
cmd .RunE = func (cmd * cobra.Command , args []string ) error {
57
56
// 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 )
60
59
cmdErr := ogRunE (cmd , args )
61
60
stopCobraCmdTimer (cmdErr )
62
61
return cmdErr
@@ -66,9 +65,9 @@ func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command, mp metric.Mete
66
65
}
67
66
}
68
67
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 ) {
70
69
ctx := cmd .Context ()
71
- durationCounter , _ := meter .Float64Counter (
70
+ durationCounter , _ := cli . getDefaultMeter () .Float64Counter (
72
71
"command.time" ,
73
72
metric .WithDescription ("Measures the duration of the cobra command" ),
74
73
metric .WithUnit ("ms" ),
@@ -77,14 +76,66 @@ func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter, attrs []attr
77
76
78
77
return func (err error ) {
79
78
duration := float64 (time .Since (start )) / float64 (time .Millisecond )
80
- cmdStatusAttrs := attributesFromError (err )
79
+ cmdStatusAttrs := attributesFromCommandError (err )
81
80
durationCounter .Add (ctx , duration ,
82
81
metric .WithAttributes (attrs ... ),
83
82
metric .WithAttributes (cmdStatusAttrs ... ),
84
83
)
85
84
}
86
85
}
87
86
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
+
88
139
func stdioAttributes (streams Streams ) []attribute.KeyValue {
89
140
// we don't wrap stderr, but we do wrap in/out
90
141
_ , stderrTty := term .GetFdInfo (streams .Err ())
@@ -95,7 +146,9 @@ func stdioAttributes(streams Streams) []attribute.KeyValue {
95
146
}
96
147
}
97
148
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 {
99
152
attrs := []attribute.KeyValue {}
100
153
exitCode := 0
101
154
if err != nil {
@@ -114,6 +167,27 @@ func attributesFromError(err error) []attribute.KeyValue {
114
167
return attrs
115
168
}
116
169
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
+
117
191
// otelErrorType returns an attribute for the error type based on the error category.
118
192
func otelErrorType (err error ) string {
119
193
name := "generic"
@@ -158,9 +232,9 @@ func getFullCommandName(cmd *cobra.Command) string {
158
232
}
159
233
160
234
// 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 (
164
238
"github.com/docker/cli" ,
165
239
metric .WithInstrumentationVersion (version .Version ),
166
240
)
0 commit comments