@@ -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,8 +15,8 @@ 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 )... )
@@ -69,7 +70,7 @@ func (cli *DockerCli) InstrumentCobraCommands(ctx context.Context, cmd *cobra.Co
69
70
// It should be called immediately before command execution, and returns a stopInstrumentation function
70
71
// that must be called with the error resulting from the command execution.
71
72
func (cli * DockerCli ) StartInstrumentation (cmd * cobra.Command ) (stopInstrumentation func (error )) {
72
- baseAttrs := BaseCommandAttributes (cmd , cli )
73
+ baseAttrs := baseCommandAttributes (cmd , cli )
73
74
return startCobraCommandTimer (cli .MeterProvider (), baseAttrs )
74
75
}
75
76
@@ -89,7 +90,7 @@ func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue)
89
90
defer cancel ()
90
91
91
92
duration := float64 (time .Since (start )) / float64 (time .Millisecond )
92
- cmdStatusAttrs := attributesFromError (err )
93
+ cmdStatusAttrs := attributesFromCommandError (err )
93
94
durationCounter .Add (ctx , duration ,
94
95
metric .WithAttributes (attrs ... ),
95
96
metric .WithAttributes (cmdStatusAttrs ... ),
@@ -100,6 +101,66 @@ func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue)
100
101
}
101
102
}
102
103
104
+ // basePluginCommandAttributes returns a slice of attribute.KeyValue to attach to metrics/traces
105
+ func basePluginCommandAttributes (plugincmd * exec.Cmd , streams Streams ) []attribute.KeyValue {
106
+ pluginPath := strings .Split (plugincmd .Path , "-" )
107
+ pluginName := pluginPath [len (pluginPath )- 1 ]
108
+ return append ([]attribute.KeyValue {
109
+ attribute .String ("plugin.name" , pluginName ),
110
+ }, stdioAttributes (streams )... )
111
+ }
112
+
113
+ // wrappedCmd is used to wrap an exec.Cmd in order to instrument the
114
+ // command with otel by using the TimedRun() func
115
+ type wrappedCmd struct {
116
+ * exec.Cmd
117
+
118
+ baseAttrs []attribute.KeyValue
119
+ cli * DockerCli
120
+ }
121
+
122
+ // TimedRun measures the duration of the command execution using an otel meter
123
+ func (c * wrappedCmd ) TimedRun (ctx context.Context ) error {
124
+ stopPluginCommandTimer := c .cli .startPluginCommandTimer (c .cli .MeterProvider (), c .baseAttrs )
125
+ err := c .Cmd .Run ()
126
+ stopPluginCommandTimer (err )
127
+ return err
128
+ }
129
+
130
+ // InstrumentPluginCommand instruments the plugin's exec.Cmd to measure its execution time
131
+ // Execute the returned command with TimedRun() to record the execution time.
132
+ func (cli * DockerCli ) InstrumentPluginCommand (plugincmd * exec.Cmd ) * wrappedCmd {
133
+ baseAttrs := basePluginCommandAttributes (plugincmd , cli )
134
+ newCmd := & wrappedCmd {Cmd : plugincmd , baseAttrs : baseAttrs , cli : cli }
135
+ return newCmd
136
+ }
137
+
138
+ func (cli * DockerCli ) startPluginCommandTimer (mp metric.MeterProvider , attrs []attribute.KeyValue ) func (err error ) {
139
+ durationCounter , _ := getDefaultMeter (mp ).Float64Counter (
140
+ "plugin.command.time" ,
141
+ metric .WithDescription ("Measures the duration of the plugin execution" ),
142
+ metric .WithUnit ("ms" ),
143
+ )
144
+ start := time .Now ()
145
+
146
+ return func (err error ) {
147
+ // Use a new context for the export so that the command being cancelled
148
+ // doesn't affect the metrics, and we get metrics for cancelled commands.
149
+ ctx , cancel := context .WithTimeout (context .Background (), exportTimeout )
150
+ defer cancel ()
151
+
152
+ duration := float64 (time .Since (start )) / float64 (time .Millisecond )
153
+ pluginStatusAttrs := attributesFromPluginError (err )
154
+ durationCounter .Add (ctx , duration ,
155
+ metric .WithAttributes (attrs ... ),
156
+ metric .WithAttributes (pluginStatusAttrs ... ),
157
+ )
158
+ if mp , ok := mp .(MeterProvider ); ok {
159
+ mp .ForceFlush (ctx )
160
+ }
161
+ }
162
+ }
163
+
103
164
func stdioAttributes (streams Streams ) []attribute.KeyValue {
104
165
// we don't wrap stderr, but we do wrap in/out
105
166
_ , stderrTty := term .GetFdInfo (streams .Err ())
@@ -110,7 +171,9 @@ func stdioAttributes(streams Streams) []attribute.KeyValue {
110
171
}
111
172
}
112
173
113
- func attributesFromError (err error ) []attribute.KeyValue {
174
+ // Used to create attributes from an error.
175
+ // The error is expected to be returned from the execution of a cobra command
176
+ func attributesFromCommandError (err error ) []attribute.KeyValue {
114
177
attrs := []attribute.KeyValue {}
115
178
exitCode := 0
116
179
if err != nil {
@@ -129,6 +192,27 @@ func attributesFromError(err error) []attribute.KeyValue {
129
192
return attrs
130
193
}
131
194
195
+ // Used to create attributes from an error.
196
+ // The error is expected to be returned from the execution of a plugin
197
+ func attributesFromPluginError (err error ) []attribute.KeyValue {
198
+ attrs := []attribute.KeyValue {}
199
+ exitCode := 0
200
+ if err != nil {
201
+ exitCode = 1
202
+ if stderr , ok := err .(statusError ); ok {
203
+ // StatusError should only be used for errors, and all errors should
204
+ // have a non-zero exit status, so only set this here if this value isn't 0
205
+ if stderr .StatusCode != 0 {
206
+ exitCode = stderr .StatusCode
207
+ }
208
+ }
209
+ attrs = append (attrs , attribute .String ("plugin.error.type" , otelErrorType (err )))
210
+ }
211
+ attrs = append (attrs , attribute .Int ("plugin.status.code" , exitCode ))
212
+
213
+ return attrs
214
+ }
215
+
132
216
// otelErrorType returns an attribute for the error type based on the error category.
133
217
func otelErrorType (err error ) string {
134
218
name := "generic"
@@ -149,7 +233,7 @@ func (e statusError) Error() string {
149
233
}
150
234
151
235
// getCommandName gets the cobra command name in the format
152
- // `... parentCommandName commandName` by traversing it's parent commands recursively.
236
+ // `... parentCommandName commandName` by traversing its parent commands recursively.
153
237
// until the root command is reached.
154
238
//
155
239
// Note: The root command's name is excluded. If cmd is the root cmd, return ""
@@ -163,7 +247,7 @@ func getCommandName(cmd *cobra.Command) string {
163
247
}
164
248
165
249
// getFullCommandName gets the full cobra command name in the format
166
- // `... parentCommandName commandName` by traversing it's parent commands recursively
250
+ // `... parentCommandName commandName` by traversing its parent commands recursively
167
251
// until the root command is reached.
168
252
func getFullCommandName (cmd * cobra.Command ) string {
169
253
if cmd .HasParent () {
0 commit comments