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:
+14
-14
@@ -36,18 +36,18 @@ Version: %s
|
|||||||
`
|
`
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Print banner
|
// Print banner to stderr so it doesn't mix with structured log output on stdout
|
||||||
fmt.Printf(Banner, Version)
|
fmt.Fprintf(os.Stderr, Banner, Version)
|
||||||
fmt.Println()
|
fmt.Fprintln(os.Stderr)
|
||||||
|
|
||||||
// Load configuration
|
// Setup logger first, before anything else, so all messages use consistent format
|
||||||
cfg := config.Load()
|
slogger, secrets := config.SetupLogger()
|
||||||
cfg.Version = Version
|
|
||||||
|
|
||||||
// Setup logger
|
|
||||||
slogger, secrets := cfg.SetupLogger()
|
|
||||||
slog.SetDefault(slogger)
|
slog.SetDefault(slogger)
|
||||||
|
|
||||||
|
// Load configuration (uses the logger for startup messages)
|
||||||
|
cfg := config.Load(slogger)
|
||||||
|
cfg.Version = Version
|
||||||
|
|
||||||
// Create adapter for our interface
|
// Create adapter for our interface
|
||||||
log := logger.NewAdapter(slogger, secrets)
|
log := logger.NewAdapter(slogger, secrets)
|
||||||
|
|
||||||
@@ -193,10 +193,10 @@ func printEndpoints(listen string) {
|
|||||||
// ANSI escape codes for clickable link (OSC 8 hyperlink)
|
// ANSI escape codes for clickable link (OSC 8 hyperlink)
|
||||||
clickableURL := fmt.Sprintf("\033]8;;%s\033\\%s\033]8;;\033\\", url, url)
|
clickableURL := fmt.Sprintf("\033]8;;%s\033\\%s\033]8;;\033\\", url, url)
|
||||||
|
|
||||||
fmt.Println("\n🌐 Web Interface:")
|
fmt.Fprintln(os.Stderr, "\nWeb Interface:")
|
||||||
fmt.Println("────────────────────────────────────────────────")
|
fmt.Fprintln(os.Stderr, "────────────────────────────────────────────────")
|
||||||
fmt.Printf(" Open in browser: %s\n", clickableURL)
|
fmt.Fprintf(os.Stderr, " Open in browser: %s\n", clickableURL)
|
||||||
fmt.Println("────────────────────────────────────────────────")
|
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
@@ -69,8 +69,10 @@ type yamlConfig struct {
|
|||||||
} `yaml:"api"`
|
} `yaml:"api"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load returns configuration with defaults
|
// Load returns configuration with defaults. The provided logger is used for
|
||||||
func Load() *Config {
|
// 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")
|
dataPath := getEnv("STRIX_DATA_PATH", "./data")
|
||||||
|
|
||||||
cfg := &Config{
|
cfg := &Config{
|
||||||
@@ -127,14 +129,19 @@ func Load() *Config {
|
|||||||
|
|
||||||
// Validate listen address
|
// Validate listen address
|
||||||
if err := validateListen(cfg.Server.Listen); err != nil {
|
if err := validateListen(cfg.Server.Listen); err != nil {
|
||||||
fmt.Fprintf(os.Stderr, "ERROR: Invalid listen address '%s': %v\n", cfg.Server.Listen, err)
|
log.Error("invalid listen address, using default :4567",
|
||||||
fmt.Fprintf(os.Stderr, "Using default: :4567\n")
|
slog.String("address", cfg.Server.Listen),
|
||||||
|
slog.String("error", err.Error()),
|
||||||
|
)
|
||||||
cfg.Server.Listen = ":4567"
|
cfg.Server.Listen = ":4567"
|
||||||
configSource = "default (validation failed)"
|
configSource = "default (validation failed)"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log configuration source
|
// 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
|
return cfg
|
||||||
}
|
}
|
||||||
@@ -226,12 +233,30 @@ func validateListen(listen string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupLogger configures the global logger. It returns the logger and a
|
// SetupLogger creates the application logger by reading log configuration
|
||||||
// SecretStore that can be used to register credentials for automatic masking
|
// from environment variables and Home Assistant options. It must be called
|
||||||
// in all log output.
|
// before Load() so that all startup messages use a consistent output format.
|
||||||
func (c *Config) SetupLogger() (*slog.Logger, *logger.SecretStore) {
|
//
|
||||||
|
// 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
|
var level slog.Level
|
||||||
switch c.Logger.Level {
|
switch logLevel {
|
||||||
case "debug":
|
case "debug":
|
||||||
level = slog.LevelDebug
|
level = slog.LevelDebug
|
||||||
case "warn":
|
case "warn":
|
||||||
@@ -242,15 +267,15 @@ func (c *Config) SetupLogger() (*slog.Logger, *logger.SecretStore) {
|
|||||||
level = slog.LevelInfo
|
level = slog.LevelInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
opts := &slog.HandlerOptions{
|
handlerOpts := &slog.HandlerOptions{
|
||||||
Level: level,
|
Level: level,
|
||||||
}
|
}
|
||||||
|
|
||||||
var handler slog.Handler
|
var handler slog.Handler
|
||||||
if c.Logger.Format == "json" {
|
if logFormat == "json" {
|
||||||
handler = slog.NewJSONHandler(os.Stdout, opts)
|
handler = slog.NewJSONHandler(os.Stdout, handlerOpts)
|
||||||
} else {
|
} else {
|
||||||
handler = slog.NewTextHandler(os.Stdout, opts)
|
handler = slog.NewTextHandler(os.Stdout, handlerOpts)
|
||||||
}
|
}
|
||||||
|
|
||||||
secrets := logger.NewSecretStore()
|
secrets := logger.NewSecretStore()
|
||||||
|
|||||||
Reference in New Issue
Block a user