mirror of
https://github.com/docker/compose.git
synced 2025-07-06 21:34:25 +02:00
Merge pull request #630 from docker/cli_metrics_failures
Cli metrics failures
This commit is contained in:
commit
01ea2488a2
@ -28,6 +28,7 @@ import (
|
|||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
|
||||||
"github.com/docker/compose-cli/context/store"
|
"github.com/docker/compose-cli/context/store"
|
||||||
|
"github.com/docker/compose-cli/errdefs"
|
||||||
"github.com/docker/compose-cli/prompt"
|
"github.com/docker/compose-cli/prompt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -40,7 +41,7 @@ type ContextParams struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ErrSubscriptionNotFound is returned when a required subscription is not found
|
// ErrSubscriptionNotFound is returned when a required subscription is not found
|
||||||
var ErrSubscriptionNotFound = errors.New("subscription not found")
|
var ErrSubscriptionNotFound = errors.Wrapf(errdefs.ErrNotFound, "subscription")
|
||||||
|
|
||||||
// IsSubscriptionNotFoundError returns true if the unwrapped error is IsSubscriptionNotFoundError
|
// IsSubscriptionNotFoundError returns true if the unwrapped error is IsSubscriptionNotFoundError
|
||||||
func IsSubscriptionNotFoundError(err error) bool {
|
func IsSubscriptionNotFoundError(err error) bool {
|
||||||
@ -138,7 +139,7 @@ func (helper contextCreateACIHelper) chooseGroup(ctx context.Context, subscripti
|
|||||||
group, err := helper.selector.Select("Select a resource group", groupNames)
|
group, err := helper.selector.Select("Select a resource group", groupNames)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == terminal.InterruptErr {
|
if err == terminal.InterruptErr {
|
||||||
os.Exit(0)
|
return resources.Group{}, errdefs.ErrCanceled
|
||||||
}
|
}
|
||||||
|
|
||||||
return resources.Group{}, err
|
return resources.Group{}, err
|
||||||
|
@ -70,7 +70,7 @@ $ docker context create my-context --description "some description" --docker "ho
|
|||||||
Use: "create CONTEXT",
|
Use: "create CONTEXT",
|
||||||
Short: "Create new context",
|
Short: "Create new context",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
mobycli.Exec()
|
mobycli.Exec(cmd.Root())
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
Long: longHelp,
|
Long: longHelp,
|
||||||
|
@ -27,7 +27,7 @@ func inspectCommand() *cobra.Command {
|
|||||||
Use: "inspect",
|
Use: "inspect",
|
||||||
Short: "Display detailed information on one or more contexts",
|
Short: "Display detailed information on one or more contexts",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
mobycli.Exec()
|
mobycli.Exec(cmd.Root())
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,7 @@ func runList(cmd *cobra.Command, opts lsOpts) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if opts.format != "" {
|
if opts.format != "" {
|
||||||
mobycli.Exec()
|
mobycli.Exec(cmd.Root())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ func runLogin(cmd *cobra.Command, args []string) error {
|
|||||||
backend := args[0]
|
backend := args[0]
|
||||||
return errors.New("unknown backend type for cloud login: " + backend)
|
return errors.New("unknown backend type for cloud login: " + backend)
|
||||||
}
|
}
|
||||||
mobycli.Exec()
|
mobycli.Exec(cmd.Root())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,6 +37,6 @@ func Command() *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runLogout(cmd *cobra.Command, args []string) error {
|
func runLogout(cmd *cobra.Command, args []string) error {
|
||||||
mobycli.Exec()
|
mobycli.Exec(cmd.Root())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -51,7 +51,7 @@ func runVersion(cmd *cobra.Command, version string) error {
|
|||||||
// we don't want to fail on error, there is an error if the engine is not available but it displays client version info
|
// we don't want to fail on error, there is an error if the engine is not available but it displays client version info
|
||||||
// Still, technically the [] byte versionResult could be nil, just let the original command display what it has to display
|
// Still, technically the [] byte versionResult could be nil, just let the original command display what it has to display
|
||||||
if versionResult == nil {
|
if versionResult == nil {
|
||||||
mobycli.Exec()
|
mobycli.Exec(cmd.Root())
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
var s string = string(versionResult)
|
var s string = string(versionResult)
|
||||||
|
33
cli/main.go
33
cli/main.go
@ -102,7 +102,7 @@ func main() {
|
|||||||
SilenceUsage: true,
|
SilenceUsage: true,
|
||||||
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if !isContextAgnosticCommand(cmd) {
|
if !isContextAgnosticCommand(cmd) {
|
||||||
mobycli.ExecIfDefaultCtxType(cmd.Context())
|
mobycli.ExecIfDefaultCtxType(cmd.Context(), cmd.Root())
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
@ -136,7 +136,7 @@ func main() {
|
|||||||
helpFunc := root.HelpFunc()
|
helpFunc := root.HelpFunc()
|
||||||
root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
root.SetHelpFunc(func(cmd *cobra.Command, args []string) {
|
||||||
if !isContextAgnosticCommand(cmd) {
|
if !isContextAgnosticCommand(cmd) {
|
||||||
mobycli.ExecIfDefaultCtxType(cmd.Context())
|
mobycli.ExecIfDefaultCtxType(cmd.Context(), cmd.Root())
|
||||||
}
|
}
|
||||||
helpFunc(cmd, args)
|
helpFunc(cmd, args)
|
||||||
})
|
})
|
||||||
@ -158,7 +158,7 @@ func main() {
|
|||||||
|
|
||||||
// --host and --version should immediately be forwarded to the original cli
|
// --host and --version should immediately be forwarded to the original cli
|
||||||
if opts.Host != "" || opts.Version {
|
if opts.Host != "" || opts.Version {
|
||||||
mobycli.Exec()
|
mobycli.Exec(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Config == "" {
|
if opts.Config == "" {
|
||||||
@ -171,7 +171,7 @@ func main() {
|
|||||||
|
|
||||||
s, err := store.New(configDir)
|
s, err := store.New(configDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
mobycli.Exec()
|
mobycli.Exec(root)
|
||||||
}
|
}
|
||||||
|
|
||||||
ctype := store.DefaultContextType
|
ctype := store.DefaultContextType
|
||||||
@ -185,41 +185,43 @@ func main() {
|
|||||||
root.AddCommand(volume.ACICommand())
|
root.AddCommand(volume.ACICommand())
|
||||||
}
|
}
|
||||||
|
|
||||||
metrics.Track(ctype, os.Args[1:], root.PersistentFlags())
|
|
||||||
|
|
||||||
ctx = apicontext.WithCurrentContext(ctx, currentContext)
|
ctx = apicontext.WithCurrentContext(ctx, currentContext)
|
||||||
ctx = store.WithContextStore(ctx, s)
|
ctx = store.WithContextStore(ctx, s)
|
||||||
|
|
||||||
if err = root.ExecuteContext(ctx); err != nil {
|
if err = root.ExecuteContext(ctx); err != nil {
|
||||||
// if user canceled request, simply exit without any error message
|
// if user canceled request, simply exit without any error message
|
||||||
if errors.Is(ctx.Err(), context.Canceled) {
|
if errdefs.IsErrCanceled(err) || errors.Is(ctx.Err(), context.Canceled) {
|
||||||
|
metrics.Track(ctype, os.Args[1:], root.PersistentFlags(), metrics.CanceledStatus)
|
||||||
os.Exit(130)
|
os.Exit(130)
|
||||||
}
|
}
|
||||||
if ctype == store.AwsContextType {
|
if ctype == store.AwsContextType {
|
||||||
exit(root, currentContext, errors.Errorf(`%q context type has been renamed. Recreate the context by running:
|
exit(root, currentContext, errors.Errorf(`%q context type has been renamed. Recreate the context by running:
|
||||||
$ docker context create %s <name>`, cc.Type(), store.EcsContextType))
|
$ docker context create %s <name>`, cc.Type(), store.EcsContextType), ctype)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Context should always be handled by new CLI
|
// Context should always be handled by new CLI
|
||||||
requiredCmd, _, _ := root.Find(os.Args[1:])
|
requiredCmd, _, _ := root.Find(os.Args[1:])
|
||||||
if requiredCmd != nil && isContextAgnosticCommand(requiredCmd) {
|
if requiredCmd != nil && isContextAgnosticCommand(requiredCmd) {
|
||||||
exit(root, currentContext, err)
|
exit(root, currentContext, err, ctype)
|
||||||
}
|
}
|
||||||
mobycli.ExecIfDefaultCtxType(ctx)
|
mobycli.ExecIfDefaultCtxType(ctx, root)
|
||||||
|
|
||||||
checkIfUnknownCommandExistInDefaultContext(err, currentContext)
|
checkIfUnknownCommandExistInDefaultContext(err, currentContext, root)
|
||||||
|
|
||||||
exit(root, currentContext, err)
|
exit(root, currentContext, err, ctype)
|
||||||
}
|
}
|
||||||
|
metrics.Track(ctype, os.Args[1:], root.PersistentFlags(), metrics.SuccessStatus)
|
||||||
}
|
}
|
||||||
|
|
||||||
func exit(cmd *cobra.Command, ctx string, err error) {
|
func exit(root *cobra.Command, ctx string, err error, ctype string) {
|
||||||
|
metrics.Track(ctype, os.Args[1:], root.PersistentFlags(), metrics.FailureStatus)
|
||||||
|
|
||||||
if errors.Is(err, errdefs.ErrLoginRequired) {
|
if errors.Is(err, errdefs.ErrLoginRequired) {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
os.Exit(errdefs.ExitCodeLoginRequired)
|
os.Exit(errdefs.ExitCodeLoginRequired)
|
||||||
}
|
}
|
||||||
if errors.Is(err, errdefs.ErrNotImplemented) {
|
if errors.Is(err, errdefs.ErrNotImplemented) {
|
||||||
cmd, _, _ := cmd.Traverse(os.Args[1:])
|
cmd, _, _ := root.Traverse(os.Args[1:])
|
||||||
name := cmd.Name()
|
name := cmd.Name()
|
||||||
parent := cmd.Parent()
|
parent := cmd.Parent()
|
||||||
if parent != nil && parent.Parent() != nil {
|
if parent != nil && parent.Parent() != nil {
|
||||||
@ -237,13 +239,14 @@ func fatal(err error) {
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkIfUnknownCommandExistInDefaultContext(err error, currentContext string) {
|
func checkIfUnknownCommandExistInDefaultContext(err error, currentContext string, root *cobra.Command) {
|
||||||
submatch := unknownCommandRegexp.FindSubmatch([]byte(err.Error()))
|
submatch := unknownCommandRegexp.FindSubmatch([]byte(err.Error()))
|
||||||
if len(submatch) == 2 {
|
if len(submatch) == 2 {
|
||||||
dockerCommand := string(submatch[1])
|
dockerCommand := string(submatch[1])
|
||||||
|
|
||||||
if mobycli.IsDefaultContextCommand(dockerCommand) {
|
if mobycli.IsDefaultContextCommand(dockerCommand) {
|
||||||
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)
|
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)
|
||||||
|
metrics.Track(currentContext, os.Args[1:], root.PersistentFlags(), metrics.FailureStatus)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -24,8 +24,11 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
apicontext "github.com/docker/compose-cli/context"
|
apicontext "github.com/docker/compose-cli/context"
|
||||||
"github.com/docker/compose-cli/context/store"
|
"github.com/docker/compose-cli/context/store"
|
||||||
|
"github.com/docker/compose-cli/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
var delegatedContextTypes = []string{store.DefaultContextType}
|
var delegatedContextTypes = []string{store.DefaultContextType}
|
||||||
@ -33,8 +36,8 @@ var delegatedContextTypes = []string{store.DefaultContextType}
|
|||||||
// ComDockerCli name of the classic cli binary
|
// ComDockerCli name of the classic cli binary
|
||||||
const ComDockerCli = "com.docker.cli"
|
const ComDockerCli = "com.docker.cli"
|
||||||
|
|
||||||
// ExecIfDefaultCtxType delegates to com.docker.cli if on moby or AWS context (until there is an AWS backend)
|
// ExecIfDefaultCtxType delegates to com.docker.cli if on moby context
|
||||||
func ExecIfDefaultCtxType(ctx context.Context) {
|
func ExecIfDefaultCtxType(ctx context.Context, root *cobra.Command) {
|
||||||
currentContext := apicontext.CurrentContext(ctx)
|
currentContext := apicontext.CurrentContext(ctx)
|
||||||
|
|
||||||
s := store.ContextStore(ctx)
|
s := store.ContextStore(ctx)
|
||||||
@ -42,7 +45,7 @@ func ExecIfDefaultCtxType(ctx context.Context) {
|
|||||||
currentCtx, err := s.Get(currentContext)
|
currentCtx, err := s.Get(currentContext)
|
||||||
// Only run original docker command if the current context is not ours.
|
// Only run original docker command if the current context is not ours.
|
||||||
if err != nil || mustDelegateToMoby(currentCtx.Type()) {
|
if err != nil || mustDelegateToMoby(currentCtx.Type()) {
|
||||||
Exec()
|
Exec(root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -56,7 +59,7 @@ func mustDelegateToMoby(ctxType string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Exec delegates to com.docker.cli if on moby context
|
// Exec delegates to com.docker.cli if on moby context
|
||||||
func Exec() {
|
func Exec(root *cobra.Command) {
|
||||||
cmd := exec.Command(ComDockerCli, os.Args[1:]...)
|
cmd := exec.Command(ComDockerCli, os.Args[1:]...)
|
||||||
cmd.Stdin = os.Stdin
|
cmd.Stdin = os.Stdin
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
@ -83,12 +86,16 @@ func Exec() {
|
|||||||
err := cmd.Run()
|
err := cmd.Run()
|
||||||
childExit <- true
|
childExit <- true
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
metrics.Track(store.DefaultContextName, os.Args[1:], root.PersistentFlags(), metrics.FailureStatus)
|
||||||
|
|
||||||
if exiterr, ok := err.(*exec.ExitError); ok {
|
if exiterr, ok := err.(*exec.ExitError); ok {
|
||||||
os.Exit(exiterr.ExitCode())
|
os.Exit(exiterr.ExitCode())
|
||||||
}
|
}
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
metrics.Track(store.DefaultContextName, os.Args[1:], root.PersistentFlags(), metrics.SuccessStatus)
|
||||||
|
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -30,6 +30,7 @@ import (
|
|||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
|
|
||||||
"github.com/docker/compose-cli/context/store"
|
"github.com/docker/compose-cli/context/store"
|
||||||
|
"github.com/docker/compose-cli/errdefs"
|
||||||
"github.com/docker/compose-cli/prompt"
|
"github.com/docker/compose-cli/prompt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -155,7 +156,7 @@ func (h contextCreateAWSHelper) chooseProfile(section map[string]ini.Section) (s
|
|||||||
selected, err := h.user.Select("Select AWS Profile", profiles)
|
selected, err := h.user.Select("Select AWS Profile", profiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == terminal.InterruptErr {
|
if err == terminal.InterruptErr {
|
||||||
os.Exit(-1)
|
return "", errdefs.ErrCanceled
|
||||||
}
|
}
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
@ -42,6 +42,8 @@ var (
|
|||||||
// ErrNotImplemented is returned when a backend doesn't implement
|
// ErrNotImplemented is returned when a backend doesn't implement
|
||||||
// an action
|
// an action
|
||||||
ErrNotImplemented = errors.New("not implemented")
|
ErrNotImplemented = errors.New("not implemented")
|
||||||
|
// ErrCanceled is returned when the command was canceled by user
|
||||||
|
ErrCanceled = errors.New("canceled")
|
||||||
// ErrParsingFailed is returned when a string cannot be parsed
|
// ErrParsingFailed is returned when a string cannot be parsed
|
||||||
ErrParsingFailed = errors.New("parsing failed")
|
ErrParsingFailed = errors.New("parsing failed")
|
||||||
// ErrWrongContextType is returned when the caller tries to get a context
|
// ErrWrongContextType is returned when the caller tries to get a context
|
||||||
@ -78,3 +80,8 @@ func IsErrNotImplemented(err error) bool {
|
|||||||
func IsErrParsingFailed(err error) bool {
|
func IsErrParsingFailed(err error) bool {
|
||||||
return errors.Is(err, ErrParsingFailed)
|
return errors.Is(err, ErrParsingFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IsErrCanceled returns true if the unwrapped error is ErrCanceled
|
||||||
|
func IsErrCanceled(err error) bool {
|
||||||
|
return errors.Is(err, ErrCanceled)
|
||||||
|
}
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type client struct {
|
type client struct {
|
||||||
@ -33,6 +34,7 @@ type Command struct {
|
|||||||
Command string `json:"command"`
|
Command string `json:"command"`
|
||||||
Context string `json:"context"`
|
Context string `json:"context"`
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
|
Status string `json:"status"`
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -40,6 +42,12 @@ const (
|
|||||||
CLISource = "cli"
|
CLISource = "cli"
|
||||||
// APISource is sent for API metrics
|
// APISource is sent for API metrics
|
||||||
APISource = "api"
|
APISource = "api"
|
||||||
|
// SuccessStatus is sent for API metrics
|
||||||
|
SuccessStatus = "success"
|
||||||
|
// FailureStatus is sent for API metrics
|
||||||
|
FailureStatus = "failure"
|
||||||
|
// CanceledStatus is sent for API metrics
|
||||||
|
CanceledStatus = "canceled"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Client sends metrics to Docker Desktopn
|
// Client sends metrics to Docker Desktopn
|
||||||
@ -64,24 +72,23 @@ func NewClient() Client {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *client) Send(command Command) {
|
func (c *client) Send(command Command) {
|
||||||
wasIn := make(chan bool)
|
result := make(chan bool, 1)
|
||||||
|
|
||||||
// Fire and forget, we don't want to slow down the user waiting for DD
|
|
||||||
// metrics endpoint to respond. We could lose some events but that's ok.
|
|
||||||
go func() {
|
go func() {
|
||||||
defer func() {
|
postMetrics(command, c)
|
||||||
_ = recover()
|
result <- true
|
||||||
}()
|
|
||||||
|
|
||||||
wasIn <- true
|
|
||||||
|
|
||||||
req, err := json.Marshal(command)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, _ = c.httpClient.Post("http://localhost/usage", "application/json", bytes.NewBuffer(req))
|
|
||||||
}()
|
}()
|
||||||
<-wasIn
|
|
||||||
|
|
||||||
|
// wait for the post finished, or timeout in case anything freezes.
|
||||||
|
// Posting metrics without Desktop listening returns in less than a ms, and a handful of ms (often <2ms) when Desktop is listening
|
||||||
|
select {
|
||||||
|
case <-result:
|
||||||
|
case <-time.After(50 * time.Millisecond):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func postMetrics(command Command, c *client) {
|
||||||
|
req, err := json.Marshal(command)
|
||||||
|
if err == nil {
|
||||||
|
_, _ = c.httpClient.Post("http://localhost/usage", "application/json", bytes.NewBuffer(req))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -77,7 +77,7 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// Track sends the tracking analytics to Docker Desktop
|
// Track sends the tracking analytics to Docker Desktop
|
||||||
func Track(context string, args []string, flags *flag.FlagSet) {
|
func Track(context string, args []string, flags *flag.FlagSet, status string) {
|
||||||
command := getCommand(args, flags)
|
command := getCommand(args, flags)
|
||||||
if command != "" {
|
if command != "" {
|
||||||
c := NewClient()
|
c := NewClient()
|
||||||
@ -85,6 +85,7 @@ func Track(context string, args []string, flags *flag.FlagSet) {
|
|||||||
Command: command,
|
Command: command,
|
||||||
Context: context,
|
Context: context,
|
||||||
Source: CLISource,
|
Source: CLISource,
|
||||||
|
Status: status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,9 +41,7 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func metricsServerInterceptor(clictx context.Context) grpc.UnaryServerInterceptor {
|
func metricsServerInterceptor(clictx context.Context, client metrics.Client) grpc.UnaryServerInterceptor {
|
||||||
client := metrics.NewClient()
|
|
||||||
|
|
||||||
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||||
currentContext, err := getIncomingContext(ctx)
|
currentContext, err := getIncomingContext(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -53,15 +51,21 @@ func metricsServerInterceptor(clictx context.Context) grpc.UnaryServerIntercepto
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
data, err := handler(ctx, req)
|
||||||
|
|
||||||
|
status := metrics.SuccessStatus
|
||||||
|
if err != nil {
|
||||||
|
status = metrics.FailureStatus
|
||||||
|
}
|
||||||
command := methodMapping[info.FullMethod]
|
command := methodMapping[info.FullMethod]
|
||||||
if command != "" {
|
if command != "" {
|
||||||
client.Send(metrics.Command{
|
client.Send(metrics.Command{
|
||||||
Command: command,
|
Command: command,
|
||||||
Context: currentContext,
|
Context: currentContext,
|
||||||
Source: metrics.APISource,
|
Source: metrics.APISource,
|
||||||
|
Status: status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
return data, err
|
||||||
return handler(ctx, req)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -21,9 +21,13 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
"gotest.tools/v3/assert"
|
"gotest.tools/v3/assert"
|
||||||
|
|
||||||
|
"github.com/docker/compose-cli/errdefs"
|
||||||
|
"github.com/docker/compose-cli/metrics"
|
||||||
containersv1 "github.com/docker/compose-cli/protos/containers/v1"
|
containersv1 "github.com/docker/compose-cli/protos/containers/v1"
|
||||||
contextsv1 "github.com/docker/compose-cli/protos/contexts/v1"
|
contextsv1 "github.com/docker/compose-cli/protos/contexts/v1"
|
||||||
streamsv1 "github.com/docker/compose-cli/protos/streams/v1"
|
streamsv1 "github.com/docker/compose-cli/protos/streams/v1"
|
||||||
@ -48,6 +52,44 @@ func TestAllMethodsHaveCorrespondingCliCommand(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTrackSuccess(t *testing.T) {
|
||||||
|
var mockMetrics = &mockMetricsClient{}
|
||||||
|
mockMetrics.On("Send", metrics.Command{Command: "ps", Context: "aci", Status: "success", Source: "api"}).Return()
|
||||||
|
interceptor := metricsServerInterceptor(context.TODO(), mockMetrics)
|
||||||
|
|
||||||
|
_, err := interceptor(incomingContext("aci"), nil, containerMethodRoute("List"), mockHandler(nil))
|
||||||
|
assert.NilError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTrackSFailures(t *testing.T) {
|
||||||
|
var mockMetrics = &mockMetricsClient{}
|
||||||
|
mockMetrics.On("Send", metrics.Command{Command: "ps", Context: "default", Status: "failure", Source: "api"}).Return()
|
||||||
|
interceptor := metricsServerInterceptor(context.TODO(), mockMetrics)
|
||||||
|
|
||||||
|
_, err := interceptor(incomingContext("default"), nil, containerMethodRoute("Create"), mockHandler(errdefs.ErrLoginRequired))
|
||||||
|
assert.Assert(t, err == errdefs.ErrLoginRequired)
|
||||||
|
}
|
||||||
|
|
||||||
|
func containerMethodRoute(action string) *grpc.UnaryServerInfo {
|
||||||
|
var info = &grpc.UnaryServerInfo{
|
||||||
|
FullMethod: "/com.docker.api.protos.containers.v1.Containers/" + action,
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func mockHandler(err error) func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func incomingContext(status string) context.Context {
|
||||||
|
ctx := metadata.NewIncomingContext(context.TODO(), metadata.MD{
|
||||||
|
(key): []string{status},
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
func setupServer() *grpc.Server {
|
func setupServer() *grpc.Server {
|
||||||
ctx := context.TODO()
|
ctx := context.TODO()
|
||||||
s := New(ctx)
|
s := New(ctx)
|
||||||
@ -57,3 +99,11 @@ func setupServer() *grpc.Server {
|
|||||||
contextsv1.RegisterContextsServer(s, p.ContextsProxy())
|
contextsv1.RegisterContextsServer(s, p.ContextsProxy())
|
||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockMetricsClient struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *mockMetricsClient) Send(command metrics.Command) {
|
||||||
|
s.Called(command)
|
||||||
|
}
|
||||||
|
@ -24,6 +24,8 @@ import (
|
|||||||
"google.golang.org/grpc"
|
"google.golang.org/grpc"
|
||||||
"google.golang.org/grpc/health"
|
"google.golang.org/grpc/health"
|
||||||
"google.golang.org/grpc/health/grpc_health_v1"
|
"google.golang.org/grpc/health/grpc_health_v1"
|
||||||
|
|
||||||
|
"github.com/docker/compose-cli/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
// New returns a new GRPC server.
|
// New returns a new GRPC server.
|
||||||
@ -31,7 +33,7 @@ func New(ctx context.Context) *grpc.Server {
|
|||||||
s := grpc.NewServer(
|
s := grpc.NewServer(
|
||||||
grpc.ChainUnaryInterceptor(
|
grpc.ChainUnaryInterceptor(
|
||||||
unaryServerInterceptor(ctx),
|
unaryServerInterceptor(ctx),
|
||||||
metricsServerInterceptor(ctx),
|
metricsServerInterceptor(ctx, metrics.NewClient()),
|
||||||
),
|
),
|
||||||
grpc.StreamInterceptor(streamServerInterceptor(ctx)),
|
grpc.StreamInterceptor(streamServerInterceptor(ctx)),
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user