diff --git a/CHANGELOG.md b/CHANGELOG.md index bd6387a..2493244 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## v0.4.0 - 2026-10-08 + +### Breaking Changes + +- Logging configuration now lives in the config file under top-level `log` settings instead of `LOG_FORMAT` and `LOG_LEVEL` environment variables. + + ```yaml + log: + format: text + level: debug + ``` + +### Changed + +- `logger.Setup` now receives `config.Log`, keeping logger configuration with the rest of the application config. + ## v0.3.0 - 2026-10-08 ### Breaking Changes diff --git a/README.md b/README.md index 0a26bf6..cec46b1 100644 --- a/README.md +++ b/README.md @@ -217,13 +217,13 @@ Run the agent: GITHUB_TOKEN=your_token_here go run . -config your-config.yaml ``` -Logging is JSON by default. +Logging is JSON at `info` level by default. Configure logging in your config +file when you want a different format or level: -Optional logging environment variables: - -```sh -LOG_FORMAT=text -LOG_LEVEL=debug +```yaml +log: + format: text + level: debug ``` ## Operating Model @@ -254,7 +254,7 @@ Example: ```sh make -f ./test/Makefile dev-up -GITHUB_TOKEN=your_token_here LOG_FORMAT=text go run . -config ./test/sample.config.yaml +GITHUB_TOKEN=your_token_here go run . -config ./test/sample.config.yaml ``` The sample deploy command calls: @@ -280,6 +280,8 @@ Required workload fields: Common optional fields: +- `log.format`: log output format. Set to `text` for local development; JSON is the default. +- `log.level`: minimum log level. Supports `debug`, `info`, `warn`, and `error`; defaults to `info`. - `release_prefix`: release tag prefix used to scope metadata lookup. Recommended when a repository publishes more than one deployable artifact stream. - `notification_url`: Slack webhook URL. - `check_interval`: defaults to `60s` when below `10s`. diff --git a/config/config.go b/config/config.go index 8897d67..7bdc5e7 100644 --- a/config/config.go +++ b/config/config.go @@ -13,12 +13,25 @@ import ( ) type Config struct { + // Log is optional and controls reconD's process-level logging output. + Log Log `yaml:"log"` + // Workloads is the required list of container workloads reconD should watch // and reconcile. Configure one entry for each independently deployed // service. Workloads []Workload `yaml:"workloads"` } +type Log struct { + // Format is optional and controls the log handler format. Set it to "text" + // for local development; any other value uses structured JSON output. + Format string `yaml:"format"` + + // Level is optional and controls the minimum log level. Supported values are + // "debug", "info", "warn", and "error"; empty defaults to "info". + Level string `yaml:"level"` +} + // Workload is a standalone unit of deployment (e.g., a container) reconD can watch and reconcile type Workload struct { // Name is required and identifies this workload in logs, notifications, and diff --git a/config/config_test.go b/config/config_test.go index d572e9b..aa649dd 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -39,6 +39,9 @@ func TestLoadWorkloadListYAML(t *testing.T) { path := filepath.Join(t.TempDir(), "workloads.yml") require.NoError(t, os.WriteFile(path, []byte(` +log: + format: text + level: debug workloads: - name: api release_prefix: api-prod @@ -64,6 +67,8 @@ workloads: cfg, err := Load(path) require.NoError(t, err) + require.Equal(t, "text", cfg.Log.Format) + require.Equal(t, "debug", cfg.Log.Level) require.Len(t, cfg.Workloads, 2) require.Equal(t, "api", cfg.Workloads[0].Name) require.Equal(t, "api-prod", cfg.Workloads[0].ReleasePrefix) diff --git a/logger/logger.go b/logger/logger.go index b3f8c44..8bdf583 100644 --- a/logger/logger.go +++ b/logger/logger.go @@ -2,29 +2,27 @@ // // Design decisions: // - JSON output by default for log aggregation (journald, Loki, Datadog) -// - Text output when LOG_FORMAT=text (local development) -// - Level controlled by LOG_LEVEL env var (debug, info, warn, error) +// - Text output when log.format=text (local development) +// - Level controlled by log.level (debug, info, warn, error) // - Structured fields: workload, phase, digest, duration included consistently package logger import ( - "context" "log/slog" "os" "strings" -) -// contextKey is the key for the logger stored in context.Context. -type contextKey struct{} + "github.com/ercross/reconD/config" +) // Setup configures the default global slog logger. // Call once at startup before spawning goroutines. -func Setup() *slog.Logger { - level := parseLevel(os.Getenv("LOG_LEVEL")) +func Setup(cfg config.Log) *slog.Logger { + level := parseLevel(cfg.Level) opts := &slog.HandlerOptions{Level: level} var handler slog.Handler - if strings.ToLower(os.Getenv("LOG_FORMAT")) == "text" { + if strings.ToLower(cfg.Format) == "text" { handler = slog.NewTextHandler(os.Stdout, opts) } else { // JSON is the default: structured logs integrate with journald and @@ -43,20 +41,6 @@ func WithContainerName(logger *slog.Logger, containerName string) *slog.Logger { return logger.With("container", containerName) } -// WithContext stores logger in ctx, enabling logger retrieval in deep call stacks. -func WithContext(ctx context.Context, logger *slog.Logger) context.Context { - return context.WithValue(ctx, contextKey{}, logger) -} - -// FromContext retrieves the logger stored in ctx. -// Falls back to the default slog logger if none is stored. -func FromContext(ctx context.Context) *slog.Logger { - if l, ok := ctx.Value(contextKey{}).(*slog.Logger); ok { - return l - } - return slog.Default() -} - func parseLevel(s string) slog.Level { switch strings.ToLower(s) { case "debug": @@ -77,7 +61,6 @@ const ( PhaseDrift = "drift_check" PhasePull = "image_pull" PhaseStrategy = "deployment_strategy" - PhaseMigration = "migration" PhaseRestart = "restart" PhaseHealthCheck = "health_check" PhaseCommit = "commit" diff --git a/main.go b/main.go index c6536a7..ebe89b8 100644 --- a/main.go +++ b/main.go @@ -22,7 +22,7 @@ import ( func main() { if err := run(); err != nil { - fmt.Fprintf(os.Stderr, "agent error: %v\n", err) + _, _ = fmt.Fprintf(os.Stderr, "agent error: %v\n", err) os.Exit(1) } } @@ -34,14 +34,14 @@ func run() error { return fmt.Errorf("missing config path") } - log := logger.Setup() - log.Info("deployment agent starting", "config", *configPath) - cfg, err := config.Load(*configPath) if err != nil { return fmt.Errorf("load config error %q: %w", *configPath, err) } + log := logger.Setup(cfg.Log) + log.Info("deployment agent starting", "config", *configPath) + ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGTERM, syscall.SIGINT) defer stop()