Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit e7d8d05

Browse files
committed
metrics: initial logic for command execution duration
Add a timer around command invocations to be reported with metrics. This isn't actually sent anywhere currently, as it's meant for evented data which is forthcoming. (We could report it with the current events, but it's not clear that there's any value in doing so.) The signature for `Track()` has been changed to take an object with all the fields. This is both for sanity to keep the method from getting ridiculously long, and to make it easier to unify the usage (heartbeat) and event code paths by ensuring we have all the data for both. Signed-off-by: Milas Bowman <[email protected]>
1 parent 16482c0 commit e7d8d05

File tree

5 files changed

+113
-26
lines changed

5 files changed

+113
-26
lines changed

cli/main.go

Lines changed: 66 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -253,10 +253,20 @@ func main() {
253253

254254
root.AddCommand(command)
255255

256-
if err = root.ExecuteContext(ctx); err != nil {
257-
handleError(ctx, err, ctype, currentContext, cc, root)
256+
start := time.Now().UTC()
257+
err = root.ExecuteContext(ctx)
258+
duration := time.Since(start)
259+
if err != nil {
260+
handleError(ctx, err, ctype, currentContext, cc, root, start, duration)
258261
}
259-
metricsClient.Track(ctype, os.Args[1:], metrics.SuccessStatus)
262+
metricsClient.Track(
263+
metrics.CmdMeta{
264+
ContextType: ctype,
265+
Args: os.Args[1:],
266+
Status: metrics.SuccessStatus,
267+
Start: start,
268+
Duration: duration,
269+
})
260270
}
261271

262272
func customizeCliForACI(command *cobra.Command, proxy *api.ServiceProxy) {
@@ -275,33 +285,64 @@ func customizeCliForACI(command *cobra.Command, proxy *api.ServiceProxy) {
275285
}
276286
}
277287

278-
func handleError(ctx context.Context, err error, ctype string, currentContext string, cc *store.DockerContext, root *cobra.Command) {
288+
func handleError(
289+
ctx context.Context,
290+
err error,
291+
ctype string,
292+
currentContext string,
293+
cc *store.DockerContext,
294+
root *cobra.Command,
295+
start time.Time,
296+
duration time.Duration,
297+
) {
279298
// if user canceled request, simply exit without any error message
280299
if api.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
281-
metricsClient.Track(ctype, os.Args[1:], metrics.CanceledStatus)
300+
metricsClient.Track(
301+
metrics.CmdMeta{
302+
ContextType: ctype,
303+
Args: os.Args[1:],
304+
Status: metrics.CanceledStatus,
305+
Start: start,
306+
Duration: duration,
307+
},
308+
)
282309
os.Exit(130)
283310
}
284311
if ctype == store.AwsContextType {
285-
exit(currentContext, errors.Errorf(`%q context type has been renamed. Recreate the context by running:
286-
$ docker context create %s <name>`, cc.Type(), store.EcsContextType), ctype)
312+
exit(
313+
currentContext,
314+
errors.Errorf(`%q context type has been renamed. Recreate the context by running:
315+
$ docker context create %s <name>`, cc.Type(), store.EcsContextType),
316+
ctype,
317+
start,
318+
duration,
319+
)
287320
}
288321

289322
// Context should always be handled by new CLI
290323
requiredCmd, _, _ := root.Find(os.Args[1:])
291324
if requiredCmd != nil && isContextAgnosticCommand(requiredCmd) {
292-
exit(currentContext, err, ctype)
325+
exit(currentContext, err, ctype, start, duration)
293326
}
294327
mobycli.ExecIfDefaultCtxType(ctx, root)
295328

296329
checkIfUnknownCommandExistInDefaultContext(err, currentContext, ctype)
297330

298-
exit(currentContext, err, ctype)
331+
exit(currentContext, err, ctype, start, duration)
299332
}
300333

301-
func exit(ctx string, err error, ctype string) {
334+
func exit(ctx string, err error, ctype string, start time.Time, duration time.Duration) {
302335
if exit, ok := err.(cli.StatusError); ok {
303336
// TODO(milas): shouldn't this use the exit code to determine status?
304-
metricsClient.Track(ctype, os.Args[1:], metrics.SuccessStatus)
337+
metricsClient.Track(
338+
metrics.CmdMeta{
339+
ContextType: ctype,
340+
Args: os.Args[1:],
341+
Status: metrics.SuccessStatus,
342+
Start: start,
343+
Duration: duration,
344+
},
345+
)
305346
os.Exit(exit.StatusCode)
306347
}
307348

@@ -316,7 +357,15 @@ func exit(ctx string, err error, ctype string) {
316357
metricsStatus = metrics.CommandSyntaxFailure.MetricsStatus
317358
exitCode = metrics.CommandSyntaxFailure.ExitCode
318359
}
319-
metricsClient.Track(ctype, os.Args[1:], metricsStatus)
360+
metricsClient.Track(
361+
metrics.CmdMeta{
362+
ContextType: ctype,
363+
Args: os.Args[1:],
364+
Status: metricsStatus,
365+
Start: start,
366+
Duration: duration,
367+
},
368+
)
320369

321370
if errors.Is(err, api.ErrLoginRequired) {
322371
fmt.Fprintln(os.Stderr, err)
@@ -351,7 +400,11 @@ func checkIfUnknownCommandExistInDefaultContext(err error, currentContext string
351400

352401
if mobycli.IsDefaultContextCommand(dockerCommand) {
353402
fmt.Fprintf(os.Stderr, "Command %q not available in current context (%s), you can use the \"default\" context to run this command\n", dockerCommand, currentContext)
354-
metricsClient.Track(contextType, os.Args[1:], metrics.FailureStatus)
403+
metricsClient.Track(metrics.CmdMeta{
404+
ContextType: contextType,
405+
Args: os.Args[1:],
406+
Status: metrics.FailureStatus,
407+
})
355408
os.Exit(1)
356409
}
357410
}

cli/metrics/client.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ import (
2626
// specified file path.
2727
const EnvVarDebugMetricsPath = "DOCKER_METRICS_DEBUG_LOG"
2828

29+
type CmdMeta struct {
30+
ContextType string
31+
Args []string
32+
Status string
33+
ExitCode int
34+
Start time.Time
35+
Duration time.Duration
36+
}
37+
2938
type client struct {
3039
cliversion *cliversion
3140
reporter Reporter
@@ -62,7 +71,7 @@ type Client interface {
6271
// Note that metric collection is best-effort, so any errors are ignored.
6372
SendUsage(Command)
6473
// Track creates an event for a command execution and reports it.
65-
Track(context string, args []string, status string)
74+
Track(cmd CmdMeta)
6675
}
6776

6877
// NewClient returns a new metrics client that will send metrics using the

cli/metrics/metrics.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,17 @@ import (
2525
"github.com/docker/compose-cli/cli/metrics/metadata"
2626
)
2727

28-
func (c *client) Track(context string, args []string, status string) {
28+
func (c *client) Track(cmd CmdMeta) {
2929
if isInvokedAsCliBackend() {
3030
return
3131
}
32-
command := GetCommand(args)
32+
command := GetCommand(cmd.Args)
3333
if command != "" {
3434
c.SendUsage(Command{
3535
Command: command,
36-
Context: context,
37-
Source: c.getMetadata(CLISource, args),
38-
Status: status,
36+
Context: cmd.ContextType,
37+
Source: c.getMetadata(CLISource, cmd.Args),
38+
Status: cmd.Status,
3939
})
4040
}
4141
}

cli/mobycli/exec.go

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"regexp"
2828
"runtime"
2929
"strings"
30+
"time"
3031

3132
"github.com/google/shlex"
3233
"github.com/spf13/cobra"
@@ -76,20 +77,35 @@ func Exec(_ *cobra.Command) {
7677
metricsClient.WithCliVersionFunc(func() string {
7778
return CliVersion()
7879
})
80+
start := time.Now().UTC()
7981
childExit := make(chan bool)
8082
err := RunDocker(childExit, os.Args[1:]...)
8183
childExit <- true
84+
duration := time.Since(start)
8285
if err != nil {
8386
if exiterr, ok := err.(*exec.ExitError); ok {
8487
exitCode := exiterr.ExitCode()
8588
metricsClient.Track(
86-
store.DefaultContextType,
87-
os.Args[1:],
88-
metrics.FailureCategoryFromExitCode(exitCode).MetricsStatus,
89+
metrics.CmdMeta{
90+
ContextType: store.DefaultContextType,
91+
Args: os.Args[1:],
92+
Status: metrics.FailureCategoryFromExitCode(exitCode).MetricsStatus,
93+
ExitCode: exitCode,
94+
Start: start,
95+
Duration: duration,
96+
},
8997
)
9098
os.Exit(exitCode)
9199
}
92-
metricsClient.Track(store.DefaultContextType, os.Args[1:], metrics.FailureStatus)
100+
metricsClient.Track(
101+
metrics.CmdMeta{
102+
ContextType: store.DefaultContextType,
103+
Args: os.Args[1:],
104+
Status: metrics.FailureStatus,
105+
Start: start,
106+
Duration: duration,
107+
},
108+
)
93109
fmt.Fprintln(os.Stderr, err)
94110
os.Exit(1)
95111
}
@@ -98,7 +114,16 @@ func Exec(_ *cobra.Command) {
98114
if command == "login" && !metrics.HasQuietFlag(commandArgs) {
99115
displayPATSuggestMsg(commandArgs)
100116
}
101-
metricsClient.Track(store.DefaultContextType, os.Args[1:], metrics.SuccessStatus)
117+
metricsClient.Track(
118+
metrics.CmdMeta{
119+
ContextType: store.DefaultContextType,
120+
Args: os.Args[1:],
121+
Status: metrics.SuccessStatus,
122+
ExitCode: 0,
123+
Start: start,
124+
Duration: duration,
125+
},
126+
)
102127

103128
os.Exit(0)
104129
}

cli/server/metrics_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,6 @@ func (s *mockMetricsClient) SendUsage(command metrics.Command) {
130130
s.Called(command)
131131
}
132132

133-
func (s *mockMetricsClient) Track(context string, args []string, status string) {
134-
s.Called(context, args, status)
133+
func (s *mockMetricsClient) Track(cmd metrics.CmdMeta) {
134+
s.Called(cmd)
135135
}

0 commit comments

Comments
 (0)