Separate structured logs from human-readable output

Move SetupLogger() to a standalone function called before config.Load()
so the logger is available from the very start. Replace all fmt.Printf
calls in config.go with slog calls. Redirect banner and endpoint info
to stderr, keeping stdout clean for structured log output (JSON/text).

Fixes #5
This commit is contained in:
eduard256
2026-03-22 17:44:16 +00:00
parent 4fe5ae9447
commit 3fafdbc6ce
2 changed files with 53 additions and 28 deletions
+14 -14
View File
@@ -36,18 +36,18 @@ Version: %s
`
func main() {
// Print banner
fmt.Printf(Banner, Version)
fmt.Println()
// Print banner to stderr so it doesn't mix with structured log output on stdout
fmt.Fprintf(os.Stderr, Banner, Version)
fmt.Fprintln(os.Stderr)
// Load configuration
cfg := config.Load()
cfg.Version = Version
// Setup logger
slogger, secrets := cfg.SetupLogger()
// Setup logger first, before anything else, so all messages use consistent format
slogger, secrets := config.SetupLogger()
slog.SetDefault(slogger)
// Load configuration (uses the logger for startup messages)
cfg := config.Load(slogger)
cfg.Version = Version
// Create adapter for our interface
log := logger.NewAdapter(slogger, secrets)
@@ -193,10 +193,10 @@ func printEndpoints(listen string) {
// ANSI escape codes for clickable link (OSC 8 hyperlink)
clickableURL := fmt.Sprintf("\033]8;;%s\033\\%s\033]8;;\033\\", url, url)
fmt.Println("\n🌐 Web Interface:")
fmt.Println("────────────────────────────────────────────────")
fmt.Printf(" Open in browser: %s\n", clickableURL)
fmt.Println("────────────────────────────────────────────────")
fmt.Fprintln(os.Stderr, "\nWeb Interface:")
fmt.Fprintln(os.Stderr, "────────────────────────────────────────────────")
fmt.Fprintf(os.Stderr, " Open in browser: %s\n", clickableURL)
fmt.Fprintln(os.Stderr, "────────────────────────────────────────────────")
fmt.Println("\n📚 Documentation: https://github.com/eduard256/Strix")
fmt.Fprintln(os.Stderr, "\nDocumentation: https://github.com/eduard256/Strix")
}
+39 -14
View File
@@ -69,8 +69,10 @@ type yamlConfig struct {
} `yaml:"api"`
}
// Load returns configuration with defaults
func Load() *Config {
// Load returns configuration with defaults. The provided logger is used for
// all startup messages so that output format stays consistent (JSON or text)
// with the rest of the application logs.
func Load(log *slog.Logger) *Config {
dataPath := getEnv("STRIX_DATA_PATH", "./data")
cfg := &Config{
@@ -127,14 +129,19 @@ func Load() *Config {
// Validate listen address
if err := validateListen(cfg.Server.Listen); err != nil {
fmt.Fprintf(os.Stderr, "ERROR: Invalid listen address '%s': %v\n", cfg.Server.Listen, err)
fmt.Fprintf(os.Stderr, "Using default: :4567\n")
log.Error("invalid listen address, using default :4567",
slog.String("address", cfg.Server.Listen),
slog.String("error", err.Error()),
)
cfg.Server.Listen = ":4567"
configSource = "default (validation failed)"
}
// Log configuration source
fmt.Printf("INFO: API listen address '%s' loaded from %s\n", cfg.Server.Listen, configSource)
log.Info("configuration loaded",
slog.String("listen", cfg.Server.Listen),
slog.String("source", configSource),
)
return cfg
}
@@ -226,12 +233,30 @@ func validateListen(listen string) error {
return nil
}
// SetupLogger configures the global logger. It returns the logger and a
// SecretStore that can be used to register credentials for automatic masking
// in all log output.
func (c *Config) SetupLogger() (*slog.Logger, *logger.SecretStore) {
// SetupLogger creates the application logger by reading log configuration
// from environment variables and Home Assistant options. It must be called
// before Load() so that all startup messages use a consistent output format.
//
// Configuration priority: defaults < HA options < environment variables.
func SetupLogger() (*slog.Logger, *logger.SecretStore) {
// Read log settings from environment (same defaults as Config)
logLevel := getEnv("STRIX_LOG_LEVEL", "info")
logFormat := getEnv("STRIX_LOG_FORMAT", "json")
// Apply Home Assistant overrides if running as HA add-on
if data, err := os.ReadFile("/data/options.json"); err == nil {
var opts haOptions
if err := json.Unmarshal(data, &opts); err == nil {
if opts.LogLevel != "" {
logLevel = opts.LogLevel
}
// Home Assistant add-on always uses JSON logging for the HA log viewer
logFormat = "json"
}
}
var level slog.Level
switch c.Logger.Level {
switch logLevel {
case "debug":
level = slog.LevelDebug
case "warn":
@@ -242,15 +267,15 @@ func (c *Config) SetupLogger() (*slog.Logger, *logger.SecretStore) {
level = slog.LevelInfo
}
opts := &slog.HandlerOptions{
handlerOpts := &slog.HandlerOptions{
Level: level,
}
var handler slog.Handler
if c.Logger.Format == "json" {
handler = slog.NewJSONHandler(os.Stdout, opts)
if logFormat == "json" {
handler = slog.NewJSONHandler(os.Stdout, handlerOpts)
} else {
handler = slog.NewTextHandler(os.Stdout, opts)
handler = slog.NewTextHandler(os.Stdout, handlerOpts)
}
secrets := logger.NewSecretStore()