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 5cbae0b9a80..61f11ea7cf0 100644 --- a/go.mod +++ b/go.mod @@ -140,3 +140,5 @@ exclude ( github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 ) + +replace github.com/docker/cli => github.com/jsternberg/cli v25.0.1-0.20261023181239-bcd15ee400ad+incompatible diff --git a/go.sum b/go.sum index abe8b3a9b75..4b6b31ed014 100644 --- a/go.sum +++ b/go.sum @@ -76,8 +76,6 @@ 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.2+incompatible h1:trup/vUxx5uUo2t+87Cd9AmITR+DvKUe4H2tTtZN8FY= -github.com/docker/cli v29.9.2+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= @@ -135,6 +133,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf h1:FtEj8sfIcaaBfAKrE1Cwb61YDtYq9JxChK1c7AKce7s= github.com/inhies/go-bytesize v0.0.0-20220417184213-4913239db9cf/go.mod h1:yrqSXGoD/4EKfF26AOGzscPOgTTJcyAwM2rpixWT+t4= +github.com/jsternberg/cli v25.0.1-0.20261023181239-bcd15ee400ad+incompatible h1:Ea9EzYKBBWMmDmbqkmuQCXCfqfBTwTHiTAc8sH+oi2Q= +github.com/jsternberg/cli v25.0.1-0.20261023181239-bcd15ee400ad+incompatible/go.mod h1:QSUBBpjaw2KRnlXj0e8Uvl9cGOAZdpBdRMRCse8Xv2U= github.com/klauspost/compress v1.20.0 h1:a3C1ke2ohxFymNlb2HWAHjDeKCI90scRskErZkR0ezA= github.com/klauspost/compress v1.20.0/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI= github.com/mattn/go-runewidth v0.0.31 h1:1oCV4ReTlosDBc6rYRw5cL8S/4y8iv5oRl4SJ5IBgwo= 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) -}