mirror of https://github.com/docker/compose.git
Merge pull request #624 from docker/feat-api-metrics
Add interceptor for API metrics
This commit is contained in:
commit
e904c71b04
|
@ -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"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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))
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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()
|
||||||
|
|
Loading…
Reference in New Issue