mirror of https://github.com/docker/cli.git
Merge pull request #4978 from laurazard/otel-add-tty
otel: capture whether process was invoked from a terminal
This commit is contained in:
commit
10b9810989
|
@ -110,7 +110,7 @@ func (r *telemetryResource) init() {
|
|||
return
|
||||
}
|
||||
|
||||
opts := append(r.defaultOptions(), r.opts...)
|
||||
opts := append(defaultResourceOptions(), r.opts...)
|
||||
res, err := resource.New(context.Background(), opts...)
|
||||
if err != nil {
|
||||
otel.Handle(err)
|
||||
|
@ -122,7 +122,7 @@ func (r *telemetryResource) init() {
|
|||
r.opts = nil
|
||||
}
|
||||
|
||||
func (r *telemetryResource) defaultOptions() []resource.Option {
|
||||
func defaultResourceOptions() []resource.Option {
|
||||
return []resource.Option{
|
||||
resource.WithDetectors(serviceNameDetector{}),
|
||||
resource.WithAttributes(
|
||||
|
|
|
@ -8,18 +8,18 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/docker/cli/cli/version"
|
||||
"github.com/moby/term"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/metric"
|
||||
)
|
||||
|
||||
// BaseMetricAttributes returns an attribute.Set containing attributes to attach to metrics/traces
|
||||
func BaseMetricAttributes(cmd *cobra.Command) attribute.Set {
|
||||
attrList := []attribute.KeyValue{
|
||||
// BaseCommandAttributes returns an attribute.Set containing attributes to attach to metrics/traces
|
||||
func BaseCommandAttributes(cmd *cobra.Command, streams Streams) []attribute.KeyValue {
|
||||
return append([]attribute.KeyValue{
|
||||
attribute.String("command.name", getCommandName(cmd)),
|
||||
}
|
||||
return attribute.NewSet(attrList...)
|
||||
}, stdioAttributes(streams)...)
|
||||
}
|
||||
|
||||
// InstrumentCobraCommands wraps all cobra commands' RunE funcs to set a command duration metric using otel.
|
||||
|
@ -27,7 +27,7 @@ func BaseMetricAttributes(cmd *cobra.Command) attribute.Set {
|
|||
// Note: this should be the last func to wrap/modify the PersistentRunE/RunE funcs before command execution.
|
||||
//
|
||||
// can also be used for spans!
|
||||
func InstrumentCobraCommands(cmd *cobra.Command, mp metric.MeterProvider) {
|
||||
func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command, mp metric.MeterProvider) {
|
||||
meter := getDefaultMeter(mp)
|
||||
// If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default
|
||||
ogPersistentPreRunE := cmd.PersistentPreRunE
|
||||
|
@ -56,7 +56,8 @@ func InstrumentCobraCommands(cmd *cobra.Command, mp metric.MeterProvider) {
|
|||
}
|
||||
cmd.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
// start the timer as the first step of every cobra command
|
||||
stopCobraCmdTimer := startCobraCommandTimer(cmd, meter)
|
||||
baseAttrs := BaseCommandAttributes(cmd, cli)
|
||||
stopCobraCmdTimer := startCobraCommandTimer(cmd, meter, baseAttrs)
|
||||
cmdErr := ogRunE(cmd, args)
|
||||
stopCobraCmdTimer(cmdErr)
|
||||
return cmdErr
|
||||
|
@ -66,9 +67,8 @@ func InstrumentCobraCommands(cmd *cobra.Command, mp metric.MeterProvider) {
|
|||
}
|
||||
}
|
||||
|
||||
func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter) func(err error) {
|
||||
func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter, attrs []attribute.KeyValue) func(err error) {
|
||||
ctx := cmd.Context()
|
||||
baseAttrs := BaseMetricAttributes(cmd)
|
||||
durationCounter, _ := meter.Float64Counter(
|
||||
"command.time",
|
||||
metric.WithDescription("Measures the duration of the cobra command"),
|
||||
|
@ -80,12 +80,22 @@ func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter) func(err err
|
|||
duration := float64(time.Since(start)) / float64(time.Millisecond)
|
||||
cmdStatusAttrs := attributesFromError(err)
|
||||
durationCounter.Add(ctx, duration,
|
||||
metric.WithAttributeSet(baseAttrs),
|
||||
metric.WithAttributeSet(attribute.NewSet(cmdStatusAttrs...)),
|
||||
metric.WithAttributes(attrs...),
|
||||
metric.WithAttributes(cmdStatusAttrs...),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func stdioAttributes(streams Streams) []attribute.KeyValue {
|
||||
// we don't wrap stderr, but we do wrap in/out
|
||||
_, stderrTty := term.GetFdInfo(streams.Err())
|
||||
return []attribute.KeyValue{
|
||||
attribute.Bool("command.stdin.isatty", streams.In().IsTerminal()),
|
||||
attribute.Bool("command.stdout.isatty", streams.Out().IsTerminal()),
|
||||
attribute.Bool("command.stderr.isatty", stderrTty),
|
||||
}
|
||||
}
|
||||
|
||||
func attributesFromError(err error) []attribute.KeyValue {
|
||||
attrs := []attribute.KeyValue{}
|
||||
exitCode := 0
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/docker/cli/cli/streams"
|
||||
"github.com/spf13/cobra"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"gotest.tools/v3/assert"
|
||||
)
|
||||
|
||||
|
@ -92,3 +99,91 @@ func TestGetCommandName(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStdioAttributes(t *testing.T) {
|
||||
outBuffer := new(bytes.Buffer)
|
||||
errBuffer := new(bytes.Buffer)
|
||||
t.Parallel()
|
||||
for _, tc := range []struct {
|
||||
test string
|
||||
stdinTty bool
|
||||
stdoutTty bool
|
||||
// TODO(laurazard): test stderr
|
||||
expected []attribute.KeyValue
|
||||
}{
|
||||
{
|
||||
test: "",
|
||||
expected: []attribute.KeyValue{
|
||||
attribute.Bool("command.stdin.isatty", false),
|
||||
attribute.Bool("command.stdout.isatty", false),
|
||||
attribute.Bool("command.stderr.isatty", false),
|
||||
},
|
||||
},
|
||||
{
|
||||
test: "",
|
||||
stdinTty: true,
|
||||
stdoutTty: true,
|
||||
expected: []attribute.KeyValue{
|
||||
attribute.Bool("command.stdin.isatty", true),
|
||||
attribute.Bool("command.stdout.isatty", true),
|
||||
attribute.Bool("command.stderr.isatty", false),
|
||||
},
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.test, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
cli := &DockerCli{
|
||||
in: streams.NewIn(io.NopCloser(strings.NewReader(""))),
|
||||
out: streams.NewOut(outBuffer),
|
||||
err: errBuffer,
|
||||
}
|
||||
cli.In().SetIsTerminal(tc.stdinTty)
|
||||
cli.Out().SetIsTerminal(tc.stdoutTty)
|
||||
actual := stdioAttributes(cli)
|
||||
|
||||
assert.Check(t, reflect.DeepEqual(actual, tc.expected))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAttributesFromError(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, tc := range []struct {
|
||||
testName string
|
||||
err error
|
||||
expected []attribute.KeyValue
|
||||
}{
|
||||
{
|
||||
testName: "no error",
|
||||
err: nil,
|
||||
expected: []attribute.KeyValue{
|
||||
attribute.String("command.status.code", "0"),
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "non-0 exit code",
|
||||
err: statusError{StatusCode: 127},
|
||||
expected: []attribute.KeyValue{
|
||||
attribute.String("command.error.type", "generic"),
|
||||
attribute.String("command.status.code", "127"),
|
||||
},
|
||||
},
|
||||
{
|
||||
testName: "canceled",
|
||||
err: context.Canceled,
|
||||
expected: []attribute.KeyValue{
|
||||
attribute.String("command.error.type", "canceled"),
|
||||
attribute.String("command.status.code", "1"),
|
||||
},
|
||||
},
|
||||
} {
|
||||
tc := tc
|
||||
t.Run(tc.testName, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
actual := attributesFromError(tc.err)
|
||||
assert.Check(t, reflect.DeepEqual(actual, tc.expected))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -310,7 +310,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
|
|||
mp := dockerCli.MeterProvider(ctx)
|
||||
defer mp.Shutdown(ctx)
|
||||
otel.SetMeterProvider(mp)
|
||||
command.InstrumentCobraCommands(cmd, mp)
|
||||
dockerCli.InstrumentCobraCommands(cmd, mp)
|
||||
|
||||
var envs []string
|
||||
args, os.Args, envs, err = processAliases(dockerCli, cmd, args, os.Args)
|
||||
|
|
Loading…
Reference in New Issue