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

Signed-off-by: Djordje Lukic <djordje.lukic@docker.com>
This commit is contained in:
Djordje Lukic 2020-08-27 11:27:00 +02:00 committed by Guillaume Tardif
parent 6cb19ed26d
commit 2570ebec86
5 changed files with 140 additions and 1 deletions

View File

@ -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

View File

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

67
server/metrics.go Normal file
View File

@ -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
server/metrics_test.go Normal file
View File

@ -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
}

View File

@ -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()