mirror of
https://github.com/docker/compose.git
synced 2025-07-22 13:14:29 +02:00
otel: refactor root command span reporting
* Move all the initialization code out of `main.go` * Ensure spans are reported when there's an error with the command * Attach the Compose version & active Docker context to the resource instead of the span * Name the root CLI span `cli/<cmd>` for clarity and grab the full subcommand path (e.g. `alpha-viz` instead of just `viz`) Signed-off-by: Milas Bowman <milas.bowman@docker.com>
This commit is contained in:
parent
a2ce602f6c
commit
e1f8603a62
131
cmd/cmdtrace/cmd_span.go
Normal file
131
cmd/cmdtrace/cmd_span.go
Normal file
@ -0,0 +1,131 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 Docker Compose CLI authors
|
||||||
|
|
||||||
|
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 cmdtrace
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
dockercli "github.com/docker/cli/cli"
|
||||||
|
"github.com/docker/cli/cli/command"
|
||||||
|
commands "github.com/docker/compose/v2/cmd/compose"
|
||||||
|
"github.com/docker/compose/v2/internal/tracing"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
"go.opentelemetry.io/otel/codes"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Setup should be called as part of the command's PersistentPreRunE
|
||||||
|
// as soon as possible after initializing the dockerCli.
|
||||||
|
//
|
||||||
|
// It initializes the tracer for the CLI using both auto-detection
|
||||||
|
// from the Docker context metadata as well as standard OTEL_ env
|
||||||
|
// vars, creates a root span for the command, and wraps the actual
|
||||||
|
// command invocation to ensure the span is properly finalized and
|
||||||
|
// exported before exit.
|
||||||
|
func Setup(cmd *cobra.Command, dockerCli command.Cli) error {
|
||||||
|
tracingShutdown, err := tracing.InitTracing(dockerCli)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("initializing tracing: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := cmd.Context()
|
||||||
|
ctx, cmdSpan := tracing.Tracer.Start(
|
||||||
|
ctx,
|
||||||
|
"cli/"+strings.Join(commandName(cmd), "-"),
|
||||||
|
)
|
||||||
|
cmd.SetContext(ctx)
|
||||||
|
wrapRunE(cmd, cmdSpan, tracingShutdown)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrapRunE injects a wrapper function around the command's actual RunE (or Run)
|
||||||
|
// method. This is necessary to capture the command result for reporting as well
|
||||||
|
// as flushing any spans before exit.
|
||||||
|
//
|
||||||
|
// Unfortunately, PersistentPostRun(E) can't be used for this purpose because it
|
||||||
|
// only runs if RunE does _not_ return an error, but this should run unconditionally.
|
||||||
|
func wrapRunE(c *cobra.Command, cmdSpan trace.Span, tracingShutdown tracing.ShutdownFunc) {
|
||||||
|
origRunE := c.RunE
|
||||||
|
if origRunE == nil {
|
||||||
|
origRun := c.Run
|
||||||
|
//nolint:unparam // wrapper function for RunE, always returns nil by design
|
||||||
|
origRunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
origRun(cmd, args)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.Run = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.RunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
cmdErr := origRunE(cmd, args)
|
||||||
|
if cmdSpan != nil {
|
||||||
|
if cmdErr != nil && !errors.Is(cmdErr, context.Canceled) {
|
||||||
|
// default exit code is 1 if a more descriptive error
|
||||||
|
// wasn't returned
|
||||||
|
exitCode := 1
|
||||||
|
var statusErr dockercli.StatusError
|
||||||
|
if errors.As(cmdErr, &statusErr) {
|
||||||
|
exitCode = statusErr.StatusCode
|
||||||
|
}
|
||||||
|
cmdSpan.SetStatus(codes.Error, "CLI command returned error")
|
||||||
|
cmdSpan.RecordError(cmdErr, trace.WithAttributes(
|
||||||
|
attribute.Int("exit_code", exitCode),
|
||||||
|
))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
cmdSpan.SetStatus(codes.Ok, "")
|
||||||
|
}
|
||||||
|
cmdSpan.End()
|
||||||
|
}
|
||||||
|
if tracingShutdown != nil {
|
||||||
|
// use background for root context because the cmd's context might have
|
||||||
|
// been canceled already
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
// TODO(milas): add an env var to enable logging from the
|
||||||
|
// OTel components for debugging purposes
|
||||||
|
_ = tracingShutdown(ctx)
|
||||||
|
}
|
||||||
|
return cmdErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// commandName returns the path components for a given command.
|
||||||
|
//
|
||||||
|
// The root Compose command and anything before (i.e. "docker")
|
||||||
|
// are not included.
|
||||||
|
//
|
||||||
|
// For example:
|
||||||
|
// - docker compose alpha watch -> [alpha, watch]
|
||||||
|
// - docker-compose up -> [up]
|
||||||
|
func commandName(cmd *cobra.Command) []string {
|
||||||
|
var name []string
|
||||||
|
for c := cmd; c != nil; c = c.Parent() {
|
||||||
|
if c.Name() == commands.PluginName {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
name = append(name, c.Name())
|
||||||
|
}
|
||||||
|
sort.Sort(sort.Reverse(sort.StringSlice(name)))
|
||||||
|
return name
|
||||||
|
}
|
54
cmd/main.go
54
cmd/main.go
@ -17,55 +17,35 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
|
||||||
|
|
||||||
dockercli "github.com/docker/cli/cli"
|
dockercli "github.com/docker/cli/cli"
|
||||||
"github.com/docker/cli/cli-plugins/manager"
|
"github.com/docker/cli/cli-plugins/manager"
|
||||||
"github.com/docker/cli/cli-plugins/plugin"
|
"github.com/docker/cli/cli-plugins/plugin"
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/pkg/errors"
|
"github.com/docker/compose/v2/cmd/cmdtrace"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
"go.opentelemetry.io/otel/attribute"
|
|
||||||
"go.opentelemetry.io/otel/codes"
|
|
||||||
"go.opentelemetry.io/otel/trace"
|
|
||||||
|
|
||||||
"github.com/docker/compose/v2/cmd/compatibility"
|
"github.com/docker/compose/v2/cmd/compatibility"
|
||||||
commands "github.com/docker/compose/v2/cmd/compose"
|
commands "github.com/docker/compose/v2/cmd/compose"
|
||||||
"github.com/docker/compose/v2/internal"
|
"github.com/docker/compose/v2/internal"
|
||||||
"github.com/docker/compose/v2/internal/tracing"
|
|
||||||
"github.com/docker/compose/v2/pkg/api"
|
"github.com/docker/compose/v2/pkg/api"
|
||||||
"github.com/docker/compose/v2/pkg/compose"
|
"github.com/docker/compose/v2/pkg/compose"
|
||||||
)
|
)
|
||||||
|
|
||||||
func pluginMain() {
|
func pluginMain() {
|
||||||
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
|
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
|
||||||
var tracingShutdown tracing.ShutdownFunc
|
|
||||||
var cmdSpan trace.Span
|
|
||||||
|
|
||||||
serviceProxy := api.NewServiceProxy().WithService(compose.NewComposeService(dockerCli))
|
serviceProxy := api.NewServiceProxy().WithService(compose.NewComposeService(dockerCli))
|
||||||
cmd := commands.RootCommand(dockerCli, serviceProxy)
|
cmd := commands.RootCommand(dockerCli, serviceProxy)
|
||||||
originalPreRun := cmd.PersistentPreRunE
|
originalPreRun := cmd.PersistentPreRunE
|
||||||
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||||
|
// initialize the dockerCli instance
|
||||||
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
|
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// the call to plugin.PersistentPreRunE is what actually
|
// TODO(milas): add an env var to enable logging from the
|
||||||
// initializes the command.Cli instance, so this is the earliest
|
// OTel components for debugging purposes
|
||||||
// that tracing can be practically initialized (in the future,
|
_ = cmdtrace.Setup(cmd, dockerCli)
|
||||||
// this could ideally happen in coordination with docker/cli)
|
|
||||||
tracingShutdown, _ = tracing.InitTracing(dockerCli)
|
|
||||||
|
|
||||||
ctx := cmd.Context()
|
|
||||||
ctx, cmdSpan = tracing.Tracer.Start(
|
|
||||||
ctx, "cli/"+cmd.Name(),
|
|
||||||
trace.WithAttributes(
|
|
||||||
attribute.String("compose.version", internal.Version),
|
|
||||||
attribute.String("docker.context", dockerCli.CurrentContext()),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
cmd.SetContext(ctx)
|
|
||||||
|
|
||||||
if originalPreRun != nil {
|
if originalPreRun != nil {
|
||||||
return originalPreRun(cmd, args)
|
return originalPreRun(cmd, args)
|
||||||
@ -73,30 +53,6 @@ func pluginMain() {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// manually wrap RunE instead of using PersistentPostRunE because the
|
|
||||||
// latter only runs when RunE does _not_ return an error, but the
|
|
||||||
// tracing clean-up logic should always be invoked
|
|
||||||
originalPersistentPostRunE := cmd.PersistentPostRunE
|
|
||||||
cmd.PersistentPostRunE = func(cmd *cobra.Command, args []string) (err error) {
|
|
||||||
defer func() {
|
|
||||||
if cmdSpan != nil {
|
|
||||||
if err != nil && !errors.Is(err, context.Canceled) {
|
|
||||||
cmdSpan.SetStatus(codes.Error, "CLI command returned error")
|
|
||||||
cmdSpan.RecordError(err)
|
|
||||||
}
|
|
||||||
cmdSpan.End()
|
|
||||||
}
|
|
||||||
if tracingShutdown != nil {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
|
||||||
defer cancel()
|
|
||||||
_ = tracingShutdown(ctx)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
if originalPersistentPostRunE != nil {
|
|
||||||
return originalPersistentPostRunE(cmd, args)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
|
cmd.SetFlagErrorFunc(func(c *cobra.Command, err error) error {
|
||||||
return dockercli.StatusError{
|
return dockercli.StatusError{
|
||||||
StatusCode: compose.CommandSyntaxFailure.ExitCode,
|
StatusCode: compose.CommandSyntaxFailure.ExitCode,
|
||||||
|
@ -24,6 +24,9 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/docker/compose/v2/internal"
|
||||||
|
"go.opentelemetry.io/otel/attribute"
|
||||||
|
|
||||||
"github.com/docker/cli/cli/command"
|
"github.com/docker/cli/cli/command"
|
||||||
"github.com/moby/buildkit/util/tracing/detect"
|
"github.com/moby/buildkit/util/tracing/detect"
|
||||||
_ "github.com/moby/buildkit/util/tracing/detect/delegated" //nolint:blank-imports
|
_ "github.com/moby/buildkit/util/tracing/detect/delegated" //nolint:blank-imports
|
||||||
@ -103,6 +106,8 @@ func InitProvider(dockerCli command.Cli) (ShutdownFunc, error) {
|
|||||||
ctx,
|
ctx,
|
||||||
resource.WithAttributes(
|
resource.WithAttributes(
|
||||||
semconv.ServiceName("compose"),
|
semconv.ServiceName("compose"),
|
||||||
|
semconv.ServiceVersion(internal.Version),
|
||||||
|
attribute.String("docker.context", dockerCli.CurrentContext()),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user