Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions cli/azd/pkg/azdext/extension_host.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ import (
"context"
"errors"
"fmt"
"io"
"log"
"os"
"strconv"
"sync"

"google.golang.org/grpc/codes"
Expand Down Expand Up @@ -149,6 +152,17 @@ 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 truthy, keep
// logging to stderr for diagnostics.
// 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)
}
Comment on lines +161 to +164
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ExtensionHost.Run() now sets the global standard logger output to io.Discard and never restores it. In real extension binaries that exit after Run returns this is mostly harmless, but it creates surprising global side effects for unit tests (especially those running in parallel) and for any program that might call Run() and continue doing work afterward. Consider saving the original log writer and restoring it via defer when Run() returns, or clearly scoping the silencing to the broker logger rather than the process-wide default logger.

Suggested change
// "1", "t", "TRUE", "true", etc.).
if isDebug, err := strconv.ParseBool(os.Getenv("AZD_EXT_DEBUG")); err != nil || !isDebug {
log.SetOutput(io.Discard)
}
// "1", "t", "TRUE", "true", etc.).
originalLogWriter := log.Default().Writer()
restoreLogOutput := false
if isDebug, err := strconv.ParseBool(os.Getenv("AZD_EXT_DEBUG")); err != nil || !isDebug {
log.SetOutput(io.Discard)
restoreLogOutput = true
}
if restoreLogOutput {
defer log.SetOutput(originalLogWriter)
}

Copilot uses AI. Check for mistakes.

// Determine which managers will be active
hasServiceTargets := len(er.serviceTargets) > 0
hasFrameworkServices := len(er.frameworkServices) > 0
Expand Down
92 changes: 92 additions & 0 deletions cli/azd/pkg/azdext/extension_host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@
package azdext

import (
"bytes"
"context"
"errors"
"io"
"log"
"os"
"sync"
"testing"
"time"
Expand Down Expand Up @@ -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) {
Comment on lines +674 to +678
Copy link

Copilot AI Feb 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test mutates global process state (standard logger output and AZD_EXT_DEBUG), but other tests in this same file/package are marked t.Parallel() and call ExtensionHost.Run(), which now also mutates the global logger. That means this test can still run concurrently with those parallel tests and become flaky due to cross-test interference. Consider removing t.Parallel() from tests that call ExtensionHost.Run(), or guarding all log/env mutations with a shared package-level mutex so tests that touch global state cannot overlap.

Copilot uses AI. Check for mistakes.
// 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")
})
}
Loading