Skip to content

Commit 74c6e7b

Browse files
authored
Redesign console output for better UX and readability (#42)
1 parent 71d4da2 commit 74c6e7b

File tree

6 files changed

+654
-10
lines changed

6 files changed

+654
-10
lines changed

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,28 @@ var Version = "dev"
1111
// BuildTime is set at build time via -ldflags.
1212
var BuildTime = "unknown"
1313

14+
// VersionInfo represents version information for JSON output.
15+
type VersionInfo struct {
16+
Version string `json:"version"`
17+
BuildTime string `json:"buildTime"`
18+
}
19+
1420
// NewVersionCommand creates the version command.
1521
func NewVersionCommand() *cobra.Command {
1622
return &cobra.Command{
1723
Use: "version",
1824
Short: "Show version information",
1925
Long: `Display the version of the azd app extension.`,
2026
RunE: func(cmd *cobra.Command, args []string) error {
27+
// JSON output
28+
if output.IsJSON() {
29+
return output.PrintJSON(VersionInfo{
30+
Version: Version,
31+
BuildTime: BuildTime,
32+
})
33+
}
34+
35+
// Default output
2136
output.Header("azd app extension")
2237
output.Label("Version", Version)
2338
output.Label("Built", BuildTime)

cli/src/cmd/app/main.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,16 @@ import (
55
"os"
66

77
"github.com/jongio/azd-app/cli/src/cmd/app/commands"
8+
"github.com/jongio/azd-app/cli/src/internal/logging"
89
"github.com/jongio/azd-app/cli/src/internal/output"
910

1011
"github.com/spf13/cobra"
1112
)
1213

1314
var (
14-
outputFormat string
15-
debugMode bool
15+
outputFormat string
16+
debugMode bool
17+
structuredLogs bool
1618
)
1719

1820
func main() {
@@ -25,13 +27,27 @@ func main() {
2527
if debugMode {
2628
os.Setenv("AZD_APP_DEBUG", "true")
2729
}
30+
31+
// Configure logging
32+
logging.SetupLogger(debugMode, structuredLogs)
33+
34+
// Log startup in debug mode
35+
if debugMode {
36+
logging.Debug("Starting azd app extension",
37+
"version", commands.Version,
38+
"command", cmd.Name(),
39+
"args", args,
40+
)
41+
}
42+
2843
return output.SetFormat(outputFormat)
2944
},
3045
}
3146

3247
// Add global flags
3348
rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "default", "Output format (default, json)")
3449
rootCmd.PersistentFlags().BoolVar(&debugMode, "debug", false, "Enable debug logging")
50+
rootCmd.PersistentFlags().BoolVar(&structuredLogs, "structured-logs", false, "Enable structured JSON logging to stderr")
3551

3652
// Register all commands
3753
rootCmd.AddCommand(

cli/src/internal/logging/logger.go

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
package logging
2+
3+
import (
4+
"io"
5+
"log/slog"
6+
"os"
7+
"strings"
8+
)
9+
10+
// Level represents the logging level.
11+
type Level int
12+
13+
const (
14+
// LevelDebug is for debug messages.
15+
LevelDebug Level = iota
16+
// LevelInfo is for informational messages.
17+
LevelInfo
18+
// LevelWarn is for warnings.
19+
LevelWarn
20+
// LevelError is for errors.
21+
LevelError
22+
)
23+
24+
var (
25+
// globalLogger is the default logger instance
26+
globalLogger *slog.Logger
27+
// currentLevel is the current log level
28+
currentLevel = LevelInfo
29+
// isStructured tracks whether structured JSON logging is enabled
30+
isStructured = false
31+
// outputWriter tracks the current output destination
32+
outputWriter io.Writer = os.Stderr
33+
)
34+
35+
func init() {
36+
// Initialize with default logger (no-op for non-debug mode)
37+
SetupLogger(false, false)
38+
}
39+
40+
// SetupLogger configures the global logger.
41+
// debug enables debug-level logging
42+
// structured enables structured JSON logging
43+
func SetupLogger(debug, structured bool) {
44+
var level slog.Level
45+
if debug {
46+
level = slog.LevelDebug
47+
currentLevel = LevelDebug
48+
} else {
49+
level = slog.LevelInfo
50+
currentLevel = LevelInfo
51+
}
52+
53+
// Track the handler type for later use
54+
isStructured = structured
55+
outputWriter = os.Stderr
56+
57+
var handler slog.Handler
58+
opts := &slog.HandlerOptions{
59+
Level: level,
60+
}
61+
62+
if structured {
63+
// JSON structured logging
64+
handler = slog.NewJSONHandler(outputWriter, opts)
65+
} else {
66+
// Text logging for human consumption
67+
handler = slog.NewTextHandler(outputWriter, opts)
68+
}
69+
70+
globalLogger = slog.New(handler)
71+
slog.SetDefault(globalLogger)
72+
}
73+
74+
// SetLevel sets the logging level.
75+
func SetLevel(level Level) {
76+
currentLevel = level
77+
var slogLevel slog.Level
78+
switch level {
79+
case LevelDebug:
80+
slogLevel = slog.LevelDebug
81+
case LevelInfo:
82+
slogLevel = slog.LevelInfo
83+
case LevelWarn:
84+
slogLevel = slog.LevelWarn
85+
case LevelError:
86+
slogLevel = slog.LevelError
87+
default:
88+
slogLevel = slog.LevelInfo
89+
}
90+
91+
// Preserve the handler type (structured vs text) when changing levels
92+
opts := &slog.HandlerOptions{Level: slogLevel}
93+
var handler slog.Handler
94+
if isStructured {
95+
handler = slog.NewJSONHandler(outputWriter, opts)
96+
} else {
97+
handler = slog.NewTextHandler(outputWriter, opts)
98+
}
99+
globalLogger = slog.New(handler)
100+
slog.SetDefault(globalLogger)
101+
}
102+
103+
// SetOutput sets the output destination for logs.
104+
func SetOutput(w io.Writer) {
105+
outputWriter = w
106+
107+
var slogLevel slog.Level
108+
switch currentLevel {
109+
case LevelDebug:
110+
slogLevel = slog.LevelDebug
111+
case LevelInfo:
112+
slogLevel = slog.LevelInfo
113+
case LevelWarn:
114+
slogLevel = slog.LevelWarn
115+
case LevelError:
116+
slogLevel = slog.LevelError
117+
default:
118+
slogLevel = slog.LevelInfo
119+
}
120+
121+
opts := &slog.HandlerOptions{Level: slogLevel}
122+
var handler slog.Handler
123+
if isStructured {
124+
handler = slog.NewJSONHandler(outputWriter, opts)
125+
} else {
126+
handler = slog.NewTextHandler(outputWriter, opts)
127+
}
128+
globalLogger = slog.New(handler)
129+
slog.SetDefault(globalLogger)
130+
}
131+
132+
// IsDebugEnabled returns true if debug logging is enabled.
133+
func IsDebugEnabled() bool {
134+
return currentLevel == LevelDebug || os.Getenv("AZD_APP_DEBUG") == "true"
135+
}
136+
137+
// Debug logs a debug message with optional key-value pairs.
138+
func Debug(msg string, args ...any) {
139+
if IsDebugEnabled() {
140+
globalLogger.Debug(msg, args...)
141+
}
142+
}
143+
144+
// Info logs an info message with optional key-value pairs.
145+
func Info(msg string, args ...any) {
146+
globalLogger.Info(msg, args...)
147+
}
148+
149+
// Warn logs a warning message with optional key-value pairs.
150+
func Warn(msg string, args ...any) {
151+
globalLogger.Warn(msg, args...)
152+
}
153+
154+
// Error logs an error message with optional key-value pairs.
155+
func Error(msg string, args ...any) {
156+
globalLogger.Error(msg, args...)
157+
}
158+
159+
// With creates a new logger with the given attributes.
160+
func With(args ...any) *slog.Logger {
161+
return globalLogger.With(args...)
162+
}
163+
164+
// ParseLevel parses a string into a Level.
165+
func ParseLevel(s string) Level {
166+
switch strings.ToLower(s) {
167+
case "debug":
168+
return LevelDebug
169+
case "info":
170+
return LevelInfo
171+
case "warn", "warning":
172+
return LevelWarn
173+
case "error":
174+
return LevelError
175+
default:
176+
return LevelInfo
177+
}
178+
}

0 commit comments

Comments
 (0)