@@ -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,63 @@ 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 and 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 it's 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
+ }
159
+ }
160
+
103
161
func stdioAttributes (streams Streams ) []attribute.KeyValue {
104
162
// we don't wrap stderr, but we do wrap in/out
105
163
_ , stderrTty := term .GetFdInfo (streams .Err ())
@@ -110,7 +168,9 @@ func stdioAttributes(streams Streams) []attribute.KeyValue {
110
168
}
111
169
}
112
170
113
- func attributesFromError (err error ) []attribute.KeyValue {
171
+ // Used to create attributes from an error.
172
+ // The error is expected to be returned from the execution of a cobra command
173
+ func attributesFromCommandError (err error ) []attribute.KeyValue {
114
174
attrs := []attribute.KeyValue {}
115
175
exitCode := 0
116
176
if err != nil {
@@ -129,6 +189,27 @@ func attributesFromError(err error) []attribute.KeyValue {
129
189
return attrs
130
190
}
131
191
192
+ // Used to create attributes from an error.
193
+ // The error is expected to be returned from the execution of a plugin
194
+ func attributesFromPluginError (err error ) []attribute.KeyValue {
195
+ attrs := []attribute.KeyValue {}
196
+ exitCode := 0
197
+ if err != nil {
198
+ exitCode = 1
199
+ if stderr , ok := err .(statusError ); ok {
200
+ // StatusError should only be used for errors, and all errors should
201
+ // have a non-zero exit status, so only set this here if this value isn't 0
202
+ if stderr .StatusCode != 0 {
203
+ exitCode = stderr .StatusCode
204
+ }
205
+ }
206
+ attrs = append (attrs , attribute .String ("plugin.error.type" , otelErrorType (err )))
207
+ }
208
+ attrs = append (attrs , attribute .Int ("plugin.status.code" , exitCode ))
209
+
210
+ return attrs
211
+ }
212
+
132
213
// otelErrorType returns an attribute for the error type based on the error category.
133
214
func otelErrorType (err error ) string {
134
215
name := "generic"
0 commit comments