Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 0 additions & 68 deletions cli-plugins/manager/cobra.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,10 @@ package manager
import (
"fmt"
"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 (
Expand Down Expand Up @@ -105,67 +101,3 @@ func AddPluginCommandStubs(dockerCli command.Cli, rootCmd *cobra.Command) (err e
})
return err
}

const (
dockerCliAttributePrefix = command.DockerCliAttributePrefix

cobraCommandPath = attribute.Key("cobra.command_path")
)

func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
commandPath := cmd.Annotations[CommandAnnotationPluginCommandPath]
if commandPath == "" {
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
}

attrSet := attribute.NewSet(
cobraCommandPath.String(commandPath),
)

kvs := make([]attribute.KeyValue, 0, attrSet.Len())
for iter := attrSet.Iter(); iter.Next(); {
attr := iter.Attribute()
kvs = append(kvs, attribute.KeyValue{
Key: dockerCliAttributePrefix + attr.Key,
Value: attr.Value,
})
}
return attribute.NewSet(kvs...)
}

func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
// 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(); {
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, ","))
}
}
return env
}
4 changes: 3 additions & 1 deletion cli-plugins/manager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ const (

// ResourceAttributesEnvvar is the name of the envvar that includes additional
// resource attributes for OTEL.
ResourceAttributesEnvvar = command.ResourceAttributesEnvvar
//
// Deprecated: The "OTEL_RESOURCE_ATTRIBUTES" env-var is part of the OpenTelemetry specification; users should define their own const for this. This const will be removed in the next release.
ResourceAttributesEnvvar = "OTEL_RESOURCE_ATTRIBUTES"
)

// errPluginNotFound is the error returned when a plugin could not be found.
Expand Down
84 changes: 84 additions & 0 deletions cli-plugins/manager/telemetry.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package manager

import (
"fmt"
"os"
"strings"

"github.com/spf13/cobra"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/baggage"
)

const (
// resourceAttributesEnvVar is the name of the envvar that includes additional
// resource attributes for OTEL as defined in the [OpenTelemetry specification].
//
// [OpenTelemetry specification]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration
resourceAttributesEnvVar = "OTEL_RESOURCE_ATTRIBUTES"

// dockerCLIAttributePrefix is the prefix for any docker cli OTEL attributes.
//
// It is a copy of the const defined in [command.dockerCLIAttributePrefix].
dockerCLIAttributePrefix = "docker.cli."
cobraCommandPath = attribute.Key("cobra.command_path")
)

func getPluginResourceAttributes(cmd *cobra.Command, plugin Plugin) attribute.Set {
commandPath := cmd.Annotations[CommandAnnotationPluginCommandPath]
if commandPath == "" {
commandPath = fmt.Sprintf("%s %s", cmd.CommandPath(), plugin.Name)
}

attrSet := attribute.NewSet(
cobraCommandPath.String(commandPath),
)

kvs := make([]attribute.KeyValue, 0, attrSet.Len())
for iter := attrSet.Iter(); iter.Next(); {
attr := iter.Attribute()
kvs = append(kvs, attribute.KeyValue{
Key: dockerCLIAttributePrefix + attr.Key,
Value: attr.Value,
})
}
return attribute.NewSet(kvs...)
}

func appendPluginResourceAttributesEnvvar(env []string, cmd *cobra.Command, plugin Plugin) []string {
if attrs := getPluginResourceAttributes(cmd, plugin); attrs.Len() > 0 {
// 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(); {
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, ","))
}
}
return env
}
44 changes: 0 additions & 44 deletions cli/command/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import (
"path/filepath"
"runtime"
"strconv"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -593,46 +592,3 @@ 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, ",")
}
47 changes: 47 additions & 0 deletions cli/command/telemetry.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"os"
"path/filepath"
"strings"
"sync"
"time"

Expand Down Expand Up @@ -216,3 +217,49 @@ func (r *cliReader) ForceFlush(ctx context.Context) error {
func deltaTemporality(_ sdkmetric.InstrumentKind) metricdata.Temporality {
return metricdata.DeltaTemporality
}

// resourceAttributesEnvVar is the name of the envvar that includes additional
// resource attributes for OTEL as defined in the [OpenTelemetry specification].
//
// [OpenTelemetry specification]: https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/#general-sdk-configuration
const resourceAttributesEnvVar = "OTEL_RESOURCE_ATTRIBUTES"

func filterResourceAttributesEnvvar() {
if v := os.Getenv(resourceAttributesEnvVar); v != "" {
if filtered := filterResourceAttributes(v); filtered != "" {
_ = os.Setenv(resourceAttributesEnvVar, filtered)
} else {
_ = os.Unsetenv(resourceAttributesEnvVar)
}
}
}

// dockerCLIAttributePrefix is the prefix for any docker cli OTEL attributes.
// When updating, make sure to also update the copy in cli-plugins/manager.
//
// TODO(thaJeztah): move telemetry-related code to an (internal) package to reduce dependency on cli/command in cli-plugins, which has too many imports.
const dockerCLIAttributePrefix = "docker.cli."

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, ",")
}
Loading