diff --git a/cli-plugins/manager/cobra.go b/cli-plugins/manager/cobra.go index 4bfa06fa5c..ec45045b06 100644 --- a/cli-plugins/manager/cobra.go +++ b/cli-plugins/manager/cobra.go @@ -2,14 +2,15 @@ package manager import ( "fmt" - "net/url" "os" "strings" "sync" "github.com/docker/cli/cli/command" "github.com/spf13/cobra" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/baggage" ) const ( @@ -106,7 +107,7 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e } const ( - dockerCliAttributePrefix = attribute.Key("docker.cli") + dockerCliAttributePrefix = command.DockerCliAttributePrefix cobraCommandPath = attribute.Key("cobra.command_path") ) @@ -125,7 +126,7 @@ func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Se for iter := attrSet.Iter(); iter.Next(); { attr := iter.Attribute() kvs = append(kvs, attribute.KeyValue{ - Key: dockerCliAttributePrefix + "." + attr.Key, + Key: dockerCliAttributePrefix + attr.Key, Value: attr.Value, }) } @@ -134,14 +135,37 @@ func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Se func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string { if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 { - // values in environment variables need to be in baggage format - // otel/baggage package can be used after update to v1.22, currently it encodes incorrectly - attrsSlice := make([]string, attrs.Len()) + // Construct baggage members for each of the attributes. + // Ignore any failures as these aren't significant and + // represent an internal issue. + members := make([]baggage.Member, 0, attrs.Len()) for iter := attrs.Iter(); iter.Next(); { - i, v := iter.IndexedAttribute() - attrsSlice[i] = string(v.Key) + "=" + url.PathEscape(v.Value.AsString()) + attr := iter.Attribute() + m, err := baggage.NewMemberRaw(string(attr.Key), attr.Value.AsString()) + if err != nil { + otel.Handle(err) + continue + } + members = append(members, m) + } + + // Combine plugin added resource attributes with ones found in the environment + // variable. Our own attributes should be namespaced so there shouldn't be a + // conflict. We do not parse the environment variable because we do not want + // to handle errors in user configuration. + attrsSlice := make([]string, 0, 2) + if v := strings.TrimSpace(os.Getenv(ResourceAttributesEnvvar)); v != "" { + attrsSlice = append(attrsSlice, v) + } + if b, err := baggage.New(members...); err != nil { + otel.Handle(err) + } else if b.Len() > 0 { + attrsSlice = append(attrsSlice, b.String()) + } + + if len(attrsSlice) > 0 { + env = append(env, ResourceAttributesEnvvar+"="+strings.Join(attrsSlice, ",")) } - env = append(env, ResourceAttributesEnvvar+"="+strings.Join(attrsSlice, ",")) } return env } diff --git a/cli-plugins/manager/cobra_test.go b/cli-plugins/manager/cobra_test.go new file mode 100644 index 0000000000..207b536f1f --- /dev/null +++ b/cli-plugins/manager/cobra_test.go @@ -0,0 +1,26 @@ +package manager + +import ( + "testing" + + "github.com/spf13/cobra" + "gotest.tools/v3/assert" +) + +func TestPluginResourceAttributesEnvvar(t *testing.T) { + cmd := &cobra.Command{ + Annotations: map[string]string{ + cobra.CommandDisplayNameAnnotation: "docker", + }, + } + + // Ensure basic usage is fine. + env := appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"}) + assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=docker.cli.cobra.command_path=docker%20compose"}, env) + + // Add a user-based environment variable to OTEL_RESOURCE_ATTRIBUTES. + t.Setenv("OTEL_RESOURCE_ATTRIBUTES", "a.b.c=foo") + + env = appendPluginResourceAttributesEnvvar(nil, cmd, Plugin{Name: "compose"}) + assert.DeepEqual(t, []string{"OTEL_RESOURCE_ATTRIBUTES=a.b.c=foo,docker.cli.cobra.command_path=docker%20compose"}, env) +} diff --git a/cli-plugins/manager/manager.go b/cli-plugins/manager/manager.go index 9f795bc498..09b5b14e73 100644 --- a/cli-plugins/manager/manager.go +++ b/cli-plugins/manager/manager.go @@ -26,7 +26,7 @@ const ( // ResourceAttributesEnvvar is the name of the envvar that includes additional // resource attributes for OTEL. - ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES" + ResourceAttributesEnvvar = command.ResourceAttributesEnvvar ) // errPluginNotFound is the error returned when a plugin could not be found. diff --git a/cli/command/cli.go b/cli/command/cli.go index 227720fa07..1452e1a240 100644 --- a/cli/command/cli.go +++ b/cli/command/cli.go @@ -11,6 +11,7 @@ import ( "path/filepath" "runtime" "strconv" + "strings" "sync" "time" @@ -292,6 +293,7 @@ func (cli *DockerCli) Initialize(opts *cliflags.ClientOptions, ops ...CLIOption) if cli.enableGlobalTracer { cli.createGlobalTracerProvider(cli.baseCtx) } + filterResourceAttributesEnvvar() return nil } @@ -591,3 +593,46 @@ func DefaultContextStoreConfig() store.Config { defaultStoreEndpoints..., ) } + +const ( + // ResourceAttributesEnvvar is the name of the envvar that includes additional + // resource attributes for OTEL. + ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES" + + // DockerCliAttributePrefix is the prefix for any docker cli OTEL attributes. + DockerCliAttributePrefix = "docker.cli." +) + +func filterResourceAttributesEnvvar() { + if v := os.Getenv(ResourceAttributesEnvvar); v != "" { + if filtered := filterResourceAttributes(v); filtered != "" { + os.Setenv(ResourceAttributesEnvvar, filtered) + } else { + os.Unsetenv(ResourceAttributesEnvvar) + } + } +} + +func filterResourceAttributes(s string) string { + if trimmed := strings.TrimSpace(s); trimmed == "" { + return trimmed + } + + pairs := strings.Split(s, ",") + elems := make([]string, 0, len(pairs)) + for _, p := range pairs { + k, _, found := strings.Cut(p, "=") + if !found { + // Do not interact with invalid otel resources. + elems = append(elems, p) + continue + } + + // Skip attributes that have our docker.cli prefix. + if strings.HasPrefix(k, DockerCliAttributePrefix) { + continue + } + elems = append(elems, p) + } + return strings.Join(elems, ",") +}