diff --git a/cmd/buildx/main.go b/cmd/buildx/main.go index e300ae9c170..69124a3aeb4 100644 --- a/cmd/buildx/main.go +++ b/cmd/buildx/main.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "os" @@ -15,6 +16,7 @@ import ( cliflags "github.com/docker/cli/cli/flags" "github.com/moby/buildkit/solver/errdefs" "github.com/moby/buildkit/util/stack" + "go.opentelemetry.io/otel" //nolint:staticcheck // vendored dependencies may still use this "github.com/containerd/containerd/pkg/seed" @@ -38,10 +40,27 @@ func runStandalone(cmd *command.DockerCli) error { if err := cmd.Initialize(cliflags.NewClientOptions()); err != nil { return err } + defer flushMetrics(cmd) + rootCmd := commands.NewRootCmd(os.Args[0], false, cmd) return rootCmd.Execute() } +// flushMetrics will manually flush metrics from the configured +// meter provider. This is needed when running in standalone mode +// because the meter provider is initialized by the cli library, +// but the mechanism for forcing it to report is not presently +// exposed and not invoked when run in standalone mode. +// There are plans to fix that in the next release, but this is +// needed temporarily until the API for this is more thorough. +func flushMetrics(cmd *command.DockerCli) { + if mp, ok := cmd.MeterProvider().(command.MeterProvider); ok { + if err := mp.ForceFlush(context.Background()); err != nil { + otel.Handle(err) + } + } +} + func runPlugin(cmd *command.DockerCli) error { rootCmd := commands.NewRootCmd("buildx", true, cmd) return plugin.RunPlugin(cmd, rootCmd, manager.Metadata{ diff --git a/commands/build.go b/commands/build.go index ac77bc7a074..c228a40eb82 100644 --- a/commands/build.go +++ b/commands/build.go @@ -269,8 +269,7 @@ func (o *buildOptionsHash) String() string { } func runBuild(ctx context.Context, dockerCli command.Cli, options buildOptions) (err error) { - mp := dockerCli.MeterProvider(ctx) - defer metricutil.Shutdown(ctx, mp) + mp := dockerCli.MeterProvider() ctx, end, err := tracing.TraceCurrentCommand(ctx, "build") if err != nil { diff --git a/go.mod b/go.mod index 120d6707993..92cb3eebfef 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/containerd/typeurl/v2 v2.1.1 github.com/creack/pty v1.1.18 github.com/distribution/reference v0.5.0 - github.com/docker/cli v26.0.1-0.20240410153731-b6c552212837+incompatible // v26.1.0-dev + github.com/docker/cli v26.1.3+incompatible github.com/docker/cli-docs-tool v0.7.0 github.com/docker/docker v26.0.0+incompatible github.com/docker/go-units v0.5.0 diff --git a/go.sum b/go.sum index 3cb469d43b9..0bbb482e7c4 100644 --- a/go.sum +++ b/go.sum @@ -117,8 +117,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/distribution/reference v0.5.0 h1:/FUIFXtfc/x2gpa5/VGfiGLuOIdYa1t65IKK2OFGvA0= github.com/distribution/reference v0.5.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v26.0.1-0.20240410153731-b6c552212837+incompatible h1:KTmSJjZSQM+cpaczHecGsBNlgJtRccef/62pCOeiA9o= -github.com/docker/cli v26.0.1-0.20240410153731-b6c552212837+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v26.1.3+incompatible h1:bUpXT/N0kDE3VUHI2r5VMsYQgi38kYuoC0oL9yt3lqc= +github.com/docker/cli v26.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli-docs-tool v0.7.0 h1:M2Da98Unz2kz3A5d4yeSGbhyOge2mfYSNjAFt01Rw0M= github.com/docker/cli-docs-tool v0.7.0/go.mod h1:zMjqTFCU361PRh8apiXzeAZ1Q/xupbIwTusYpzCXS/o= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= diff --git a/util/metricutil/metric.go b/util/metricutil/metric.go index 6e64067b9ac..10d49720055 100644 --- a/util/metricutil/metric.go +++ b/util/metricutil/metric.go @@ -1,11 +1,7 @@ package metricutil import ( - "context" - "github.com/docker/buildx/version" - "github.com/docker/cli/cli/command" - "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/metric" ) @@ -15,10 +11,3 @@ func Meter(mp metric.MeterProvider) metric.Meter { return mp.Meter(version.Package, metric.WithInstrumentationVersion(version.Version)) } - -// Shutdown invokes Shutdown on the MeterProvider and then reports any error to the OTEL handler. -func Shutdown(ctx context.Context, mp command.MeterProvider) { - if err := mp.Shutdown(ctx); err != nil { - otel.Handle(err) - } -} diff --git a/vendor/github.com/docker/cli/cli-plugins/hooks/template.go b/vendor/github.com/docker/cli/cli-plugins/hooks/template.go index d7e114e1cd4..e6bd69f3877 100644 --- a/vendor/github.com/docker/cli/cli-plugins/hooks/template.go +++ b/vendor/github.com/docker/cli/cli-plugins/hooks/template.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "strconv" + "strings" "text/template" "github.com/spf13/cobra" @@ -71,18 +72,18 @@ func TemplateReplaceArg(i int) string { return fmt.Sprintf(hookTemplateArg, strconv.Itoa(i)) } -func ParseTemplate(hookTemplate string, cmd *cobra.Command) (string, error) { +func ParseTemplate(hookTemplate string, cmd *cobra.Command) ([]string, error) { tmpl := template.New("").Funcs(commandFunctions) tmpl, err := tmpl.Parse(hookTemplate) if err != nil { - return "", err + return nil, err } b := bytes.Buffer{} err = tmpl.Execute(&b, cmd) if err != nil { - return "", err + return nil, err } - return b.String(), nil + return strings.Split(b.String(), "\n"), nil } var ErrHookTemplateParse = errors.New("failed to parse hook template") diff --git a/vendor/github.com/docker/cli/cli-plugins/manager/hooks.go b/vendor/github.com/docker/cli/cli-plugins/manager/hooks.go index b4f8d16ddfb..4d99e679b0d 100644 --- a/vendor/github.com/docker/cli/cli-plugins/manager/hooks.go +++ b/vendor/github.com/docker/cli/cli-plugins/manager/hooks.go @@ -14,31 +14,42 @@ import ( // that plugins declaring support for hooks get passed when // being invoked following a CLI command execution. type HookPluginData struct { - RootCmd string - Flags map[string]string + // RootCmd is a string representing the matching hook configuration + // which is currently being invoked. If a hook for `docker context` is + // configured and the user executes `docker context ls`, the plugin will + // be invoked with `context`. + RootCmd string + Flags map[string]string + CommandError string } -// RunPluginHooks calls the hook subcommand for all present -// CLI plugins that declare support for hooks in their metadata -// and parses/prints their responses. -func RunPluginHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, plugin string, args []string) error { - subCmdName := subCommand.Name() - if plugin != "" { - subCmdName = plugin - } - var flags map[string]string - if plugin == "" { - flags = getCommandFlags(subCommand) - } else { - flags = getNaiveFlags(args) - } - nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, subCmdName, flags) +// RunCLICommandHooks is the entrypoint into the hooks execution flow after +// a main CLI command was executed. It calls the hook subcommand for all +// present CLI plugins that declare support for hooks in their metadata and +// parses/prints their responses. +func RunCLICommandHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, cmdErrorMessage string) { + commandName := strings.TrimPrefix(subCommand.CommandPath(), rootCmd.Name()+" ") + flags := getCommandFlags(subCommand) + + runHooks(dockerCli, rootCmd, subCommand, commandName, flags, cmdErrorMessage) +} + +// RunPluginHooks is the entrypoint for the hooks execution flow +// after a plugin command was just executed by the CLI. +func RunPluginHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, args []string) { + commandName := strings.Join(args, " ") + flags := getNaiveFlags(args) + + runHooks(dockerCli, rootCmd, subCommand, commandName, flags, "") +} + +func runHooks(dockerCli command.Cli, rootCmd, subCommand *cobra.Command, invokedCommand string, flags map[string]string, cmdErrorMessage string) { + nextSteps := invokeAndCollectHooks(dockerCli, rootCmd, subCommand, invokedCommand, flags, cmdErrorMessage) hooks.PrintNextSteps(dockerCli.Err(), nextSteps) - return nil } -func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, hookCmdName string, flags map[string]string) []string { +func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command, subCmdStr string, flags map[string]string, cmdErrorMessage string) []string { pluginsCfg := dockerCli.ConfigFile().Plugins if pluginsCfg == nil { return nil @@ -46,7 +57,8 @@ func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command nextSteps := make([]string, 0, len(pluginsCfg)) for pluginName, cfg := range pluginsCfg { - if !registersHook(cfg, hookCmdName) { + match, ok := pluginMatch(cfg, subCmdStr) + if !ok { continue } @@ -55,7 +67,11 @@ func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command continue } - hookReturn, err := p.RunHook(hookCmdName, flags) + hookReturn, err := p.RunHook(HookPluginData{ + RootCmd: match, + Flags: flags, + CommandError: cmdErrorMessage, + }) if err != nil { // skip misbehaving plugins, but don't halt execution continue @@ -76,23 +92,46 @@ func invokeAndCollectHooks(dockerCli command.Cli, rootCmd, subCmd *cobra.Command if err != nil { continue } - nextSteps = append(nextSteps, processedHook) + nextSteps = append(nextSteps, processedHook...) } return nextSteps } -func registersHook(pluginCfg map[string]string, subCmdName string) bool { - hookCmdStr, ok := pluginCfg["hooks"] - if !ok { - return false +// pluginMatch takes a plugin configuration and a string representing the +// command being executed (such as 'image ls' – the root 'docker' is omitted) +// and, if the configuration includes a hook for the invoked command, returns +// the configured hook string. +func pluginMatch(pluginCfg map[string]string, subCmd string) (string, bool) { + configuredPluginHooks, ok := pluginCfg["hooks"] + if !ok || configuredPluginHooks == "" { + return "", false } - commands := strings.Split(hookCmdStr, ",") + + commands := strings.Split(configuredPluginHooks, ",") for _, hookCmd := range commands { - if hookCmd == subCmdName { - return true + if hookMatch(hookCmd, subCmd) { + return hookCmd, true } } - return false + + return "", false +} + +func hookMatch(hookCmd, subCmd string) bool { + hookCmdTokens := strings.Split(hookCmd, " ") + subCmdTokens := strings.Split(subCmd, " ") + + if len(hookCmdTokens) > len(subCmdTokens) { + return false + } + + for i, v := range hookCmdTokens { + if v != subCmdTokens[i] { + return false + } + } + + return true } func getCommandFlags(cmd *cobra.Command) map[string]string { diff --git a/vendor/github.com/docker/cli/cli-plugins/manager/manager.go b/vendor/github.com/docker/cli/cli-plugins/manager/manager.go index d3b7b7b5260..031b04f6bb4 100644 --- a/vendor/github.com/docker/cli/cli-plugins/manager/manager.go +++ b/vendor/github.com/docker/cli/cli-plugins/manager/manager.go @@ -240,8 +240,7 @@ func PluginRunCommand(dockerCli command.Cli, name string, rootcmd *cobra.Command cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, ReexecEnvvar+"="+os.Args[0]) + cmd.Env = append(cmd.Environ(), ReexecEnvvar+"="+os.Args[0]) cmd.Env = appendPluginResourceAttributesEnvvar(cmd.Env, rootcmd, plugin) return cmd, nil diff --git a/vendor/github.com/docker/cli/cli-plugins/manager/plugin.go b/vendor/github.com/docker/cli/cli-plugins/manager/plugin.go index 88600f4e592..2cffafaccab 100644 --- a/vendor/github.com/docker/cli/cli-plugins/manager/plugin.go +++ b/vendor/github.com/docker/cli/cli-plugins/manager/plugin.go @@ -2,6 +2,7 @@ package manager import ( "encoding/json" + "os" "os/exec" "path/filepath" "regexp" @@ -104,16 +105,16 @@ func newPlugin(c Candidate, cmds []*cobra.Command) (Plugin, error) { // RunHook executes the plugin's hooks command // and returns its unprocessed output. -func (p *Plugin) RunHook(cmdName string, flags map[string]string) ([]byte, error) { - hDataBytes, err := json.Marshal(HookPluginData{ - RootCmd: cmdName, - Flags: flags, - }) +func (p *Plugin) RunHook(hookData HookPluginData) ([]byte, error) { + hDataBytes, err := json.Marshal(hookData) if err != nil { return nil, wrapAsPluginError(err, "failed to marshall hook data") } - hookCmdOutput, err := exec.Command(p.Path, p.Name, HookSubcommandName, string(hDataBytes)).Output() + pCmd := exec.Command(p.Path, p.Name, HookSubcommandName, string(hDataBytes)) + pCmd.Env = os.Environ() + pCmd.Env = append(pCmd.Env, ReexecEnvvar+"="+os.Args[0]) + hookCmdOutput, err := pCmd.Output() if err != nil { return nil, wrapAsPluginError(err, "failed to execute plugin hook subcommand") } diff --git a/vendor/github.com/docker/cli/cli-plugins/plugin/plugin.go b/vendor/github.com/docker/cli/cli-plugins/plugin/plugin.go index c04edbb549e..05dd2e7c9af 100644 --- a/vendor/github.com/docker/cli/cli-plugins/plugin/plugin.go +++ b/vendor/github.com/docker/cli/cli-plugins/plugin/plugin.go @@ -52,6 +52,24 @@ func RunPlugin(dockerCli *command.DockerCli, plugin *cobra.Command, meta manager opts = append(opts, withPluginClientConn(plugin.Name())) } err = tcmd.Initialize(opts...) + ogRunE := cmd.RunE + if ogRunE == nil { + ogRun := cmd.Run + // necessary because error will always be nil here + // see: https://github.com/golangci/golangci-lint/issues/1379 + //nolint:unparam + ogRunE = func(cmd *cobra.Command, args []string) error { + ogRun(cmd, args) + return nil + } + cmd.Run = nil + } + cmd.RunE = func(cmd *cobra.Command, args []string) error { + stopInstrumentation := dockerCli.StartInstrumentation(cmd) + err := ogRunE(cmd, args) + stopInstrumentation(err) + return err + } }) return err } diff --git a/vendor/github.com/docker/cli/cli/command/cli.go b/vendor/github.com/docker/cli/cli/command/cli.go index a8b4c88bcad..28253f8c16c 100644 --- a/vendor/github.com/docker/cli/cli/command/cli.go +++ b/vendor/github.com/docker/cli/cli/command/cli.go @@ -273,6 +273,11 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) return ResolveDefaultContext(cli.options, cli.contextStoreConfig) }, } + + // TODO(krissetto): pass ctx to the funcs instead of using this + cli.createGlobalMeterProvider(cli.baseCtx) + cli.createGlobalTracerProvider(cli.baseCtx) + return nil } diff --git a/vendor/github.com/docker/cli/cli/command/telemetry.go b/vendor/github.com/docker/cli/cli/command/telemetry.go index 92e80420ac4..d18d94d4a99 100644 --- a/vendor/github.com/docker/cli/cli/command/telemetry.go +++ b/vendor/github.com/docker/cli/cli/command/telemetry.go @@ -41,35 +41,25 @@ type TelemetryClient interface { // each time this function is invoked. Resource() *resource.Resource - // TracerProvider returns a TracerProvider. This TracerProvider will be configured - // with the default tracing components for a CLI program along with any options given - // for the SDK. - TracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) TracerProvider + // TracerProvider returns the currently initialized TracerProvider. This TracerProvider will be configured + // with the default tracing components for a CLI program + TracerProvider() trace.TracerProvider - // MeterProvider returns a MeterProvider. This MeterProvider will be configured - // with the default metric components for a CLI program along with any options given - // for the SDK. - MeterProvider(ctx context.Context, opts ...sdkmetric.Option) MeterProvider + // MeterProvider returns the currently initialized MeterProvider. This MeterProvider will be configured + // with the default metric components for a CLI program + MeterProvider() metric.MeterProvider } func (cli *DockerCli) Resource() *resource.Resource { return cli.res.Get() } -func (cli *DockerCli) TracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) TracerProvider { - allOpts := make([]sdktrace.TracerProviderOption, 0, len(opts)+2) - allOpts = append(allOpts, sdktrace.WithResource(cli.Resource())) - allOpts = append(allOpts, dockerSpanExporter(ctx, cli)...) - allOpts = append(allOpts, opts...) - return sdktrace.NewTracerProvider(allOpts...) +func (cli *DockerCli) TracerProvider() trace.TracerProvider { + return otel.GetTracerProvider() } -func (cli *DockerCli) MeterProvider(ctx context.Context, opts ...sdkmetric.Option) MeterProvider { - allOpts := make([]sdkmetric.Option, 0, len(opts)+2) - allOpts = append(allOpts, sdkmetric.WithResource(cli.Resource())) - allOpts = append(allOpts, dockerMetricExporter(ctx, cli)...) - allOpts = append(allOpts, opts...) - return sdkmetric.NewMeterProvider(allOpts...) +func (cli *DockerCli) MeterProvider() metric.MeterProvider { + return otel.GetMeterProvider() } // WithResourceOptions configures additional options for the default resource. The default @@ -122,6 +112,28 @@ func (r *telemetryResource) init() { r.opts = nil } +// createGlobalMeterProvider creates a new MeterProvider from the initialized DockerCli struct +// with the given options and sets it as the global meter provider +func (cli *DockerCli) createGlobalMeterProvider(ctx context.Context, opts ...sdkmetric.Option) { + allOpts := make([]sdkmetric.Option, 0, len(opts)+2) + allOpts = append(allOpts, sdkmetric.WithResource(cli.Resource())) + allOpts = append(allOpts, dockerMetricExporter(ctx, cli)...) + allOpts = append(allOpts, opts...) + mp := sdkmetric.NewMeterProvider(allOpts...) + otel.SetMeterProvider(mp) +} + +// createGlobalTracerProvider creates a new TracerProvider from the initialized DockerCli struct +// with the given options and sets it as the global tracer provider +func (cli *DockerCli) createGlobalTracerProvider(ctx context.Context, opts ...sdktrace.TracerProviderOption) { + allOpts := make([]sdktrace.TracerProviderOption, 0, len(opts)+2) + allOpts = append(allOpts, sdktrace.WithResource(cli.Resource())) + allOpts = append(allOpts, dockerSpanExporter(ctx, cli)...) + allOpts = append(allOpts, opts...) + tp := sdktrace.NewTracerProvider(allOpts...) + otel.SetTracerProvider(tp) +} + func defaultResourceOptions() []resource.Option { return []resource.Option{ resource.WithDetectors(serviceNameDetector{}), @@ -174,11 +186,6 @@ func newCLIReader(exp sdkmetric.Exporter) sdkmetric.Reader { } func (r *cliReader) Shutdown(ctx context.Context) error { - var rm metricdata.ResourceMetrics - if err := r.Reader.Collect(ctx, &rm); err != nil { - return err - } - // Place a pretty tight constraint on the actual reporting. // We don't want CLI metrics to prevent the CLI from exiting // so if there's some kind of issue we need to abort pretty @@ -186,6 +193,15 @@ func (r *cliReader) Shutdown(ctx context.Context) error { ctx, cancel := context.WithTimeout(ctx, exportTimeout) defer cancel() + return r.ForceFlush(ctx) +} + +func (r *cliReader) ForceFlush(ctx context.Context) error { + var rm metricdata.ResourceMetrics + if err := r.Reader.Collect(ctx, &rm); err != nil { + return err + } + return r.exporter.Export(ctx, &rm) } diff --git a/vendor/github.com/docker/cli/cli/command/telemetry_docker.go b/vendor/github.com/docker/cli/cli/command/telemetry_docker.go index 9f6253af481..5dc72e2bb52 100644 --- a/vendor/github.com/docker/cli/cli/command/telemetry_docker.go +++ b/vendor/github.com/docker/cli/cli/command/telemetry_docker.go @@ -41,24 +41,20 @@ func dockerExporterOTLPEndpoint(cli Cli) (endpoint string, secure bool) { otelCfg = m[otelContextFieldName] } - if otelCfg == nil { - return "", false + if otelCfg != nil { + otelMap, ok := otelCfg.(map[string]any) + if !ok { + otel.Handle(errors.Errorf( + "unexpected type for field %q: %T (expected: %T)", + otelContextFieldName, + otelCfg, + otelMap, + )) + } + // keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/ + endpoint, _ = otelMap[otelExporterOTLPEndpoint].(string) } - otelMap, ok := otelCfg.(map[string]any) - if !ok { - otel.Handle(errors.Errorf( - "unexpected type for field %q: %T (expected: %T)", - otelContextFieldName, - otelCfg, - otelMap, - )) - return "", false - } - - // keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/ - endpoint, _ = otelMap[otelExporterOTLPEndpoint].(string) - // Override with env var value if it exists AND IS SET // (ignore otel defaults for this override when the key exists but is empty) if override := os.Getenv(debugEnvVarPrefix + otelExporterOTLPEndpoint); override != "" { diff --git a/vendor/github.com/docker/cli/cli/command/telemetry_utils.go b/vendor/github.com/docker/cli/cli/command/telemetry_utils.go index 5749b4f18ce..87b976ce69a 100644 --- a/vendor/github.com/docker/cli/cli/command/telemetry_utils.go +++ b/vendor/github.com/docker/cli/cli/command/telemetry_utils.go @@ -26,8 +26,7 @@ func BaseCommandAttributes(cmd *cobra.Command, streams Streams) []attribute.KeyV // Note: this should be the last func to wrap/modify the PersistentRunE/RunE funcs before command execution. // // can also be used for spans! -func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command, mp metric.MeterProvider) { - meter := getDefaultMeter(mp) +func (cli *DockerCli) InstrumentCobraCommands(ctx context.Context, cmd *cobra.Command) { // If PersistentPreRunE is nil, make it execute PersistentPreRun and return nil by default ogPersistentPreRunE := cmd.PersistentPreRunE if ogPersistentPreRunE == nil { @@ -55,10 +54,9 @@ func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command, mp metric.Mete } cmd.RunE = func(cmd *cobra.Command, args []string) error { // start the timer as the first step of every cobra command - baseAttrs := BaseCommandAttributes(cmd, cli) - stopCobraCmdTimer := startCobraCommandTimer(cmd, meter, baseAttrs) + stopInstrumentation := cli.StartInstrumentation(cmd) cmdErr := ogRunE(cmd, args) - stopCobraCmdTimer(cmdErr) + stopInstrumentation(cmdErr) return cmdErr } @@ -66,8 +64,17 @@ func (cli *DockerCli) InstrumentCobraCommands(cmd *cobra.Command, mp metric.Mete } } -func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter, attrs []attribute.KeyValue) func(err error) { - ctx := cmd.Context() +// StartInstrumentation instruments CLI commands with the individual metrics and spans configured. +// It's the main command OTel utility, and new command-related metrics should be added to it. +// It should be called immediately before command execution, and returns a stopInstrumentation function +// that must be called with the error resulting from the command execution. +func (cli *DockerCli) StartInstrumentation(cmd *cobra.Command) (stopInstrumentation func(error)) { + baseAttrs := BaseCommandAttributes(cmd, cli) + return startCobraCommandTimer(cli.MeterProvider(), baseAttrs) +} + +func startCobraCommandTimer(mp metric.MeterProvider, attrs []attribute.KeyValue) func(err error) { + meter := getDefaultMeter(mp) durationCounter, _ := meter.Float64Counter( "command.time", metric.WithDescription("Measures the duration of the cobra command"), @@ -76,12 +83,20 @@ func startCobraCommandTimer(cmd *cobra.Command, meter metric.Meter, attrs []attr start := time.Now() return func(err error) { + // Use a new context for the export so that the command being cancelled + // doesn't affect the metrics, and we get metrics for cancelled commands. + ctx, cancel := context.WithTimeout(context.Background(), exportTimeout) + defer cancel() + duration := float64(time.Since(start)) / float64(time.Millisecond) cmdStatusAttrs := attributesFromError(err) durationCounter.Add(ctx, duration, metric.WithAttributes(attrs...), metric.WithAttributes(cmdStatusAttrs...), ) + if mp, ok := mp.(MeterProvider); ok { + mp.ForceFlush(ctx) + } } } diff --git a/vendor/modules.txt b/vendor/modules.txt index 8ced3bc33d7..c0f8e2819d5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -215,7 +215,7 @@ github.com/davecgh/go-spew/spew # github.com/distribution/reference v0.5.0 ## explicit; go 1.20 github.com/distribution/reference -# github.com/docker/cli v26.0.1-0.20240410153731-b6c552212837+incompatible +# github.com/docker/cli v26.1.3+incompatible ## explicit github.com/docker/cli/cli github.com/docker/cli/cli-plugins/hooks