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) -}