From 99bd2192291b35c9f2a6af598d669d2938ca85e1 Mon Sep 17 00:00:00 2001 From: hemarina Date: Fri, 27 Feb 2026 17:06:31 -0800 Subject: [PATCH 1/2] Silence extension process stderr logs to prevent call-stack-like error output Extension processes emit verbose gRPC MessageBroker trace logs via Go's log.Printf, which defaults to os.Stderr. Since azd captures extension stderr and includes it in error messages, these internal diagnostics appear to users as call-stack-like error output. Silence the global logger in ExtensionHost.Run() by setting log.SetOutput(io.Discard) unless AZD_EXT_DEBUG=true is set. This ensures backward compatibility with extensions compiled against older SDK versions that still use log.Printf directly. --- cli/azd/pkg/azdext/extension_host.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cli/azd/pkg/azdext/extension_host.go b/cli/azd/pkg/azdext/extension_host.go index 5131090628d..62aaa6f2aaa 100644 --- a/cli/azd/pkg/azdext/extension_host.go +++ b/cli/azd/pkg/azdext/extension_host.go @@ -7,7 +7,9 @@ import ( "context" "errors" "fmt" + "io" "log" + "os" "sync" "google.golang.org/grpc/codes" @@ -149,6 +151,15 @@ func (er *ExtensionHost) Run(ctx context.Context) error { // When user declines or cancels, continue so extension doesn't exit while azd continues _ = WaitForDebugger(ctx, er.client) + // Silence the global logger in extension processes to prevent internal + // gRPC broker trace logs from appearing in stderr. Extensions compiled + // against older SDK versions still use log.Printf directly, so this + // ensures backward compatibility. When AZD_EXT_DEBUG is true, keep + // logging to stderr for diagnostics. + if os.Getenv("AZD_EXT_DEBUG") != "true" { + log.SetOutput(io.Discard) + } + // Determine which managers will be active hasServiceTargets := len(er.serviceTargets) > 0 hasFrameworkServices := len(er.frameworkServices) > 0 From f8c71186a845e948bb75b83013afc075a08b552f Mon Sep 17 00:00:00 2001 From: hemarina Date: Fri, 27 Feb 2026 17:46:42 -0800 Subject: [PATCH 2/2] add tests --- cli/azd/pkg/azdext/extension_host.go | 7 +- cli/azd/pkg/azdext/extension_host_test.go | 92 +++++++++++++++++++++++ 2 files changed, 97 insertions(+), 2 deletions(-) diff --git a/cli/azd/pkg/azdext/extension_host.go b/cli/azd/pkg/azdext/extension_host.go index 62aaa6f2aaa..c9e0e9a6cd0 100644 --- a/cli/azd/pkg/azdext/extension_host.go +++ b/cli/azd/pkg/azdext/extension_host.go @@ -10,6 +10,7 @@ import ( "io" "log" "os" + "strconv" "sync" "google.golang.org/grpc/codes" @@ -154,9 +155,11 @@ func (er *ExtensionHost) Run(ctx context.Context) error { // Silence the global logger in extension processes to prevent internal // gRPC broker trace logs from appearing in stderr. Extensions compiled // against older SDK versions still use log.Printf directly, so this - // ensures backward compatibility. When AZD_EXT_DEBUG is true, keep + // ensures backward compatibility. When AZD_EXT_DEBUG is truthy, keep // logging to stderr for diagnostics. - if os.Getenv("AZD_EXT_DEBUG") != "true" { + // Uses strconv.ParseBool to match WaitForDebugger semantics (accepts + // "1", "t", "TRUE", "true", etc.). + if isDebug, err := strconv.ParseBool(os.Getenv("AZD_EXT_DEBUG")); err != nil || !isDebug { log.SetOutput(io.Discard) } diff --git a/cli/azd/pkg/azdext/extension_host_test.go b/cli/azd/pkg/azdext/extension_host_test.go index 418e9683533..3125fa35261 100644 --- a/cli/azd/pkg/azdext/extension_host_test.go +++ b/cli/azd/pkg/azdext/extension_host_test.go @@ -4,8 +4,12 @@ package azdext import ( + "bytes" "context" "errors" + "io" + "log" + "os" "sync" "testing" "time" @@ -666,3 +670,91 @@ func TestExtensionHost_MultipleRegistrationErrors(t *testing.T) { mockServiceTargetManager.AssertExpectations(t) mockFrameworkServiceManager.AssertExpectations(t) } + +// TestExtensionHost_RunSilencesLog tests that Run() silences the global logger +// when AZD_EXT_DEBUG is not set, preventing internal gRPC broker trace logs +// from appearing in extension stderr. +// These tests mutate global state (log output, env vars) and must NOT run in parallel. +func TestExtensionHost_RunSilencesLog(t *testing.T) { + // Save and restore global log output + originalOutput := log.Writer() + defer log.SetOutput(originalOutput) + + // Save and restore AZD_EXT_DEBUG env var + originalDebug, hadDebug := os.LookupEnv("AZD_EXT_DEBUG") + defer func() { + if hadDebug { + os.Setenv("AZD_EXT_DEBUG", originalDebug) + } else { + os.Unsetenv("AZD_EXT_DEBUG") + } + }() + + t.Run("silences log output by default", func(t *testing.T) { + // Reset log to a known non-discard writer + var buf bytes.Buffer + log.SetOutput(&buf) + + // Ensure AZD_EXT_DEBUG is not set + os.Unsetenv("AZD_EXT_DEBUG") + + // Setup minimal extension host with no registrations + client := newTestAzdClient() + runner := NewExtensionHost(client) + + err := runner.Run(context.Background()) + require.NoError(t, err) + + // After Run(), global log should be silenced + assert.Equal(t, io.Discard, log.Writer(), "log output should be io.Discard when AZD_EXT_DEBUG is not set") + + // Verify log.Printf output is actually discarded + log.Printf("this should be discarded") + assert.Empty(t, buf.String(), "log output should not appear in the buffer after silencing") + }) + + t.Run("silences log output when AZD_EXT_DEBUG is empty", func(t *testing.T) { + var buf bytes.Buffer + log.SetOutput(&buf) + + os.Setenv("AZD_EXT_DEBUG", "") + + client := newTestAzdClient() + runner := NewExtensionHost(client) + + err := runner.Run(context.Background()) + require.NoError(t, err) + + assert.Equal(t, io.Discard, log.Writer(), "log output should be io.Discard when AZD_EXT_DEBUG is empty") + }) + + t.Run("silences log output when AZD_EXT_DEBUG is false", func(t *testing.T) { + var buf bytes.Buffer + log.SetOutput(&buf) + + os.Setenv("AZD_EXT_DEBUG", "false") + + client := newTestAzdClient() + runner := NewExtensionHost(client) + + err := runner.Run(context.Background()) + require.NoError(t, err) + + assert.Equal(t, io.Discard, log.Writer(), "log output should be io.Discard when AZD_EXT_DEBUG is false") + }) + + t.Run("silences log output when AZD_EXT_DEBUG is invalid", func(t *testing.T) { + var buf bytes.Buffer + log.SetOutput(&buf) + + os.Setenv("AZD_EXT_DEBUG", "notabool") + + client := newTestAzdClient() + runner := NewExtensionHost(client) + + err := runner.Run(context.Background()) + require.NoError(t, err) + + assert.Equal(t, io.Discard, log.Writer(), "log output should be io.Discard when AZD_EXT_DEBUG is invalid") + }) +}