Skip to content
This repository was archived by the owner on Jul 18, 2025. It is now read-only.

Commit 2974b2a

Browse files
authored
Merge pull request #2219 from milas/metrics-recorder
metrics: add debug implementation
2 parents 95d9fbd + 3f6822e commit 2974b2a

File tree

6 files changed

+166
-26
lines changed

6 files changed

+166
-26
lines changed

cli/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ func init() {
8888
panic(err)
8989
}
9090

91-
metricsClient = metrics.NewClient()
91+
metricsClient = metrics.NewDefaultClient()
9292
metricsClient.WithCliVersionFunc(func() string {
9393
return mobycli.CliVersion()
9494
})

cli/metrics/client.go

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,18 @@
1717
package metrics
1818

1919
import (
20-
"bytes"
21-
"context"
22-
"encoding/json"
23-
"net"
24-
"net/http"
2520
"os"
2621
"time"
2722
)
2823

24+
// EnvVarDebugMetricsPath is an optional environment variable used to debug
25+
// metrics by triggering all events to also be written as JSON lines to the
26+
// specified file path.
27+
const EnvVarDebugMetricsPath = "DOCKER_METRICS_DEBUG_LOG"
28+
2929
type client struct {
3030
cliversion *cliversion
31-
httpClient *http.Client
31+
reporter Reporter
3232
}
3333

3434
type cliversion struct {
@@ -65,18 +65,33 @@ type Client interface {
6565
Track(context string, args []string, status string)
6666
}
6767

68-
// NewClient returns a new metrics client
69-
func NewClient() Client {
68+
// NewClient returns a new metrics client that will send metrics using the
69+
// provided Reporter instance.
70+
func NewClient(reporter Reporter) Client {
7071
return &client{
7172
cliversion: &cliversion{},
72-
httpClient: &http.Client{
73-
Transport: &http.Transport{
74-
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
75-
return conn()
76-
},
77-
},
78-
},
73+
reporter: reporter,
74+
}
75+
}
76+
77+
// NewDefaultClient returns a new metrics client that will send metrics using
78+
// the default Reporter configuration, which reports via HTTP, and, optionally,
79+
// to a local file for debugging. (No format guarantees are made!)
80+
func NewDefaultClient() Client {
81+
httpClient := newHTTPClient()
82+
83+
var reporter Reporter = NewHTTPReporter(httpClient)
84+
if metricsLogPath := os.Getenv(EnvVarDebugMetricsPath); metricsLogPath != "" {
85+
if f, err := os.OpenFile(metricsLogPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
86+
panic(err)
87+
} else {
88+
reporter = NewMuxReporter(
89+
NewWriterReporter(f),
90+
reporter,
91+
)
92+
}
7993
}
94+
return NewClient(reporter)
8095
}
8196

8297
func (c *client) WithCliVersionFunc(f func() string) {
@@ -86,7 +101,7 @@ func (c *client) WithCliVersionFunc(f func() string) {
86101
func (c *client) Send(command Command) {
87102
result := make(chan bool, 1)
88103
go func() {
89-
postMetrics(command, c)
104+
c.reporter.Heartbeat(command)
90105
result <- true
91106
}()
92107

@@ -97,10 +112,3 @@ func (c *client) Send(command Command) {
97112
case <-time.After(50 * time.Millisecond):
98113
}
99114
}
100-
101-
func postMetrics(command Command, c *client) {
102-
req, err := json.Marshal(command)
103-
if err == nil {
104-
_, _ = c.httpClient.Post("http://localhost/usage", "application/json", bytes.NewBuffer(req))
105-
}
106-
}

cli/metrics/conn.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
Copyright 2022 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package metrics
18+
19+
import (
20+
"context"
21+
"net"
22+
"net/http"
23+
)
24+
25+
func newHTTPClient() *http.Client {
26+
return &http.Client{
27+
Transport: &http.Transport{
28+
DialContext: func(_ context.Context, _, _ string) (net.Conn, error) {
29+
return conn()
30+
},
31+
},
32+
}
33+
}

cli/metrics/reporter.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
Copyright 2022 Docker Compose CLI authors
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package metrics
18+
19+
import (
20+
"bytes"
21+
"encoding/json"
22+
"io"
23+
"net/http"
24+
)
25+
26+
// Reporter reports metric events generated by the client.
27+
type Reporter interface {
28+
Heartbeat(cmd Command)
29+
}
30+
31+
// HTTPReporter reports metric events to an HTTP endpoint.
32+
type HTTPReporter struct {
33+
client *http.Client
34+
}
35+
36+
// NewHTTPReporter creates a new reporter that will report metric events using
37+
// the provided HTTP client.
38+
func NewHTTPReporter(client *http.Client) HTTPReporter {
39+
return HTTPReporter{client: client}
40+
}
41+
42+
// Heartbeat reports a metric for aggregation.
43+
func (l HTTPReporter) Heartbeat(cmd Command) {
44+
entry, err := json.Marshal(cmd)
45+
if err != nil {
46+
// impossible: cannot fail on controlled input (i.e. no cycles)
47+
return
48+
}
49+
50+
resp, _ := l.client.Post(
51+
"http://localhost/usage",
52+
"application/json",
53+
bytes.NewReader(entry),
54+
)
55+
if resp != nil && resp.Body != nil {
56+
_ = resp.Body.Close()
57+
}
58+
}
59+
60+
// WriterReporter reports metrics as JSON lines to the provided writer.
61+
type WriterReporter struct {
62+
w io.Writer
63+
}
64+
65+
// NewWriterReporter creates a new reporter that will write metrics to the
66+
// provided writer as JSON lines.
67+
func NewWriterReporter(w io.Writer) WriterReporter {
68+
return WriterReporter{w: w}
69+
}
70+
71+
// Heartbeat reports a metric for aggregation.
72+
func (w WriterReporter) Heartbeat(cmd Command) {
73+
entry, err := json.Marshal(cmd)
74+
if err != nil {
75+
// impossible: cannot fail on controlled input (i.e. no cycles)
76+
return
77+
}
78+
entry = append(entry, '\n')
79+
_, _ = w.w.Write(entry)
80+
}
81+
82+
// MuxReporter wraps multiple reporter instances and reports metrics to each
83+
// instance on invocation.
84+
type MuxReporter struct {
85+
reporters []Reporter
86+
}
87+
88+
// NewMuxReporter creates a reporter that will report metrics to each of the
89+
// provided reporter instances.
90+
func NewMuxReporter(reporters ...Reporter) MuxReporter {
91+
return MuxReporter{reporters: reporters}
92+
}
93+
94+
// Heartbeat reports a metric for aggregation.
95+
func (m MuxReporter) Heartbeat(cmd Command) {
96+
for i := range m.reporters {
97+
m.reporters[i].Heartbeat(cmd)
98+
}
99+
}

cli/mobycli/exec.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func mustDelegateToMoby(ctxType string) bool {
7373

7474
// Exec delegates to com.docker.cli if on moby context
7575
func Exec(root *cobra.Command) {
76-
metricsClient := metrics.NewClient()
76+
metricsClient := metrics.NewDefaultClient()
7777
metricsClient.WithCliVersionFunc(func() string {
7878
return CliVersion()
7979
})

cli/server/server.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func New(ctx context.Context) *grpc.Server {
3333
s := grpc.NewServer(
3434
grpc.ChainUnaryInterceptor(
3535
unaryServerInterceptor(ctx),
36-
metricsServerInterceptor(metrics.NewClient()),
36+
metricsServerInterceptor(metrics.NewDefaultClient()),
3737
),
3838
grpc.StreamInterceptor(streamServerInterceptor(ctx)),
3939
)

0 commit comments

Comments
 (0)