@@ -3,6 +3,7 @@ package command
33import (
44 "context"
55 "fmt"
6+ "os/exec"
67 "strings"
78 "time"
89
@@ -14,8 +15,8 @@ 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 )... )
@@ -69,7 +70,7 @@ func (cli *DockerCli) InstrumentCobraCommands(ctx context.Context, cmd *cobra.Co
6970// It should be called immediately before command execution, and returns a stopInstrumentation function
7071// that must be called with the error resulting from the command execution.
7172func (cli * DockerCli ) StartInstrumentation (cmd * cobra.Command ) (stopInstrumentation func (error )) {
72- baseAttrs := BaseCommandAttributes (cmd , cli )
73+ baseAttrs := baseCommandAttributes (cmd , cli )
7374 return startCobraCommandTimer (cli .MeterProvider (), baseAttrs )
7475}
7576
@@ -89,7 +90,7 @@ func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue)
8990 defer cancel ()
9091
9192 duration := float64 (time .Since (start )) / float64 (time .Millisecond )
92- cmdStatusAttrs := attributesFromError (err )
93+ cmdStatusAttrs := attributesFromCommandError (err )
9394 durationCounter .Add (ctx , duration ,
9495 metric .WithAttributes (attrs ... ),
9596 metric .WithAttributes (cmdStatusAttrs ... ),
@@ -100,6 +101,63 @@ func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue)
100101 }
101102}
102103
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+
103161func stdioAttributes (streams Streams ) []attribute.KeyValue {
104162 // we don't wrap stderr, but we do wrap in/out
105163 _ , stderrTty := term .GetFdInfo (streams .Err ())
@@ -110,7 +168,9 @@ func stdioAttributes(streams Streams) []attribute.KeyValue {
110168 }
111169}
112170
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 {
114174 attrs := []attribute.KeyValue {}
115175 exitCode := 0
116176 if err != nil {
@@ -129,6 +189,27 @@ func attributesFromError(err error) []attribute.KeyValue {
129189 return attrs
130190}
131191
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+
132213// otelErrorType returns an attribute for the error type based on the error category.
133214func otelErrorType (err error ) string {
134215 name := "generic"
0 commit comments