From 36e496cf0e472f367360fcb32d484926ab97e8a7 Mon Sep 17 00:00:00 2001 From: "Jonathan A. Sternberg" Date: Mon, 9 Nov 2026 10:42:40 -0600 Subject: [PATCH] refactor(tracing): use cli provided tracer Use the docker/cli library provided tracer rather than create and manage our own. This has the benefit of less code and it puts the correct setup of the docker telemetry tracer into the same one the core cli and buildx use. This also allows compose to use the same debug environment variables that the cli and buildx use for testing telemetry. Signed-off-by: Jonathan A. Sternberg --- cmd/cmdtrace/cmd_span.go | 1 + cmd/main.go | 13 +++ go.mod | 10 +-- go.sum | 8 +- internal/tracing/docker_context.go | 123 ----------------------------- internal/tracing/mux.go | 69 ---------------- internal/tracing/tracing.go | 108 +------------------------ internal/tracing/tracing_test.go | 60 -------------- 8 files changed, 23 insertions(+), 369 deletions(-) delete mode 100644 internal/tracing/docker_context.go delete mode 100644 internal/tracing/mux.go delete mode 100644 internal/tracing/tracing_test.go diff --git a/cmd/cmdtrace/cmd_span.go b/cmd/cmdtrace/cmd_span.go index a08a158b480..b41968dcd85 100644 --- a/cmd/cmdtrace/cmd_span.go +++ b/cmd/cmdtrace/cmd_span.go @@ -60,6 +60,7 @@ func Setup(cmd *cobra.Command, dockerCli command.Cli, args []string) error { cmdSpan.SetAttributes( attribute.StringSlice("cli.flags", getFlags(cmd.Flags())), attribute.Bool("cli.isatty", dockerCli.In().IsTerminal()), + attribute.String("docker.context", dockerCli.CurrentContext()), ) cmd.SetContext(ctx) diff --git a/cmd/main.go b/cmd/main.go index 7534f0741cf..026b4d11e60 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -25,6 +25,8 @@ import ( "github.com/docker/cli/cli/command" "github.com/sirupsen/logrus" "github.com/spf13/cobra" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/sdk/resource" "github.com/docker/compose/v5/cmd/cmdtrace" "github.com/docker/compose/v5/cmd/compatibility" @@ -35,6 +37,11 @@ import ( "github.com/docker/compose/v5/pkg/compose" ) +var ( + serviceNameKey = attribute.Key("service.name") + serviceVersionKey = attribute.Key("service.version") +) + func pluginMain() { plugin.Run( func(cli command.Cli) *cobra.Command { @@ -79,6 +86,12 @@ func pluginMain() { Version: internal.Version, }, command.WithUserAgent("compose/"+internal.Version), + command.WithResourceOptions( + resource.WithAttributes( + serviceNameKey.String("compose"), + serviceVersionKey.String(internal.Version), + ), + ), ) } diff --git a/go.mod b/go.mod index 26ac8044e29..af9d87cee71 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/containerd/platforms v1.0.0-rc.5 github.com/creack/pty v1.1.24 github.com/distribution/reference v0.6.0 - github.com/docker/cli v29.9.3+incompatible + github.com/docker/cli v29.10.0-rc.1+incompatible github.com/docker/cli-docs-tool v0.11.0 github.com/docker/go-units v0.5.0 github.com/docker/secrets-engine/client v0.2.2 @@ -45,8 +45,6 @@ require ( github.com/tonistiigi/go-csvvalue v0.0.0-20240814133006-030d3b2625d0 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.71.0 go.opentelemetry.io/otel v1.46.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 go.opentelemetry.io/otel/metric v1.46.0 go.opentelemetry.io/otel/sdk v1.46.0 go.opentelemetry.io/otel/trace v1.46.0 @@ -55,7 +53,6 @@ require ( go.yaml.in/yaml/v4 v4.0.0 golang.org/x/sync v0.24.0 golang.org/x/sys v0.49.0 - google.golang.org/grpc v1.85.0 gotest.tools/v3 v3.5.2 tags.cncf.io/container-device-interface v1.1.1 ) @@ -116,8 +113,8 @@ require ( go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.71.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.71.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.46.0 // indirect go.opentelemetry.io/proto/otlp v1.11.0 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect @@ -128,6 +125,7 @@ require ( golang.org/x/time v0.16.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260825221802-da73d73af1c5 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260825221802-da73d73af1c5 // indirect + google.golang.org/grpc v1.85.0 // indirect google.golang.org/protobuf v1.36.12 // indirect gopkg.in/ini.v1 v1.67.3 // indirect ) diff --git a/go.sum b/go.sum index e9ad1d3f381..36996726c07 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,8 @@ github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5Qvfr github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI= github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/docker/cli v29.9.3+incompatible h1:SBaMp7ecohadEHMDrUJg22t9I3awqsAVhnJ7GOcWpfE= -github.com/docker/cli v29.9.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.10.0-rc.1+incompatible h1:1aGWJ1BVjBvQya2hUXOfUMzYkBItufbFe4ZcYaJz1XQ= +github.com/docker/cli v29.10.0-rc.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/cli-docs-tool v0.11.0 h1:7d8QARFb7QEobizqxmEM7fOteZEHwH/zWgHQtHZEcfE= github.com/docker/cli-docs-tool v0.11.0/go.mod h1:ma8BKiisUo8D6W05XEYIh3oa1UbgrZhi1nowyKFJa8Q= github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= @@ -272,14 +272,10 @@ go.opentelemetry.io/otel v1.46.0 h1:FHt5/CDyVxi/8IM1CH7VE/rRgq3kLHa2mSTVMO8AWyc= go.opentelemetry.io/otel v1.46.0/go.mod h1:Gj3SEScelsNC45tp4nSxRYlS+f5iez7W8XPMCt905kE= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0 h1:qkDYCAFiZXLcs1L4aY+tP2wguQ4kURANqHOQMA2et2s= go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.46.0/go.mod h1:tkipS4DRzmpAmvg+Gw4++O1IdDq6TVDnvnYU6cmbQVs= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0 h1:AP23h/mFgb/lc7tdck1Kfn9qxsM8TAeNPCU5C3pzaps= -go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v1.46.0/go.mod h1:K4EqCe1b4kGk5WR690ntg9LaBfsPoV32FwthbyoptuA= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0 h1:OFnwLJr+pF3iHrlGSzbxyuo6/6HyBlnlN1CWEJmBVcw= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.46.0/go.mod h1:716wFneO0ov19A2beH5hjfh9AK5z/VWNAtDijp1Y0/g= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0 h1:w53CDeOA/Kurp7yRsegSr6pbbr759dOvJ+yNmWM6Hxs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.46.0/go.mod h1:BOmGMCbAtvcJiSJ+hLuhgPLdDbimnraSl8irz3iY8sY= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0 h1:KrC1YrQeSt46ITMWAbgQx1M1eV1/1TKzttrBzymPmss= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.46.0/go.mod h1:zDSEzoEqsOrgBeGvH66KRgxh90VonFyJqBHA0Pk3+rM= go.opentelemetry.io/otel/metric v1.46.0 h1:yBnkXvgV7AXFILZc5K6IZe/CBFF3OS7BJ8ov6/lj0K8= go.opentelemetry.io/otel/metric v1.46.0/go.mod h1:iPmdWqifKUdzziPkvvzIJXITl56fQx2mGM/DHLB3/2o= go.opentelemetry.io/otel/metric/x v0.68.0 h1:TA/cBT23D3MnxYPwHL7YFOdYGdx0A0v+s7Mzotpd1dU= diff --git a/internal/tracing/docker_context.go b/internal/tracing/docker_context.go deleted file mode 100644 index 5d5367b0231..00000000000 --- a/internal/tracing/docker_context.go +++ /dev/null @@ -1,123 +0,0 @@ -/* - 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 tracing - -import ( - "fmt" - "os" - - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/context/store" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" - "google.golang.org/grpc" - "google.golang.org/grpc/credentials/insecure" - - "github.com/docker/compose/v5/internal/memnet" -) - -const otelConfigFieldName = "otel" - -// traceClientFromDockerContext creates a gRPC OTLP client based on metadata -// from the active Docker CLI context. -func traceClientFromDockerContext(dockerCli command.Cli, otelEnv envMap) (otlptrace.Client, error) { - // attempt to extract an OTEL config from the Docker context to enable - // automatic integration with Docker Desktop; - cfg, err := ConfigFromDockerContext(dockerCli.ContextStore(), dockerCli.CurrentContext()) - if err != nil { - return nil, fmt.Errorf("loading otel config from docker context metadata: %w", err) - } - - if cfg.Endpoint == "" { - return nil, nil - } - - // HACK: unfortunately _all_ public OTEL initialization functions - // implicitly read from the OS env, so temporarily unset them all and - // restore afterwards - defer func() { - for k, v := range otelEnv { - if err := os.Setenv(k, v); err != nil { - panic(fmt.Errorf("restoring env for %q: %w", k, err)) - } - } - }() - for k := range otelEnv { - if err := os.Unsetenv(k); err != nil { - return nil, fmt.Errorf("stashing env for %q: %w", k, err) - } - } - - conn, err := grpc.NewClient(cfg.Endpoint, - grpc.WithContextDialer(memnet.DialEndpoint), - // this dial is restricted to using a local Unix socket / named pipe, - // so there is no need for TLS - grpc.WithTransportCredentials(insecure.NewCredentials()), - ) - if err != nil { - return nil, fmt.Errorf("initializing otel connection from docker context metadata: %w", err) - } - - client := otlptracegrpc.NewClient(otlptracegrpc.WithGRPCConn(conn)) - return client, nil -} - -// ConfigFromDockerContext inspects extra metadata included as part of the -// specified Docker context to try and extract a valid OTLP client configuration. -func ConfigFromDockerContext(st store.Store, name string) (OTLPConfig, error) { - meta, err := st.GetMetadata(name) - if err != nil { - return OTLPConfig{}, err - } - - var otelCfg any - switch m := meta.Metadata.(type) { - case command.DockerContext: - otelCfg = m.AdditionalFields[otelConfigFieldName] - case map[string]any: - otelCfg = m[otelConfigFieldName] - } - if otelCfg == nil { - return OTLPConfig{}, nil - } - - otelMap, ok := otelCfg.(map[string]any) - if !ok { - return OTLPConfig{}, fmt.Errorf( - "unexpected type for field %q: %T (expected: %T)", - otelConfigFieldName, - otelCfg, - otelMap, - ) - } - - // keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/ - cfg := OTLPConfig{ - Endpoint: valueOrDefault[string](otelMap, "OTEL_EXPORTER_OTLP_ENDPOINT"), - } - return cfg, nil -} - -// valueOrDefault returns the type-cast value at the specified key in the map -// if present and the correct type; otherwise, it returns the default value for -// T. -func valueOrDefault[T any](m map[string]any, key string) T { - if v, ok := m[key].(T); ok { - return v - } - return *new(T) -} diff --git a/internal/tracing/mux.go b/internal/tracing/mux.go deleted file mode 100644 index d27e262d2cd..00000000000 --- a/internal/tracing/mux.go +++ /dev/null @@ -1,69 +0,0 @@ -/* - 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 tracing - -import ( - "context" - "errors" - "sync" - - sdktrace "go.opentelemetry.io/otel/sdk/trace" -) - -type MuxExporter struct { - exporters []sdktrace.SpanExporter -} - -func (m MuxExporter) ExportSpans(ctx context.Context, spans []sdktrace.ReadOnlySpan) error { - var ( - wg sync.WaitGroup - errMu sync.Mutex - errs = make([]error, 0, len(m.exporters)) - ) - - for _, exporter := range m.exporters { - wg.Go(func() { - if err := exporter.ExportSpans(ctx, spans); err != nil { - errMu.Lock() - errs = append(errs, err) - errMu.Unlock() - } - }) - } - wg.Wait() - return errors.Join(errs...) -} - -func (m MuxExporter) Shutdown(ctx context.Context) error { - var ( - wg sync.WaitGroup - errMu sync.Mutex - errs = make([]error, 0, len(m.exporters)) - ) - - for _, exporter := range m.exporters { - wg.Go(func() { - if err := exporter.Shutdown(ctx); err != nil { - errMu.Lock() - errs = append(errs, err) - errMu.Unlock() - } - }) - } - wg.Wait() - return errors.Join(errs...) -} diff --git a/internal/tracing/tracing.go b/internal/tracing/tracing.go index ffb68ec9746..c2e9f51b5e2 100644 --- a/internal/tracing/tracing.go +++ b/internal/tracing/tracing.go @@ -18,45 +18,16 @@ package tracing import ( "context" - "errors" - "fmt" - "os" - "strings" "github.com/docker/cli/cli/command" _ "github.com/moby/buildkit/util/tracing/childprocess" //nolint:blank-imports // initializes BuildKit tracing child process propagation. - "github.com/moby/buildkit/util/tracing/detect" "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/attribute" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" "go.opentelemetry.io/otel/propagation" - "go.opentelemetry.io/otel/sdk/resource" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.21.0" - - "github.com/docker/compose/v5/internal" ) -func init() { - detect.ServiceName = "compose" -} - -// OTLPConfig contains the necessary values to initialize an OTLP client -// manually. -// -// This supports a minimal set of options based on what is necessary for -// automatic OTEL configuration from Docker context metadata. -type OTLPConfig struct { - Endpoint string -} - // ShutdownFunc flushes and stops an OTEL exporter. type ShutdownFunc func(ctx context.Context) error -// envMap is a convenience type for OS environment variables. -type envMap map[string]string - func InitTracing(dockerCli command.Cli) (ShutdownFunc, error) { // set global propagator to tracecontext (the default is no-op). otel.SetTextMapPropagator(propagation.TraceContext{}) @@ -64,80 +35,7 @@ func InitTracing(dockerCli command.Cli) (ShutdownFunc, error) { } func InitProvider(dockerCli command.Cli) (ShutdownFunc, error) { - ctx := context.Background() - - var errs []error - var exporters []sdktrace.SpanExporter - - envClient, otelEnv := traceClientFromEnv() - if envClient != nil { - if envExporter, err := otlptrace.New(ctx, envClient); err != nil { - errs = append(errs, err) - } else if envExporter != nil { - exporters = append(exporters, envExporter) - } - } - - if dcClient, err := traceClientFromDockerContext(dockerCli, otelEnv); err != nil { - errs = append(errs, err) - } else if dcClient != nil { - if dcExporter, err := otlptrace.New(ctx, dcClient); err != nil { - errs = append(errs, err) - } else if dcExporter != nil { - exporters = append(exporters, dcExporter) - } - } - if len(errs) != 0 { - return nil, errors.Join(errs...) - } - - res, err := resource.New( - ctx, - resource.WithAttributes( - semconv.ServiceName("compose"), - semconv.ServiceVersion(internal.Version), - attribute.String("docker.context", dockerCli.CurrentContext()), - ), - ) - if err != nil { - return nil, fmt.Errorf("failed to create resource: %w", err) - } - - muxExporter := MuxExporter{exporters: exporters} - tracerProvider := sdktrace.NewTracerProvider( - sdktrace.WithResource(res), - sdktrace.WithBatcher(muxExporter), - ) - otel.SetTracerProvider(tracerProvider) - - // Shutdown will flush any remaining spans and shut down the exporter. - return tracerProvider.Shutdown, nil -} - -// traceClientFromEnv creates a GRPC OTLP client based on OS environment -// variables. -// -// https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/ -func traceClientFromEnv() (otlptrace.Client, envMap) { - hasOtelEndpointInEnv := false - otelEnv := make(map[string]string) - for _, kv := range os.Environ() { - k, v, ok := strings.Cut(kv, "=") - if !ok { - continue - } - if strings.HasPrefix(k, "OTEL_") { - otelEnv[k] = v - if strings.HasSuffix(k, "ENDPOINT") { - hasOtelEndpointInEnv = true - } - } - } - - if !hasOtelEndpointInEnv { - return nil, nil - } - - client := otlptracegrpc.NewClient() - return client, otelEnv + return func(ctx context.Context) error { + return nil + }, nil } diff --git a/internal/tracing/tracing_test.go b/internal/tracing/tracing_test.go deleted file mode 100644 index 124c842f2d4..00000000000 --- a/internal/tracing/tracing_test.go +++ /dev/null @@ -1,60 +0,0 @@ -/* - 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 tracing_test - -import ( - "testing" - - "github.com/docker/cli/cli/command" - "github.com/docker/cli/cli/context/store" - "gotest.tools/v3/assert" - - "github.com/docker/compose/v5/internal/tracing" -) - -var testStoreCfg = store.NewConfig( - func() any { - return &map[string]any{} - }, -) - -func TestExtractOtelFromContext(t *testing.T) { - if testing.Short() { - t.Skip("Requires filesystem access") - } - - dir := t.TempDir() - - st := store.New(dir, testStoreCfg) - err := st.CreateOrUpdate(store.Metadata{ - Name: "test", - Metadata: command.DockerContext{ - Description: t.Name(), - AdditionalFields: map[string]any{ - "otel": map[string]any{ - "OTEL_EXPORTER_OTLP_ENDPOINT": "localhost:1234", - }, - }, - }, - Endpoints: make(map[string]any), - }) - assert.NilError(t, err) - - cfg, err := tracing.ConfigFromDockerContext(st, "test") - assert.NilError(t, err) - assert.Equal(t, "localhost:1234", cfg.Endpoint) -}