mirror of https://github.com/docker/compose.git
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
|
@ -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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
dockercli "github.com/docker/cli/cli"
|
||||
"github.com/docker/cli/cli-plugins/manager"
|
||||
"github.com/docker/cli/cli-plugins/plugin"
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/docker/compose/v2/cmd/cmdtrace"
|
||||
"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"
|
||||
commands "github.com/docker/compose/v2/cmd/compose"
|
||||
"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/compose"
|
||||
)
|
||||
|
||||
func pluginMain() {
|
||||
plugin.Run(func(dockerCli command.Cli) *cobra.Command {
|
||||
var tracingShutdown tracing.ShutdownFunc
|
||||
var cmdSpan trace.Span
|
||||
|
||||
serviceProxy := api.NewServiceProxy().WithService(compose.NewComposeService(dockerCli))
|
||||
cmd := commands.RootCommand(dockerCli, serviceProxy)
|
||||
originalPreRun := cmd.PersistentPreRunE
|
||||
cmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
|
||||
// initialize the dockerCli instance
|
||||
if err := plugin.PersistentPreRunE(cmd, args); err != nil {
|
||||
return err
|
||||
}
|
||||
// the call to plugin.PersistentPreRunE is what actually
|
||||
// initializes the command.Cli instance, so this is the earliest
|
||||
// that tracing can be practically initialized (in the future,
|
||||
// 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)
|
||||
// TODO(milas): add an env var to enable logging from the
|
||||
// OTel components for debugging purposes
|
||||
_ = cmdtrace.Setup(cmd, dockerCli)
|
||||
|
||||
if originalPreRun != nil {
|
||||
return originalPreRun(cmd, args)
|
||||
|
@ -73,30 +53,6 @@ func pluginMain() {
|
|||
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 {
|
||||
return dockercli.StatusError{
|
||||
StatusCode: compose.CommandSyntaxFailure.ExitCode,
|
||||
|
|
|
@ -24,6 +24,9 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/docker/compose/v2/internal"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
|
||||
"github.com/docker/cli/cli/command"
|
||||
"github.com/moby/buildkit/util/tracing/detect"
|
||||
_ "github.com/moby/buildkit/util/tracing/detect/delegated" //nolint:blank-imports
|
||||
|
@ -103,6 +106,8 @@ func InitProvider(dockerCli command.Cli) (ShutdownFunc, error) {
|
|||
ctx,
|
||||
resource.WithAttributes(
|
||||
semconv.ServiceName("compose"),
|
||||
semconv.ServiceVersion(internal.Version),
|
||||
attribute.String("docker.context", dockerCli.CurrentContext()),
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
|
|
Loading…
Reference in New Issue