Browse Source

Add interceptor for API metrics, ensure registered methods have a corresponding method set for metrics

Signed-off-by: Djordje Lukic <[email protected]>
Djordje Lukic 5 years ago
parent
commit
2570ebec86
5 changed files with 140 additions and 1 deletions
  1. 8 0
      metrics/client.go
  2. 1 0
      metrics/metrics.go
  3. 67 0
      server/metrics.go
  4. 60 0
      server/metrics_test.go
  5. 4 1
      server/server.go

+ 8 - 0
metrics/client.go

@@ -32,8 +32,16 @@ type client struct {
 type Command struct {
 	Command string `json:"command"`
 	Context string `json:"context"`
+	Source  string `json:"source"`
 }
 
+const (
+	// CLISource is sent for cli metrics
+	CLISource = "cli"
+	// APISource is sent for API metrics
+	APISource = "api"
+)
+
 // Client sends metrics to Docker Desktopn
 type Client interface {
 	// Send sends the command to Docker Desktop. Note that the function doesn't

+ 1 - 0
metrics/metrics.go

@@ -95,6 +95,7 @@ func Track(context string, args []string, flags *flag.FlagSet) {
 			c.Send(Command{
 				Command: command,
 				Context: context,
+				Source:  CLISource,
 			})
 		}
 	}()

+ 67 - 0
server/metrics.go

@@ -0,0 +1,67 @@
+/*
+   Copyright 2020 Docker, Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package server
+
+import (
+	"context"
+
+	"google.golang.org/grpc"
+
+	"github.com/docker/compose-cli/metrics"
+)
+
+var (
+	methodMapping = map[string]string{
+		"/com.docker.api.protos.containers.v1.Containers/List":    "ps",
+		"/com.docker.api.protos.containers.v1.Containers/Start":   "start",
+		"/com.docker.api.protos.containers.v1.Containers/Stop":    "stop",
+		"/com.docker.api.protos.containers.v1.Containers/Run":     "run",
+		"/com.docker.api.protos.containers.v1.Containers/Exec":    "exec",
+		"/com.docker.api.protos.containers.v1.Containers/Delete":  "rm",
+		"/com.docker.api.protos.containers.v1.Containers/Kill":    "kill",
+		"/com.docker.api.protos.containers.v1.Containers/Inspect": "inspect",
+		"/com.docker.api.protos.containers.v1.Containers/Logs":    "logs",
+		"/com.docker.api.protos.streams.v1.Streaming/NewStream":   "",
+		"/com.docker.api.protos.context.v1.Contexts/List":         "context ls",
+		"/com.docker.api.protos.context.v1.Contexts/SetCurrent":   "context use",
+	}
+)
+
+func metricsServerInterceptor(clictx context.Context) grpc.UnaryServerInterceptor {
+	client := metrics.NewClient()
+
+	return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
+		currentContext, err := getIncomingContext(ctx)
+		if err != nil {
+			currentContext, err = getConfigContext(clictx)
+			if err != nil {
+				return nil, err
+			}
+		}
+
+		command := methodMapping[info.FullMethod]
+		if command != "" {
+			client.Send(metrics.Command{
+				Command: command,
+				Context: currentContext,
+				Source:  metrics.APISource,
+			})
+		}
+
+		return handler(ctx, req)
+	}
+}

+ 60 - 0
server/metrics_test.go

@@ -0,0 +1,60 @@
+/*
+   Copyright 2020 Docker, Inc.
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
+*/
+
+package server
+
+import (
+	"context"
+	"strings"
+	"testing"
+
+	"gotest.tools/v3/assert"
+
+	"google.golang.org/grpc"
+
+	containersv1 "github.com/docker/compose-cli/protos/containers/v1"
+	contextsv1 "github.com/docker/compose-cli/protos/contexts/v1"
+	streamsv1 "github.com/docker/compose-cli/protos/streams/v1"
+	"github.com/docker/compose-cli/server/proxy"
+)
+
+func TestAllMethodsHaveCorrespondingCliCommand(t *testing.T) {
+	s := setupServer()
+	i := s.GetServiceInfo()
+	for k, v := range i {
+		if k == "grpc.health.v1.Health" {
+			continue
+		}
+		var errs []string
+		for _, m := range v.Methods {
+			name := "/" + k + "/" + m.Name
+			if _, keyExists := methodMapping[name]; !keyExists {
+				errs = append(errs, name+" not mapped to a corresponding cli command")
+			}
+		}
+		assert.Equal(t, "", strings.Join(errs, "\n"))
+	}
+}
+
+func setupServer() *grpc.Server {
+	ctx := context.TODO()
+	s := New(ctx)
+	p := proxy.New(ctx)
+	containersv1.RegisterContainersServer(s, p)
+	streamsv1.RegisterStreamingServer(s, p)
+	contextsv1.RegisterContextsServer(s, p.ContextsProxy())
+	return s
+}

+ 4 - 1
server/server.go

@@ -29,7 +29,10 @@ import (
 // New returns a new GRPC server.
 func New(ctx context.Context) *grpc.Server {
 	s := grpc.NewServer(
-		grpc.UnaryInterceptor(unaryServerInterceptor(ctx)),
+		grpc.ChainUnaryInterceptor(
+			unaryServerInterceptor(ctx),
+			metricsServerInterceptor(ctx),
+		),
 		grpc.StreamInterceptor(streamServerInterceptor(ctx)),
 	)
 	hs := health.NewServer()