Skip to content

Commit de1f7fb

Browse files
authored
Extension loading/timeout enhancments (#6122)
1 parent e22e229 commit de1f7fb

22 files changed

Lines changed: 946 additions & 247 deletions

cli/azd/cmd/middleware/extensions.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import (
1818
"github.com/azure/azure-dev/cli/azd/pkg/extensions"
1919
"github.com/azure/azure-dev/cli/azd/pkg/input"
2020
"github.com/azure/azure-dev/cli/azd/pkg/ioc"
21+
"github.com/azure/azure-dev/cli/azd/pkg/output"
2122
"github.com/fatih/color"
2223
)
2324

@@ -100,6 +101,8 @@ func (m *ExtensionsMiddleware) Run(ctx context.Context, next NextFn) (*actions.A
100101

101102
forceColor := !color.NoColor
102103
var wg sync.WaitGroup
104+
var mu sync.Mutex
105+
var failedExtensions []*extensions.Extension
103106

104107
// Track total time for all extensions to become ready
105108
allExtensionsStartTime := time.Now()
@@ -145,13 +148,19 @@ func (m *ExtensionsMiddleware) Run(ctx context.Context, next NextFn) (*actions.A
145148

146149
// Wait for the extension to signal readiness or failure.
147150
// If AZD_EXT_DEBUG is set to a truthy value, wait indefinitely for debugger attachment
151+
// If AZD_EXT_TIMEOUT is set to a number (seconds), use that as the timeout (default: 5 seconds)
148152
readyCtx, cancel := getReadyContext(ctx)
149153
defer cancel()
150154

151155
startTime := time.Now()
152156
if err := ext.WaitUntilReady(readyCtx); err != nil {
153157
elapsed := time.Since(startTime)
154158
log.Printf("'%s' extension failed to become ready after %v: %v\n", ext.Id, elapsed, err)
159+
160+
// Track failed extensions for warning display
161+
mu.Lock()
162+
failedExtensions = append(failedExtensions, ext)
163+
mu.Unlock()
155164
} else {
156165
elapsed := time.Since(startTime)
157166
log.Printf("'%s' extension became ready in %v\n", ext.Id, elapsed)
@@ -162,6 +171,22 @@ func (m *ExtensionsMiddleware) Run(ctx context.Context, next NextFn) (*actions.A
162171
// Wait for all extensions to reach a terminal state (ready or failed)
163172
wg.Wait()
164173

174+
// Check for failed extensions and display warnings
175+
176+
if len(failedExtensions) > 0 {
177+
m.console.Message(ctx, output.WithWarningFormat("WARNING: Extension startup failures detected"))
178+
m.console.Message(ctx, "The following extensions failed to initialize within the timeout period:")
179+
for _, ext := range failedExtensions {
180+
m.console.Message(ctx, fmt.Sprintf(" - %s (%s)", ext.DisplayName, ext.Id))
181+
}
182+
m.console.Message(ctx, "")
183+
m.console.Message(
184+
ctx,
185+
"Some features may be unavailable. Increase timeout with AZD_EXT_TIMEOUT=<seconds> if needed.",
186+
)
187+
m.console.Message(ctx, "")
188+
}
189+
165190
// Log total time for all extensions to complete startup
166191
totalElapsed := time.Since(allExtensionsStartTime)
167192
log.Printf("All %d extensions completed startup in %v\n", len(extensionList), totalElapsed)
@@ -185,5 +210,14 @@ func getReadyContext(ctx context.Context) (context.Context, context.CancelFunc)
185210
if isDebug() {
186211
return context.WithCancel(ctx)
187212
}
188-
return context.WithTimeout(ctx, 5*time.Second)
213+
214+
// Use custom timeout from environment variable or default to 5 seconds
215+
timeout := 5 * time.Second
216+
if timeoutValue := os.Getenv("AZD_EXT_TIMEOUT"); timeoutValue != "" {
217+
if seconds, err := strconv.Atoi(timeoutValue); err == nil && seconds > 0 {
218+
timeout = time.Duration(seconds) * time.Second
219+
}
220+
}
221+
222+
return context.WithTimeout(ctx, timeout)
189223
}

cli/azd/go.work.sum

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
2+
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
23
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
34
github.com/Code-Hex/go-generics-cache v1.3.1/go.mod h1:qxcC9kRVrct9rHeiYpFWSoW1vxyillCVzX13KZG8dl4=
45
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0/go.mod h1:Cz6ft6Dkn3Et6l2v2a9/RpN7epQ1GtDlO6lj8bEcOvw=

cli/azd/internal/grpcserver/event_service.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"context"
88
"errors"
99
"fmt"
10-
"log"
1110

1211
"github.com/azure/azure-dev/cli/azd/internal/mapper"
1312
"github.com/azure/azure-dev/cli/azd/pkg/azdext"
@@ -79,7 +78,7 @@ func (s *eventService) EventStream(stream grpc.BidiStreamingServer[azdext.EventM
7978

8079
// Create message broker with EventMessageEnvelope
8180
envelope := azdext.NewEventMessageEnvelope()
82-
broker := grpcbroker.NewMessageBroker(stream, envelope)
81+
broker := grpcbroker.NewMessageBroker(stream, envelope, extension.Id)
8382

8483
// Register handlers for incoming subscription requests (no response needed)
8584
broker.On(func(ctx context.Context, msg *azdext.SubscribeProjectEvent) (*azdext.EventMessage, error) {
@@ -92,11 +91,9 @@ func (s *eventService) EventStream(stream grpc.BidiStreamingServer[azdext.EventM
9291

9392
// Run the broker's dispatcher (blocking)
9493
if err := broker.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
95-
log.Printf("Broker error in EventStream: %v", err)
9694
return fmt.Errorf("broker error: %w", err)
9795
}
9896

99-
log.Println("EventStream completed")
10097
return nil
10198
}
10299

cli/azd/internal/grpcserver/framework_service.go

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ func (s *FrameworkService) Stream(stream azdext.FrameworkService_StreamServer) e
6666

6767
// Create message broker for this stream
6868
ops := azdext.NewFrameworkServiceEnvelope()
69-
broker := grpcbroker.NewMessageBroker(stream, ops)
69+
broker := grpcbroker.NewMessageBroker(stream, ops, extension.Id)
7070

7171
// Track the language for cleanup when stream closes
7272
var registeredLanguage string
@@ -86,12 +86,9 @@ func (s *FrameworkService) Stream(stream azdext.FrameworkService_StreamServer) e
8686
// Run the broker dispatcher (blocking)
8787
// This will return when the stream closes or encounters an error
8888
if err := broker.Run(ctx); err != nil && !errors.Is(err, context.Canceled) {
89-
log.Printf("Broker error for framework service %s: %v", registeredLanguage, err)
9089
return fmt.Errorf("broker error: %w", err)
9190
}
9291

93-
log.Printf("Stream closed for framework service: %s", registeredLanguage)
94-
9592
s.providerMapMu.Lock()
9693
delete(s.providerMap, registeredLanguage)
9794
s.providerMapMu.Unlock()

cli/azd/internal/grpcserver/server.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ func (s *Server) Start() (*ServerInfo, error) {
119119
}
120120
}()
121121

122-
log.Printf("AZD Server listening on port %d", randomPort)
122+
log.Printf("AZD gRPC Server listening on port %d", randomPort)
123123

124124
return &ServerInfo{
125125
Address: fmt.Sprintf("localhost:%d", randomPort),
@@ -134,7 +134,7 @@ func (s *Server) Stop() error {
134134
}
135135

136136
s.grpcServer.Stop()
137-
log.Println("AZD Server stopped")
137+
log.Println("AZD gRPC Server stopped")
138138

139139
return nil
140140
}

cli/azd/internal/grpcserver/service_target_service.go

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ func (s *ServiceTargetService) Stream(stream azdext.ServiceTargetService_StreamS
7070

7171
// Create message broker for this stream
7272
ops := azdext.NewServiceTargetEnvelope()
73-
broker := grpcbroker.NewMessageBroker(stream, ops)
73+
broker := grpcbroker.NewMessageBroker(stream, ops, extension.Id)
7474

7575
// Track the hostType for cleanup when stream closes
7676
var registeredHostType string
@@ -94,8 +94,6 @@ func (s *ServiceTargetService) Stream(stream azdext.ServiceTargetService_StreamS
9494
return fmt.Errorf("broker error: %w", err)
9595
}
9696

97-
log.Printf("Stream closed for provider: %s", registeredHostType)
98-
9997
s.providerMapMu.Lock()
10098
delete(s.providerMap, registeredHostType)
10199
s.providerMapMu.Unlock()

cli/azd/main.go

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,14 @@ func main() {
4646

4747
log.SetFlags(log.LstdFlags | log.Lshortfile)
4848

49-
if isDebugEnabled() {
49+
debugEnabled := isDebugEnabled()
50+
logFileCleanup := setupLogging(debugEnabled)
51+
defer logFileCleanup()
52+
53+
if debugEnabled {
5054
azcorelog.SetListener(func(event azcorelog.Event, msg string) {
5155
log.Printf("%s: %s\n", event, msg)
5256
})
53-
} else {
54-
log.SetOutput(io.Discard)
5557
}
5658

5759
log.Printf("azd version: %s", internal.Version)
@@ -366,6 +368,69 @@ func readToEndAndClose(r io.ReadCloser) (string, error) {
366368
return buf.String(), err
367369
}
368370

371+
// setupLogging configures log output based on AZD_DEBUG_LOG environment variable
372+
// Returns a cleanup function that should be called when the program exits
373+
func setupLogging(debugEnabled bool) func() {
374+
debugLogValue := os.Getenv("AZD_DEBUG_LOG")
375+
376+
var logOutput io.Writer = io.Discard
377+
var cleanupFunc func() = func() {}
378+
379+
// Check if debug logging is enabled and valid
380+
if debugLogValue != "" {
381+
if isDebugLogEnabled, err := strconv.ParseBool(debugLogValue); err == nil && isDebugLogEnabled {
382+
// Create daily log files adjacent to azd binary
383+
if logFile, err := createDailyLogFile(); err == nil {
384+
if debugEnabled {
385+
// When --debug is used, write to both stderr and log file
386+
logOutput = io.MultiWriter(os.Stderr, logFile)
387+
} else {
388+
// When only AZD_DEBUG_LOG is set, write only to log file
389+
logOutput = logFile
390+
}
391+
392+
// Set cleanup function to close the log file
393+
cleanupFunc = func() {
394+
logFile.Close()
395+
}
396+
}
397+
}
398+
}
399+
400+
// If debug is enabled but no log file was created, use stderr
401+
if debugEnabled && logOutput == io.Discard {
402+
logOutput = os.Stderr
403+
}
404+
405+
log.SetOutput(logOutput)
406+
return cleanupFunc
407+
}
408+
409+
// createDailyLogFile creates a daily log file adjacent to the azd binary
410+
func createDailyLogFile() (*os.File, error) {
411+
// Get the path to the current executable
412+
execPath, err := os.Executable()
413+
if err != nil {
414+
return nil, fmt.Errorf("failed to get current executable path: %w", err)
415+
}
416+
417+
// Get the directory containing the executable
418+
execDir := filepath.Dir(execPath)
419+
420+
// Create log filename with current date
421+
currentDate := time.Now().Format("2006-01-02")
422+
logFileName := fmt.Sprintf("azd-%s.log", currentDate)
423+
logFilePath := filepath.Join(execDir, logFileName)
424+
425+
// Open or create the log file (append mode)
426+
logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
427+
if err != nil {
428+
return nil, fmt.Errorf("failed to create/open log file %s: %w", logFilePath, err)
429+
}
430+
431+
return logFile, nil
432+
}
433+
369434
func startBackgroundUploadProcess() error {
370435
// The background upload process executable is ourself
371436
execPath, err := os.Executable()

cli/azd/pkg/azdext/debugger.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import (
1717

1818
// waitForDebugger checks if AZD_EXT_DEBUG environment variable is set to a truthy value.
1919
// If set, prompts the user to attach a debugger to the current process.
20-
func waitForDebugger(ctx context.Context, azdClient *AzdClient) {
20+
func waitForDebugger(ctx context.Context, extensionId string, azdClient *AzdClient) {
2121
debugValue := os.Getenv("AZD_EXT_DEBUG")
2222
if debugValue == "" {
2323
return
@@ -28,7 +28,6 @@ func waitForDebugger(ctx context.Context, azdClient *AzdClient) {
2828
return
2929
}
3030

31-
extensionId := getExtensionId(ctx)
3231
message := fmt.Sprintf("Extension '%s' ready to debug (pid: %d).", extensionId, os.Getpid())
3332

3433
_, err = azdClient.Prompt().Confirm(ctx, &ConfirmRequest{

0 commit comments

Comments
 (0)