Skip to content
This repository was archived by the owner on Nov 27, 2023. It is now read-only.

Commit 01ea248

Browse files
authored
Merge pull request #630 from docker/cli_metrics_failures
Cli metrics failures
2 parents 57c14e7 + 6f19bbf commit 01ea248

File tree

16 files changed

+134
-51
lines changed

16 files changed

+134
-51
lines changed

aci/context.go

+3-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/pkg/errors"
2929

3030
"github.com/docker/compose-cli/context/store"
31+
"github.com/docker/compose-cli/errdefs"
3132
"github.com/docker/compose-cli/prompt"
3233
)
3334

@@ -40,7 +41,7 @@ type ContextParams struct {
4041
}
4142

4243
// ErrSubscriptionNotFound is returned when a required subscription is not found
43-
var ErrSubscriptionNotFound = errors.New("subscription not found")
44+
var ErrSubscriptionNotFound = errors.Wrapf(errdefs.ErrNotFound, "subscription")
4445

4546
// IsSubscriptionNotFoundError returns true if the unwrapped error is IsSubscriptionNotFoundError
4647
func IsSubscriptionNotFoundError(err error) bool {
@@ -138,7 +139,7 @@ func (helper contextCreateACIHelper) chooseGroup(ctx context.Context, subscripti
138139
group, err := helper.selector.Select("Select a resource group", groupNames)
139140
if err != nil {
140141
if err == terminal.InterruptErr {
141-
os.Exit(0)
142+
return resources.Group{}, errdefs.ErrCanceled
142143
}
143144

144145
return resources.Group{}, err

cli/cmd/context/create.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ $ docker context create my-context --description "some description" --docker "ho
7070
Use: "create CONTEXT",
7171
Short: "Create new context",
7272
RunE: func(cmd *cobra.Command, args []string) error {
73-
mobycli.Exec()
73+
mobycli.Exec(cmd.Root())
7474
return nil
7575
},
7676
Long: longHelp,

cli/cmd/context/inspect.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ func inspectCommand() *cobra.Command {
2727
Use: "inspect",
2828
Short: "Display detailed information on one or more contexts",
2929
RunE: func(cmd *cobra.Command, args []string) error {
30-
mobycli.Exec()
30+
mobycli.Exec(cmd.Root())
3131
return nil
3232
},
3333
}

cli/cmd/context/ls.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ func runList(cmd *cobra.Command, opts lsOpts) error {
6969
return err
7070
}
7171
if opts.format != "" {
72-
mobycli.Exec()
72+
mobycli.Exec(cmd.Root())
7373
return nil
7474
}
7575

cli/cmd/login/login.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ func runLogin(cmd *cobra.Command, args []string) error {
5656
backend := args[0]
5757
return errors.New("unknown backend type for cloud login: " + backend)
5858
}
59-
mobycli.Exec()
59+
mobycli.Exec(cmd.Root())
6060
return nil
6161
}
6262

cli/cmd/logout/logout.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,6 @@ func Command() *cobra.Command {
3737
}
3838

3939
func runLogout(cmd *cobra.Command, args []string) error {
40-
mobycli.Exec()
40+
mobycli.Exec(cmd.Root())
4141
return nil
4242
}

cli/cmd/version.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ func runVersion(cmd *cobra.Command, version string) error {
5151
// we don't want to fail on error, there is an error if the engine is not available but it displays client version info
5252
// Still, technically the [] byte versionResult could be nil, just let the original command display what it has to display
5353
if versionResult == nil {
54-
mobycli.Exec()
54+
mobycli.Exec(cmd.Root())
5555
return nil
5656
}
5757
var s string = string(versionResult)

cli/main.go

+18-15
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ func main() {
102102
SilenceUsage: true,
103103
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
104104
if !isContextAgnosticCommand(cmd) {
105-
mobycli.ExecIfDefaultCtxType(cmd.Context())
105+
mobycli.ExecIfDefaultCtxType(cmd.Context(), cmd.Root())
106106
}
107107
return nil
108108
},
@@ -136,7 +136,7 @@ func main() {
136136
helpFunc := root.HelpFunc()
137137
root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
138138
if !isContextAgnosticCommand(cmd) {
139-
mobycli.ExecIfDefaultCtxType(cmd.Context())
139+
mobycli.ExecIfDefaultCtxType(cmd.Context(), cmd.Root())
140140
}
141141
helpFunc(cmd, args)
142142
})
@@ -158,7 +158,7 @@ func main() {
158158

159159
// --host and --version should immediately be forwarded to the original cli
160160
if opts.Host != "" || opts.Version {
161-
mobycli.Exec()
161+
mobycli.Exec(root)
162162
}
163163

164164
if opts.Config == "" {
@@ -171,7 +171,7 @@ func main() {
171171

172172
s, err := store.New(configDir)
173173
if err != nil {
174-
mobycli.Exec()
174+
mobycli.Exec(root)
175175
}
176176

177177
ctype := store.DefaultContextType
@@ -185,41 +185,43 @@ func main() {
185185
root.AddCommand(volume.ACICommand())
186186
}
187187

188-
metrics.Track(ctype, os.Args[1:], root.PersistentFlags())
189-
190188
ctx = apicontext.WithCurrentContext(ctx, currentContext)
191189
ctx = store.WithContextStore(ctx, s)
192190

193191
if err = root.ExecuteContext(ctx); err != nil {
194192
// if user canceled request, simply exit without any error message
195-
if errors.Is(ctx.Err(), context.Canceled) {
193+
if errdefs.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
194+
metrics.Track(ctype, os.Args[1:], root.PersistentFlags(), metrics.CanceledStatus)
196195
os.Exit(130)
197196
}
198197
if ctype == store.AwsContextType {
199198
exit(root, currentContext, errors.Errorf(`%q context type has been renamed. Recreate the context by running:
200-
$ docker context create %s <name>`, cc.Type(), store.EcsContextType))
199+
$ docker context create %s <name>`, cc.Type(), store.EcsContextType), ctype)
201200
}
202201

203202
// Context should always be handled by new CLI
204203
requiredCmd, _, _ := root.Find(os.Args[1:])
205204
if requiredCmd != nil && isContextAgnosticCommand(requiredCmd) {
206-
exit(root, currentContext, err)
205+
exit(root, currentContext, err, ctype)
207206
}
208-
mobycli.ExecIfDefaultCtxType(ctx)
207+
mobycli.ExecIfDefaultCtxType(ctx, root)
209208

210-
checkIfUnknownCommandExistInDefaultContext(err, currentContext)
209+
checkIfUnknownCommandExistInDefaultContext(err, currentContext, root)
211210

212-
exit(root, currentContext, err)
211+
exit(root, currentContext, err, ctype)
213212
}
213+
metrics.Track(ctype, os.Args[1:], root.PersistentFlags(), metrics.SuccessStatus)
214214
}
215215

216-
func exit(cmd *cobra.Command, ctx string, err error) {
216+
func exit(root *cobra.Command, ctx string, err error, ctype string) {
217+
metrics.Track(ctype, os.Args[1:], root.PersistentFlags(), metrics.FailureStatus)
218+
217219
if errors.Is(err, errdefs.ErrLoginRequired) {
218220
fmt.Fprintln(os.Stderr, err)
219221
os.Exit(errdefs.ExitCodeLoginRequired)
220222
}
221223
if errors.Is(err, errdefs.ErrNotImplemented) {
222-
cmd, _, _ := cmd.Traverse(os.Args[1:])
224+
cmd, _, _ := root.Traverse(os.Args[1:])
223225
name := cmd.Name()
224226
parent := cmd.Parent()
225227
if parent != nil && parent.Parent() != nil {
@@ -237,13 +239,14 @@ func fatal(err error) {
237239
os.Exit(1)
238240
}
239241

240-
func checkIfUnknownCommandExistInDefaultContext(err error, currentContext string) {
242+
func checkIfUnknownCommandExistInDefaultContext(err error, currentContext string, root *cobra.Command) {
241243
submatch := unknownCommandRegexp.FindSubmatch([]byte(err.Error()))
242244
if len(submatch) == 2 {
243245
dockerCommand := string(submatch[1])
244246

245247
if mobycli.IsDefaultContextCommand(dockerCommand) {
246248
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)
249+
metrics.Track(currentContext, os.Args[1:], root.PersistentFlags(), metrics.FailureStatus)
247250
os.Exit(1)
248251
}
249252
}

cli/mobycli/exec.go

+11-4
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,28 @@ import (
2424
"os/signal"
2525
"strings"
2626

27+
"github.com/spf13/cobra"
28+
2729
apicontext "github.com/docker/compose-cli/context"
2830
"github.com/docker/compose-cli/context/store"
31+
"github.com/docker/compose-cli/metrics"
2932
)
3033

3134
var delegatedContextTypes = []string{store.DefaultContextType}
3235

3336
// ComDockerCli name of the classic cli binary
3437
const ComDockerCli = "com.docker.cli"
3538

36-
// ExecIfDefaultCtxType delegates to com.docker.cli if on moby or AWS context (until there is an AWS backend)
37-
func ExecIfDefaultCtxType(ctx context.Context) {
39+
// ExecIfDefaultCtxType delegates to com.docker.cli if on moby context
40+
func ExecIfDefaultCtxType(ctx context.Context, root *cobra.Command) {
3841
currentContext := apicontext.CurrentContext(ctx)
3942

4043
s := store.ContextStore(ctx)
4144

4245
currentCtx, err := s.Get(currentContext)
4346
// Only run original docker command if the current context is not ours.
4447
if err != nil || mustDelegateToMoby(currentCtx.Type()) {
45-
Exec()
48+
Exec(root)
4649
}
4750
}
4851

@@ -56,7 +59,7 @@ func mustDelegateToMoby(ctxType string) bool {
5659
}
5760

5861
// Exec delegates to com.docker.cli if on moby context
59-
func Exec() {
62+
func Exec(root *cobra.Command) {
6063
cmd := exec.Command(ComDockerCli, os.Args[1:]...)
6164
cmd.Stdin = os.Stdin
6265
cmd.Stdout = os.Stdout
@@ -83,12 +86,16 @@ func Exec() {
8386
err := cmd.Run()
8487
childExit <- true
8588
if err != nil {
89+
metrics.Track(store.DefaultContextName, os.Args[1:], root.PersistentFlags(), metrics.FailureStatus)
90+
8691
if exiterr, ok := err.(*exec.ExitError); ok {
8792
os.Exit(exiterr.ExitCode())
8893
}
8994
fmt.Fprintln(os.Stderr, err)
9095
os.Exit(1)
9196
}
97+
metrics.Track(store.DefaultContextName, os.Args[1:], root.PersistentFlags(), metrics.SuccessStatus)
98+
9299
os.Exit(0)
93100
}
94101

ecs/context.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import (
3030
"gopkg.in/ini.v1"
3131

3232
"github.com/docker/compose-cli/context/store"
33+
"github.com/docker/compose-cli/errdefs"
3334
"github.com/docker/compose-cli/prompt"
3435
)
3536

@@ -155,7 +156,7 @@ func (h contextCreateAWSHelper) chooseProfile(section map[string]ini.Section) (s
155156
selected, err := h.user.Select("Select AWS Profile", profiles)
156157
if err != nil {
157158
if err == terminal.InterruptErr {
158-
os.Exit(-1)
159+
return "", errdefs.ErrCanceled
159160
}
160161
return "", err
161162
}

errdefs/errors.go

+7
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ var (
4242
// ErrNotImplemented is returned when a backend doesn't implement
4343
// an action
4444
ErrNotImplemented = errors.New("not implemented")
45+
// ErrCanceled is returned when the command was canceled by user
46+
ErrCanceled = errors.New("canceled")
4547
// ErrParsingFailed is returned when a string cannot be parsed
4648
ErrParsingFailed = errors.New("parsing failed")
4749
// ErrWrongContextType is returned when the caller tries to get a context
@@ -78,3 +80,8 @@ func IsErrNotImplemented(err error) bool {
7880
func IsErrParsingFailed(err error) bool {
7981
return errors.Is(err, ErrParsingFailed)
8082
}
83+
84+
// IsErrCanceled returns true if the unwrapped error is ErrCanceled
85+
func IsErrCanceled(err error) bool {
86+
return errors.Is(err, ErrCanceled)
87+
}

metrics/client.go

+23-16
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"encoding/json"
2323
"net"
2424
"net/http"
25+
"time"
2526
)
2627

2728
type client struct {
@@ -33,13 +34,20 @@ type Command struct {
3334
Command string `json:"command"`
3435
Context string `json:"context"`
3536
Source string `json:"source"`
37+
Status string `json:"status"`
3638
}
3739

3840
const (
3941
// CLISource is sent for cli metrics
4042
CLISource = "cli"
4143
// APISource is sent for API metrics
4244
APISource = "api"
45+
// SuccessStatus is sent for API metrics
46+
SuccessStatus = "success"
47+
// FailureStatus is sent for API metrics
48+
FailureStatus = "failure"
49+
// CanceledStatus is sent for API metrics
50+
CanceledStatus = "canceled"
4351
)
4452

4553
// Client sends metrics to Docker Desktopn
@@ -64,24 +72,23 @@ func NewClient() Client {
6472
}
6573

6674
func (c *client) Send(command Command) {
67-
wasIn := make(chan bool)
68-
69-
// Fire and forget, we don't want to slow down the user waiting for DD
70-
// metrics endpoint to respond. We could lose some events but that's ok.
75+
result := make(chan bool, 1)
7176
go func() {
72-
defer func() {
73-
_ = recover()
74-
}()
75-
76-
wasIn <- true
77+
postMetrics(command, c)
78+
result <- true
79+
}()
7780

78-
req, err := json.Marshal(command)
79-
if err != nil {
80-
return
81-
}
81+
// wait for the post finished, or timeout in case anything freezes.
82+
// Posting metrics without Desktop listening returns in less than a ms, and a handful of ms (often <2ms) when Desktop is listening
83+
select {
84+
case <-result:
85+
case <-time.After(50 * time.Millisecond):
86+
}
87+
}
8288

89+
func postMetrics(command Command, c *client) {
90+
req, err := json.Marshal(command)
91+
if err == nil {
8392
_, _ = c.httpClient.Post("http://localhost/usage", "application/json", bytes.NewBuffer(req))
84-
}()
85-
<-wasIn
86-
93+
}
8794
}

metrics/metrics.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -77,14 +77,15 @@ const (
7777
)
7878

7979
// Track sends the tracking analytics to Docker Desktop
80-
func Track(context string, args []string, flags *flag.FlagSet) {
80+
func Track(context string, args []string, flags *flag.FlagSet, status string) {
8181
command := getCommand(args, flags)
8282
if command != "" {
8383
c := NewClient()
8484
c.Send(Command{
8585
Command: command,
8686
Context: context,
8787
Source: CLISource,
88+
Status: status,
8889
})
8990
}
9091
}

server/metrics.go

+9-5
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,7 @@ var (
4141
}
4242
)
4343

44-
func metricsServerInterceptor(clictx context.Context) grpc.UnaryServerInterceptor {
45-
client := metrics.NewClient()
46-
44+
func metricsServerInterceptor(clictx context.Context, client metrics.Client) grpc.UnaryServerInterceptor {
4745
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
4846
currentContext, err := getIncomingContext(ctx)
4947
if err != nil {
@@ -53,15 +51,21 @@ func metricsServerInterceptor(clictx context.Context) grpc.UnaryServerIntercepto
5351
}
5452
}
5553

54+
data, err := handler(ctx, req)
55+
56+
status := metrics.SuccessStatus
57+
if err != nil {
58+
status = metrics.FailureStatus
59+
}
5660
command := methodMapping[info.FullMethod]
5761
if command != "" {
5862
client.Send(metrics.Command{
5963
Command: command,
6064
Context: currentContext,
6165
Source: metrics.APISource,
66+
Status: status,
6267
})
6368
}
64-
65-
return handler(ctx, req)
69+
return data, err
6670
}
6771
}

0 commit comments

Comments
 (0)