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

Add  interceptor for API metrics
This commit is contained in:
Guillaume Tardif 2020-09-17 11:40:22 +02:00 committed by GitHub
commit e904c71b04
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 170 additions and 28 deletions

View File

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

View File

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

View File

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

View File

@ -32,8 +32,16 @@ type client struct {
type Command struct { type Command struct {
Command string `json:"command"` Command string `json:"command"`
Context string `json:"context"` 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 // Client sends metrics to Docker Desktopn
type Client interface { type Client interface {
// Send sends the command to Docker Desktop. Note that the function doesn't // 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) { func (c *client) Send(command Command) {
req, err := json.Marshal(command) wasIn := make(chan bool)
if err != nil {
return // 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))
} }

View File

@ -78,27 +78,15 @@ 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) {
wasIn := make(chan bool) command := getCommand(args, flags)
if command != "" {
// Fire and forget, we don't want to slow down the user waiting for DD c := NewClient()
// metrics endpoint to respond. We could lose some events but that's ok. c.Send(Command{
go func() { Command: command,
defer func() { Context: context,
_ = recover() Source: CLISource,
}() })
}
wasIn <- true
command := getCommand(args, flags)
if command != "" {
c := NewClient()
c.Send(Command{
Command: command,
Context: context,
})
}
}()
<-wasIn
} }
func getCommand(args []string, flags *flag.FlagSet) string { func getCommand(args []string, flags *flag.FlagSet) string {

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

59
server/metrics_test.go Normal file
View File

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

View File

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