Skip to content

Commit 4329ce3

Browse files
committed
refactor: decompose god files, add DI constructors, migrate to slog
Split oversized files into focused modules: - logs.go (1906->808 lines): extracted parsing, formatting, streaming, filtering - checker.go (1077->394 lines): extracted HTTP, command, and process strategies Add dependency injection constructors: - wellknown.NewServiceRegistry() for injectable service definitions - service.NewOperationManager() alongside existing singleton Clarify executor naming: - Renamed service/executor.go to service/service_process.go with doc comments - Added service/doc.go documenting package domains Extract MCP handler boilerplate: - handleSingleServiceOp() eliminates repeated validation/controller setup - Refactored handleStartService and handleRestartService to use helper Migrate healthcheck logging from zerolog to log/slog: - checker.go, checker_http.go, monitor.go converted to structured slog calls - InitializeLogging rewritten using slog handlers Addresses: #233, #234, #236, #191, #185, #201 Partially addresses: #190 (doc.go added; full split deferred due to 43 importers)
1 parent ea834e1 commit 4329ce3

16 files changed

Lines changed: 2047 additions & 1924 deletions

cli/src/cmd/app/commands/logs.go

Lines changed: 0 additions & 1101 deletions
Large diffs are not rendered by default.
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
package commands
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"strings"
7+
"time"
8+
9+
"github.com/jongio/azd-app/cli/src/internal/azure"
10+
"github.com/jongio/azd-app/cli/src/internal/service"
11+
)
12+
13+
func (e *logsExecutor) extractLogsWithContext(logs []service.LogEntry, levelFilter service.LogLevel, contextLines int) []LogEntryWithContext {
14+
if len(logs) == 0 || contextLines <= 0 {
15+
return nil
16+
}
17+
18+
// Find indices of matching entries
19+
var matchIndices []int
20+
for i, entry := range logs {
21+
if entry.Level == levelFilter {
22+
matchIndices = append(matchIndices, i)
23+
}
24+
}
25+
26+
if len(matchIndices) == 0 {
27+
return nil
28+
}
29+
30+
// Build entries with context, handling overlapping ranges
31+
result := make([]LogEntryWithContext, 0, len(matchIndices))
32+
usedIndices := make(map[int]bool) // Track indices already shown in context
33+
34+
for _, matchIdx := range matchIndices {
35+
entry := logs[matchIdx]
36+
37+
// Extract before context
38+
startBefore := matchIdx - contextLines
39+
if startBefore < 0 {
40+
startBefore = 0
41+
}
42+
before := make([]string, 0, contextLines)
43+
for i := startBefore; i < matchIdx; i++ {
44+
// Skip if this line was already shown (to avoid duplicating context)
45+
if !usedIndices[i] {
46+
before = append(before, logs[i].Message)
47+
usedIndices[i] = true
48+
}
49+
}
50+
51+
// Extract after context
52+
endAfter := matchIdx + contextLines + 1
53+
if endAfter > len(logs) {
54+
endAfter = len(logs)
55+
}
56+
after := make([]string, 0, contextLines)
57+
for i := matchIdx + 1; i < endAfter; i++ {
58+
// Skip if this line was already shown (to avoid duplicating context)
59+
if !usedIndices[i] {
60+
after = append(after, logs[i].Message)
61+
usedIndices[i] = true
62+
}
63+
}
64+
65+
// Mark the match itself as used
66+
usedIndices[matchIdx] = true
67+
68+
// Build context only if we have any lines
69+
var ctx *LogContext
70+
if len(before) > 0 || len(after) > 0 {
71+
ctx = &LogContext{
72+
Before: before,
73+
After: after,
74+
}
75+
}
76+
77+
result = append(result, LogEntryWithContext{
78+
Service: entry.Service,
79+
Message: entry.Message,
80+
Level: logLevelToString(entry.Level),
81+
Timestamp: entry.Timestamp,
82+
IsStderr: entry.IsStderr,
83+
Context: ctx,
84+
})
85+
}
86+
87+
return result
88+
}
89+
90+
// logLevelToString converts a LogLevel to its string representation.
91+
func logLevelToString(level service.LogLevel) string {
92+
switch level {
93+
case service.LogLevelInfo:
94+
return "info"
95+
case service.LogLevelWarn:
96+
return logLevelWarn
97+
case service.LogLevelError:
98+
return statusError
99+
case service.LogLevelDebug:
100+
return logLevelDebug
101+
default:
102+
return "info"
103+
}
104+
}
105+
106+
// convertAzureLogLevel maps azure.LogLevel to the shared service.LogLevel enum.
107+
func convertAzureLogLevel(level azure.LogLevel) service.LogLevel {
108+
switch level {
109+
case azure.LogLevelWarn:
110+
return service.LogLevelWarn
111+
case azure.LogLevelError:
112+
return service.LogLevelError
113+
case azure.LogLevelDebug:
114+
return service.LogLevelDebug
115+
case azure.LogLevelInfo:
116+
fallthrough
117+
default:
118+
return service.LogLevelInfo
119+
}
120+
}
121+
122+
// buildLogFilterInternal creates a log filter from executor options and azure.yaml config.
123+
func (e *logsExecutor) buildLogFilterInternal(cwd string) (*service.LogFilter, error) {
124+
var customPatterns []string
125+
126+
// Parse command-line exclude patterns
127+
if e.opts.exclude != "" {
128+
customPatterns = service.ParseExcludePatterns(e.opts.exclude)
129+
}
130+
131+
// Try to load patterns from azure.yaml logs.filters section
132+
azureYaml, err := service.ParseAzureYaml(cwd)
133+
filterConfig := getFilterConfig(azureYaml, err)
134+
if filterConfig != nil {
135+
customPatterns = append(customPatterns, filterConfig.Exclude...)
136+
}
137+
138+
// Determine if we should include built-in patterns
139+
// CLI --no-builtins flag controls this; azure.yaml always includes builtins
140+
includeBuiltins := !e.opts.noBuiltins
141+
142+
// Build the filter
143+
if includeBuiltins {
144+
return service.NewLogFilterWithBuiltins(customPatterns)
145+
}
146+
return service.NewLogFilter(customPatterns)
147+
}
148+
149+
// getOrCreateSignalChan gets or creates a signal channel with proper cleanup.
150+
// This avoids duplication and race conditions in signal handling setup.
151+
152+
func (e *logsExecutor) shouldDisplayEntry(entry service.LogEntry, levelFilter service.LogLevel, logFilter *service.LogFilter) bool {
153+
// Filter by level
154+
if levelFilter != LogLevelAll && entry.Level != levelFilter {
155+
return false
156+
}
157+
158+
// Filter by pattern
159+
if logFilter != nil && logFilter.ShouldFilter(entry.Message) {
160+
return false
161+
}
162+
163+
return true
164+
}
165+
166+
// followLogs subscribes to live log streams and displays them.
167+
168+
func parseLogLevel(level string) service.LogLevel {
169+
switch strings.ToLower(level) {
170+
case "info":
171+
return service.LogLevelInfo
172+
case logLevelWarn, "warning":
173+
return service.LogLevelWarn
174+
case "error":
175+
return service.LogLevelError
176+
case logLevelDebug:
177+
return service.LogLevelDebug
178+
case "all":
179+
return LogLevelAll
180+
default:
181+
return LogLevelAll
182+
}
183+
}
184+
185+
// filterLogsByLevel filters logs by level with pre-allocated capacity.
186+
func filterLogsByLevel(logs []service.LogEntry, level service.LogLevel) []service.LogEntry {
187+
if level == LogLevelAll {
188+
return logs
189+
}
190+
191+
// Pre-allocate with estimated capacity based on typical match rate
192+
estimatedCap := len(logs) / filterCapacityEstimate
193+
if estimatedCap < 10 {
194+
estimatedCap = 10
195+
}
196+
filtered := make([]service.LogEntry, 0, estimatedCap)
197+
for _, entry := range logs {
198+
if entry.Level == level {
199+
filtered = append(filtered, entry)
200+
}
201+
}
202+
return filtered
203+
}
204+
205+
// buildLogFilter creates a log filter from options and azure.yaml config.
206+
// This is a test helper function that wraps buildLogFilterInternal.
207+
//
208+
// Deprecated: Use executor.buildLogFilterInternal directly in new code.
209+
func buildLogFilter(cwd string, exclude string, noBuiltins bool) (*service.LogFilter, error) {
210+
var customPatterns []string
211+
212+
// Parse command-line exclude patterns
213+
if exclude != "" {
214+
customPatterns = service.ParseExcludePatterns(exclude)
215+
}
216+
217+
// Try to load patterns from azure.yaml logs.filters section
218+
azureYaml, err := service.ParseAzureYaml(cwd)
219+
filterConfig := getFilterConfig(azureYaml, err)
220+
if filterConfig != nil {
221+
customPatterns = append(customPatterns, filterConfig.Exclude...)
222+
}
223+
224+
// Determine if we should include built-in patterns
225+
// CLI --no-builtins flag controls this; azure.yaml always includes builtins
226+
includeBuiltins := !noBuiltins
227+
228+
// Build the filter
229+
if includeBuiltins {
230+
return service.NewLogFilterWithBuiltins(customPatterns)
231+
}
232+
return service.NewLogFilter(customPatterns)
233+
}
234+
235+
// getFilterConfig extracts the filter config from azure.yaml's logs section.
236+
func getFilterConfig(azureYaml *service.AzureYaml, err error) *service.LogFilterConfig {
237+
if err != nil || azureYaml == nil {
238+
return nil
239+
}
240+
return azureYaml.Logs.GetFilters()
241+
}
242+
243+
// validateLogsOptions validates command-line flag values.
244+
func validateLogsOptions(opts *logsOptions) error {
245+
// Validate tail is positive
246+
if opts.tail < 0 {
247+
return fmt.Errorf("--tail must be a positive number, got %d", opts.tail)
248+
}
249+
if opts.tail > maxTailLines {
250+
// Log warning before capping
251+
fmt.Fprintf(os.Stderr, "Warning: --tail value %d exceeds maximum, capping at %d\n", opts.tail, maxTailLines)
252+
opts.tail = maxTailLines
253+
}
254+
255+
// Validate format
256+
switch opts.format {
257+
case "text", jsonOutputVal:
258+
// Valid formats
259+
default:
260+
return fmt.Errorf("--format must be 'text' or 'json', got '%s'", opts.format)
261+
}
262+
263+
// Validate level
264+
switch strings.ToLower(opts.level) {
265+
case "info", logLevelWarn, "warning", "error", logLevelDebug, "all":
266+
// Valid levels
267+
default:
268+
return fmt.Errorf("--level must be one of: info, warn, error, debug, all; got '%s'", opts.level)
269+
}
270+
271+
// Validate context requires level to be set (not "all")
272+
if opts.contextLines > 0 {
273+
if strings.ToLower(opts.level) == "all" {
274+
return fmt.Errorf("--context requires --level to be set (info, warn, error, or debug)")
275+
}
276+
}
277+
278+
// Clamp context to valid range (0-MaxContextLines)
279+
if opts.contextLines < 0 {
280+
opts.contextLines = 0
281+
}
282+
if opts.contextLines > service.MaxContextLines {
283+
fmt.Fprintf(os.Stderr, "Warning: --context value %d exceeds maximum, capping at %d\n", opts.contextLines, service.MaxContextLines)
284+
opts.contextLines = service.MaxContextLines
285+
}
286+
287+
// Validate since duration if provided
288+
if opts.since != "" {
289+
if _, err := time.ParseDuration(opts.since); err != nil {
290+
return fmt.Errorf("--since must be a valid duration (e.g., 5m, 1h), got '%s': %w", opts.since, err)
291+
}
292+
}
293+
294+
// Validate source
295+
switch strings.ToLower(opts.source) {
296+
case string(LogSourceLocal), string(LogSourceAzure), string(LogSourceAll):
297+
// Valid sources - normalize to lowercase
298+
opts.source = strings.ToLower(opts.source)
299+
case "":
300+
// Default to local if not specified
301+
opts.source = string(LogSourceLocal)
302+
default:
303+
return fmt.Errorf("--source must be 'local', 'azure', or 'all'; got '%s'", opts.source)
304+
}
305+
306+
return nil
307+
}

0 commit comments

Comments
 (0)