@@ -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,66 @@ 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 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+
103164func stdioAttributes (streams Streams ) []attribute.KeyValue {
104165 // we don't wrap stderr, but we do wrap in/out
105166 _ , stderrTty := term .GetFdInfo (streams .Err ())
@@ -110,7 +171,9 @@ func stdioAttributes(streams Streams) []attribute.KeyValue {
110171 }
111172}
112173
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 {
114177 attrs := []attribute.KeyValue {}
115178 exitCode := 0
116179 if err != nil {
@@ -129,6 +192,27 @@ func attributesFromError(err error) []attribute.KeyValue {
129192 return attrs
130193}
131194
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+
132216// otelErrorType returns an attribute for the error type based on the error category.
133217func otelErrorType (err error ) string {
134218 name := "generic"
@@ -149,7 +233,7 @@ func (e statusError) Error() string {
149233}
150234
151235// 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.
153237// until the root command is reached.
154238//
155239// 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 {
163247}
164248
165249// 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
167251// until the root command is reached.
168252func getFullCommandName (cmd * cobra.Command ) string {
169253 if cmd .HasParent () {
0 commit comments