ソースを参照

Merge pull request #624 from docker/feat-api-metrics

Add  interceptor for API metrics
Guillaume Tardif 5 年 前
コミット
e904c71b04
8 ファイル変更170 行追加28 行削除
  1. 1 0
      aci/convert/container.go
  2. 1 0
      ecs/backend.go
  3. 2 1
      ecs/local/backend.go
  4. 27 5
      metrics/client.go
  5. 9 21
      metrics/metrics.go
  6. 67 0
      server/metrics.go
  7. 59 0
      server/metrics_test.go
  8. 4 1
      server/server.go

+ 1 - 0
aci/convert/container.go

@@ -21,6 +21,7 @@ import (
 	"strings"
 
 	"github.com/compose-spec/compose-go/types"
+
 	"github.com/docker/compose-cli/api/containers"
 )
 

+ 1 - 0
ecs/backend.go

@@ -21,6 +21,7 @@ import (
 
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/session"
+
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/api/secrets"

+ 2 - 1
ecs/local/backend.go

@@ -19,6 +19,8 @@ package local
 import (
 	"context"
 
+	"github.com/docker/docker/client"
+
 	"github.com/docker/compose-cli/api/compose"
 	"github.com/docker/compose-cli/api/containers"
 	"github.com/docker/compose-cli/api/secrets"
@@ -26,7 +28,6 @@ import (
 	"github.com/docker/compose-cli/backend"
 	"github.com/docker/compose-cli/context/cloud"
 	"github.com/docker/compose-cli/context/store"
-	"github.com/docker/docker/client"
 )
 
 const backendType = store.EcsLocalSimulationContextType

+ 27 - 5
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
@@ -56,10 +64,24 @@ func NewClient() Client {
 }
 
 func (c *client) Send(command Command) {
-	req, err := json.Marshal(command)
-	if err != nil {
-		return
-	}
+	wasIn := make(chan bool)
+
+	// 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() {
+		defer func() {
+			_ = recover()
+		}()
+
+		wasIn <- true
+
+		req, err := json.Marshal(command)
+		if err != nil {
+			return
+		}
+
+		_, _ = c.httpClient.Post("http://localhost/usage", "application/json", bytes.NewBuffer(req))
+	}()
+	<-wasIn
 
-	_, _ = c.httpClient.Post("http://localhost/usage", "application/json", bytes.NewBuffer(req))
 }

+ 9 - 21
metrics/metrics.go

@@ -78,27 +78,15 @@ const (
 
 // Track sends the tracking analytics to Docker Desktop
 func Track(context string, args []string, flags *flag.FlagSet) {
-	wasIn := make(chan bool)
-
-	// 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() {
-		defer func() {
-			_ = recover()
-		}()
-
-		wasIn <- true
-
-		command := getCommand(args, flags)
-		if command != "" {
-			c := NewClient()
-			c.Send(Command{
-				Command: command,
-				Context: context,
-			})
-		}
-	}()
-	<-wasIn
+	command := getCommand(args, flags)
+	if command != "" {
+		c := NewClient()
+		c.Send(Command{
+			Command: command,
+			Context: context,
+			Source:  CLISource,
+		})
+	}
 }
 
 func getCommand(args []string, flags *flag.FlagSet) string {

+ 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)
+	}
+}

+ 59 - 0
server/metrics_test.go

@@ -0,0 +1,59 @@
+/*
+   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"
+
+	"google.golang.org/grpc"
+	"gotest.tools/v3/assert"
+
+	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()