From 02f5554430b7cd4925915dd1a714713b3f19ed0f Mon Sep 17 00:00:00 2001 From: "hiroto.toyoda" Date: Sat, 29 Aug 2026 04:20:41 +0900 Subject: [PATCH] feat(tracing): add COMPOSE_OTEL_DEBUG to surface OTel internals Signed-off-by: hiroto.toyoda --- cmd/cmdtrace/cmd_span.go | 11 ++++++----- internal/tracing/errors.go | 32 +++++++++++++++++++++++++------- internal/tracing/tracing.go | 4 ++-- 3 files changed, 33 insertions(+), 14 deletions(-) diff --git a/cmd/cmdtrace/cmd_span.go b/cmd/cmdtrace/cmd_span.go index cce3e5db3f3..213f1b6a6b7 100644 --- a/cmd/cmdtrace/cmd_span.go +++ b/cmd/cmdtrace/cmd_span.go @@ -20,6 +20,7 @@ import ( "context" "errors" "fmt" + "io" "sort" "strings" "time" @@ -62,7 +63,7 @@ func Setup(cmd *cobra.Command, dockerCli command.Cli, args []string) error { ) cmd.SetContext(ctx) - wrapRunE(cmd, cmdSpan, tracingShutdown) + wrapRunE(cmd, cmdSpan, tracingShutdown, dockerCli.Err()) return nil } @@ -72,7 +73,7 @@ func Setup(cmd *cobra.Command, dockerCli command.Cli, args []string) error { // // Unfortunately, PersistentPostRun(E) can't be used for this purpose because it // only runs if RunE does _not_ return an error, but this should run unconditionally. -func wrapRunE(c *cobra.Command, cmdSpan trace.Span, tracingShutdown tracing.ShutdownFunc) { +func wrapRunE(c *cobra.Command, cmdSpan trace.Span, tracingShutdown tracing.ShutdownFunc, errOut io.Writer) { origRunE := c.RunE if origRunE == nil { origRun := c.Run @@ -110,9 +111,9 @@ func wrapRunE(c *cobra.Command, cmdSpan trace.Span, tracingShutdown tracing.Shut // been canceled already ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond) defer cancel() - // TODO(milas): add an env var to enable logging from the - // OTel components for debugging purposes - _ = tracingShutdown(ctx) + if err := tracingShutdown(ctx); err != nil && tracing.DebugEnabled() { + fmt.Fprintln(errOut, "otel: shutdown:", err) + } } return cmdErr } diff --git a/internal/tracing/errors.go b/internal/tracing/errors.go index 9fa615054c0..4ad4c24e4df 100644 --- a/internal/tracing/errors.go +++ b/internal/tracing/errors.go @@ -17,13 +17,31 @@ package tracing import ( + "fmt" + "os" + "strconv" + "go.opentelemetry.io/otel" ) -// skipErrors is a no-op otel.ErrorHandler. -type skipErrors struct{} - -// Handle does nothing, ignoring any errors passed to it. -func (skipErrors) Handle(_ error) {} - -var _ otel.ErrorHandler = skipErrors{} +// DebugEnabled reports whether OTel SDK/exporter internals should print +// their diagnostics, controlled by the COMPOSE_OTEL_DEBUG environment +// variable. It defaults to false so tracing plumbing never leaks into +// ordinary CLI output. +func DebugEnabled() bool { + enabled, _ := strconv.ParseBool(os.Getenv("COMPOSE_OTEL_DEBUG")) + return enabled +} + +// errorHandler is the otel.ErrorHandler installed for the CLI: it discards +// errors unless DebugEnabled reports true, in which case it prints them to +// stderr. +type errorHandler struct{} + +func (errorHandler) Handle(err error) { + if DebugEnabled() { + fmt.Fprintln(os.Stderr, "otel:", err) + } +} + +var _ otel.ErrorHandler = errorHandler{} diff --git a/internal/tracing/tracing.go b/internal/tracing/tracing.go index 85325dbea0f..a5eef8b3b98 100644 --- a/internal/tracing/tracing.go +++ b/internal/tracing/tracing.go @@ -40,8 +40,8 @@ import ( func init() { detect.ServiceName = "compose" - // do not log tracing errors to stdio - otel.SetErrorHandler(skipErrors{}) + // do not log tracing errors to stdio, unless COMPOSE_OTEL_DEBUG is set + otel.SetErrorHandler(errorHandler{}) } // OTLPConfig contains the necessary values to initialize an OTLP client