Usage metrics

Send usage to Docker Desktop
This commit is contained in:
Djordje Lukic 2020-06-22 09:55:28 +02:00
parent e24f2a36c2
commit 0c1f0f81df
6 changed files with 387 additions and 0 deletions

View File

@ -35,6 +35,7 @@ import (
_ "github.com/docker/api/azure"
_ "github.com/docker/api/example"
_ "github.com/docker/api/local"
"github.com/docker/api/metrics"
"github.com/docker/api/cli/cmd"
"github.com/docker/api/cli/cmd/compose"
@ -154,6 +155,15 @@ func main() {
if err != nil {
fatal(errors.Wrap(err, "unable to create context store"))
}
ctype := store.DefaultContextType
cc, _ := s.Get(currentContext)
if cc != nil {
ctype = cc.Type()
}
metrics.Track(ctype, os.Args[1:], root.PersistentFlags())
ctx = apicontext.WithCurrentContext(ctx, currentContext)
ctx = store.WithContextStore(ctx, s)

65
metrics/client.go Normal file
View File

@ -0,0 +1,65 @@
/*
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 metrics
import (
"bytes"
"context"
"encoding/json"
"net"
"net/http"
)
type client struct {
httpClient *http.Client
}
// Command is a command
type Command struct {
Command string `json:"command"`
Context string `json:"context"`
}
// Client sends metrics to Docker Desktopn
type Client interface {
// Send sends the command to Docker Desktop. Note that the function doesn't
// return anything, not even an error, this is because we don't really care
// if the metrics were sent or not. We only fire and forget.
Send(Command)
}
// NewClient returns a new metrics client
func NewClient() Client {
return &client{
httpClient: &http.Client{
Transport: &http.Transport{
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
return conn()
},
},
},
}
}
func (c *client) Send(command Command) {
req, err := json.Marshal(command)
if err != nil {
return
}
_, _ = c.httpClient.Post("http://localhost/usage", "application/json", bytes.NewBuffer(req))
}

29
metrics/conn_other.go Normal file
View File

@ -0,0 +1,29 @@
// +build !windows
/*
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 metrics
import "net"
const (
socket = "/var/run/docker-cli.sock"
)
func conn() (net.Conn, error) {
return net.Dial("unix", socket)
}

35
metrics/conn_windows.go Normal file
View File

@ -0,0 +1,35 @@
// +build windows
/*
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 metrics
import (
"net"
"time"
"github.com/Microsoft/go-winio"
)
const (
socket = `\\.\pipe\docker_cli`
)
func conn() (net.Conn, error) {
timeout := 200 * time.Millisecond
return winio.DialPipe(socket, &timeout)
}

94
metrics/metics_test.go Normal file
View File

@ -0,0 +1,94 @@
/*
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 metrics
import (
"testing"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
func TestFlag(t *testing.T) {
root := &cobra.Command{}
root.PersistentFlags().BoolP("debug", "d", false, "debug")
root.PersistentFlags().String("str", "str", "str")
testCases := []struct {
name string
flags []string
expected string
}{
{
name: "with long flags",
flags: []string{"--debug", "run"},
expected: "run",
},
{
name: "with short flags",
flags: []string{"-d", "run"},
expected: "run",
},
{
name: "with flags with value",
flags: []string{"--debug", "--str", "str-value", "run"},
expected: "run",
},
{
name: "with --",
flags: []string{"--debug", "--str", "str-value", "--", "run"},
expected: "",
},
{
name: "without a command",
flags: []string{"--debug", "--str", "str-value"},
expected: "",
},
{
name: "with unknown short flag",
flags: []string{"-f", "run"},
expected: "",
},
{
name: "with unknown long flag",
flags: []string{"--unknown-flag", "run"},
expected: "",
},
{
name: "management command",
flags: []string{"image", "ls"},
expected: "image ls",
},
{
name: "management command with flag",
flags: []string{"image", "--test", "ls"},
expected: "image",
},
{
name: "management subcommand with flag",
flags: []string{"image", "ls", "-q"},
expected: "image ls",
},
}
for _, testCase := range testCases {
t.Run(testCase.name, func(t *testing.T) {
result := getCommand(testCase.flags, root.PersistentFlags())
assert.Equal(t, testCase.expected, result)
})
}
}

154
metrics/metrics.go Normal file
View File

@ -0,0 +1,154 @@
/*
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 metrics
import (
"strings"
flag "github.com/spf13/pflag"
)
var managementCommands = []string{
"app",
"assemble",
"builder",
"buildx",
"cluster",
"compose",
"config",
"container",
"context",
"help",
"image",
"manifest",
"network",
"node",
"plugin",
"registry",
"secret",
"service",
"stack",
"swarm",
"system",
"template",
"trust",
"volume",
}
// Track sends the tracking analytics to Docker Desktop
func Track(context string, args []string, flags *flag.FlagSet) {
// 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()
}()
command := getCommand(args, flags)
if command != "" {
c := NewClient()
c.Send(Command{
Command: command,
Context: context,
})
}
}()
}
func getCommand(args []string, flags *flag.FlagSet) string {
command := ""
args = stripFlags(args, flags)
if len(args) != 0 {
command = args[0]
if contains(managementCommands, command) {
if sub := getSubCommand(args[1:]); sub != "" {
return command + " " + sub
}
}
}
return command
}
func getSubCommand(args []string) string {
if len(args) != 0 && isArg(args[0]) {
return args[0]
}
return ""
}
func contains(array []string, needle string) bool {
for _, val := range array {
if val == needle {
return true
}
}
return false
}
func stripFlags(args []string, flags *flag.FlagSet) []string {
commands := []string{}
for len(args) > 0 {
s := args[0]
args = args[1:]
if s == "--" {
return commands
}
if flagArg(s, flags) {
if len(args) <= 1 {
return commands
}
args = args[1:]
}
if isArg(s) {
commands = append(commands, s)
}
}
return commands
}
func flagArg(s string, flags *flag.FlagSet) bool {
return strings.HasPrefix(s, "--") && !strings.Contains(s, "=") && !hasNoOptDefVal(s[2:], flags) ||
strings.HasPrefix(s, "-") && !strings.Contains(s, "=") && len(s) == 2 && !shortHasNoOptDefVal(s[1:], flags)
}
func isArg(s string) bool {
return s != "" && !strings.HasPrefix(s, "-")
}
func hasNoOptDefVal(name string, fs *flag.FlagSet) bool {
flag := fs.Lookup(name)
if flag == nil {
return false
}
return flag.NoOptDefVal != ""
}
func shortHasNoOptDefVal(name string, fs *flag.FlagSet) bool {
flag := fs.ShorthandLookup(name[:1])
if flag == nil {
return false
}
return flag.NoOptDefVal != ""
}