Merge pull request #4978 from laurazard/otel-add-tty

otel: capture whether process was invoked from a terminal
This commit is contained in:
Bjorn Neergaard 2024-04-04 06:09:48 -06:00 committed by GitHub
commit 10b9810989
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 119 additions and 14 deletions

View File

@ -110,7 +110,7 @@ func (r *telemetryResource) init() {
return return
} }
opts := append(r.defaultOptions(), r.opts...) opts := append(defaultResourceOptions(), r.opts...)
res, err := resource.New(context.Background(), opts...) res, err := resource.New(context.Background(), opts...)
if err != nil { if err != nil {
otel.Handle(err) otel.Handle(err)
@ -122,7 +122,7 @@ func (r *telemetryResource) init() {
r.opts = nil r.opts = nil
} }
func (r *telemetryResource) defaultOptions() []resource.Option { func defaultResourceOptions() []resource.Option {
return []resource.Option{ return []resource.Option{
resource.WithDetectors(serviceNameDetector{}), resource.WithDetectors(serviceNameDetector{}),
resource.WithAttributes( resource.WithAttributes(

View File

@ -8,18 +8,18 @@ import (
"time" "time"
"github.com/docker/cli/cli/version" "github.com/docker/cli/cli/version"
"github.com/moby/term"
"github.com/pkg/errors" "github.com/pkg/errors"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric" "go.opentelemetry.io/otel/metric"
) )
// BaseMetricAttributes returns an attribute.Set containing attributes to attach to metrics/traces // BaseCommandAttributes returns an attribute.Set containing attributes to attach to metrics/traces
func BaseMetricAttributes(cmd *cobra.Command) attribute.Set { func BaseCommandAttributes(cmd *cobra.Command, streams Streams) []attribute.KeyValue {
attrList := []attribute.KeyValue{ return append([]attribute.KeyValue{
attribute.String("command.name", getCommandName(cmd)), attribute.String("command.name", getCommandName(cmd)),
} }, stdioAttributes(streams)...)
return attribute.NewSet(attrList...)
} }
// InstrumentCobraCommands wraps all cobra commands' RunE funcs to set a command duration metric using otel. // 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. // Note: this should be the last func to wrap/modify the PersistentRunE/RunE funcs before command execution.
// //
// can also be used for spans! // 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) meter := getDefaultMeter(mp)
// If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default // If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default
ogPersistentPreRunE := cmd.PersistentPreRunE ogPersistentPreRunE := cmd.PersistentPreRunE
@ -56,7 +56,8 @@ func InstrumentCobraCommands(cmd *cobra.Command, mp metric.MeterProvider) {
} }
cmd.RunE = func(cmd *cobra.Command, args []string) error { cmd.RunE = func(cmd *cobra.Command, args []string) error {
// start the timer as the first step of every cobra command // 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) cmdErr := ogRunE(cmd, args)
stopCobraCmdTimer(cmdErr) stopCobraCmdTimer(cmdErr)
return 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() ctx := cmd.Context()
baseAttrs := BaseMetricAttributes(cmd)
durationCounter, _ := meter.Float64Counter( durationCounter, _ := meter.Float64Counter(
"command.time", "command.time",
metric.WithDescription("Measures the duration of the cobra command"), 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) duration := float64(time.Since(start)) / float64(time.Millisecond)
cmdStatusAttrs := attributesFromError(err) cmdStatusAttrs := attributesFromError(err)
durationCounter.Add(ctx, duration, durationCounter.Add(ctx, duration,
metric.WithAttributeSet(baseAttrs), metric.WithAttributes(attrs...),
metric.WithAttributeSet(attribute.NewSet(cmdStatusAttrs...)), 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 { func attributesFromError(err error) []attribute.KeyValue {
attrs := []attribute.KeyValue{} attrs := []attribute.KeyValue{}
exitCode := 0 exitCode := 0

View File

@ -1,9 +1,16 @@
package command package command
import ( import (
"bytes"
"context"
"io"
"reflect"
"strings"
"testing" "testing"
"github.com/docker/cli/cli/streams"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"go.opentelemetry.io/otel/attribute"
"gotest.tools/v3/assert" "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))
})
}
}

View File

@ -310,7 +310,7 @@ func runDocker(ctx context.Context, dockerCli *command.DockerCli) error {
mp := dockerCli.MeterProvider(ctx) mp := dockerCli.MeterProvider(ctx)
defer mp.Shutdown(ctx) defer mp.Shutdown(ctx)
otel.SetMeterProvider(mp) otel.SetMeterProvider(mp)
command.InstrumentCobraCommands(cmd, mp) dockerCli.InstrumentCobraCommands(cmd, mp)
var envs []string var envs []string
args, os.Args, envs, err = processAliases(dockerCli, cmd, args, os.Args) args, os.Args, envs, err = processAliases(dockerCli, cmd, args, os.Args)