Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(otel): initialize meter provider from Docker context #11641

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/cmdtrace/cmd_span.go
Expand Up @@ -44,12 +44,13 @@ import (
// command invocation to ensure the span is properly finalized and
// exported before exit.
func Setup(cmd *cobra.Command, dockerCli command.Cli, args []string) error {
tracingShutdown, err := tracing.InitTracing(dockerCli)
ctx := cmd.Context()

tracingShutdown, err := tracing.Initialize(ctx, dockerCli)
if err != nil {
return fmt.Errorf("initializing tracing: %w", err)
}

ctx := cmd.Context()
ctx, cmdSpan := tracing.Tracer.Start(
ctx,
"cli/"+strings.Join(commandName(cmd), "-"),
Expand Down
6 changes: 3 additions & 3 deletions go.mod
Expand Up @@ -40,9 +40,12 @@ require (
github.com/tilt-dev/fsnotify v1.4.8-0.20220602155310-fff9c274a375
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.45.0
go.opentelemetry.io/otel v1.19.0
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0
go.opentelemetry.io/otel/metric v1.19.0
go.opentelemetry.io/otel/sdk v1.19.0
go.opentelemetry.io/otel/sdk/metric v1.19.0
go.opentelemetry.io/otel/trace v1.19.0
go.uber.org/goleak v1.3.0
go.uber.org/mock v0.4.0
Expand Down Expand Up @@ -149,12 +152,9 @@ require (
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.45.0 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.45.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v0.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect
go.opentelemetry.io/otel/exporters/prometheus v0.42.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.19.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/crypto v0.18.0 // indirect
golang.org/x/mod v0.14.0 // indirect
Expand Down
112 changes: 59 additions & 53 deletions internal/tracing/docker_context.go
Expand Up @@ -25,68 +25,27 @@ import (
"github.com/docker/cli/cli/command"
"github.com/docker/cli/cli/context/store"
"github.com/docker/compose/v2/internal/memnet"
"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"
)

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

dialCtx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
defer cancel()
conn, err := grpc.DialContext(
dialCtx,
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
// DockerOTLPConfig 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 DockerOTLPConfig struct {
Endpoint string
}

// 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) {
func ConfigFromDockerContext(st store.Store, name string) (DockerOTLPConfig, error) {
meta, err := st.GetMetadata(name)
if err != nil {
return OTLPConfig{}, err
return DockerOTLPConfig{}, err
}

var otelCfg interface{}
Expand All @@ -97,12 +56,12 @@ func ConfigFromDockerContext(st store.Store, name string) (OTLPConfig, error) {
otelCfg = m[otelConfigFieldName]
}
if otelCfg == nil {
return OTLPConfig{}, nil
return DockerOTLPConfig{}, nil
}

otelMap, ok := otelCfg.(map[string]interface{})
if !ok {
return OTLPConfig{}, fmt.Errorf(
return DockerOTLPConfig{}, fmt.Errorf(
"unexpected type for field %q: %T (expected: %T)",
otelConfigFieldName,
otelCfg,
Expand All @@ -111,12 +70,36 @@ func ConfigFromDockerContext(st store.Store, name string) (OTLPConfig, error) {
}

// keys from https://opentelemetry.io/docs/concepts/sdk-configuration/otlp-exporter-configuration/
cfg := OTLPConfig{
cfg := DockerOTLPConfig{
Endpoint: valueOrDefault[string](otelMap, "OTEL_EXPORTER_OTLP_ENDPOINT"),
}
return cfg, nil
}

// grpcConnection creates an OTLP/gRPC connection based on the Docker context configuration.
//
// If no endpoint is defined in the config, nil is returned.
func grpcConnection(ctx context.Context, cfg DockerOTLPConfig) (*grpc.ClientConn, error) {
if cfg.Endpoint == "" {
return nil, nil
}

dialCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
defer cancel()
conn, err := grpc.DialContext(
dialCtx,
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("dialing Docker otel: %w", err)
}
return conn, 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.
Expand All @@ -126,3 +109,26 @@ func valueOrDefault[T any](m map[string]interface{}, key string) T {
}
return *new(T)
}

// withoutOTelEnv runs a function while temporarily "hiding" all OTEL_ prefixed
// env vars and restoring them afterward.
//
// Unfortunately, the public OTEL exporter constructors ALWAYS implicitly read
// from the OS env, so this is necessary to allow for custom client construction
// without interference.
func withoutOTelEnv[T any](otelEnv envMap, fn func() (T, error)) (T, error) {
for k := range otelEnv {
if err := os.Unsetenv(k); err != nil {
panic(fmt.Errorf("stashing env for %q: %w", k, err))
}
}

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))
}
}
}()
return fn()
}