Skip to content

Commit

Permalink
feat(otel): initialize meter provider from Docker context
Browse files Browse the repository at this point in the history
In addition to tracing, add support for metering (counters/gauges/etc)
via OpenTelemetry with similar OTLP/gRPC auto-enablement. User-defined
endpoints not currently supported; this will be added as a follow-up.

Signed-off-by: Milas Bowman <milas.bowman@docker.com>
  • Loading branch information
milas committed Mar 20, 2024
1 parent 1b5fa3b commit 0b11ce6
Show file tree
Hide file tree
Showing 5 changed files with 273 additions and 131 deletions.
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()
}

0 comments on commit 0b11ce6

Please sign in to comment.