diff --git a/cmd/clonepack/serve.go b/cmd/clonepack/serve.go index 9846d56..4755b30 100644 --- a/cmd/clonepack/serve.go +++ b/cmd/clonepack/serve.go @@ -3,12 +3,12 @@ package main import ( "context" "fmt" - "log" + "log/slog" "net/http" "os" - "time" "os/signal" "syscall" + "time" "github.com/spf13/cobra" "github.com/syonad/clonepack/config" @@ -26,6 +26,8 @@ var serveCmd = &cobra.Command{ return fmt.Errorf("load config: %w", err) } + setupLogger(cfg.Log) + db, err := store.Open(cfg.DB) if err != nil { return fmt.Errorf("open store: %w", err) @@ -58,17 +60,28 @@ var serveCmd = &cobra.Command{ srv := &http.Server{Addr: addr, Handler: router} go func() { - log.Printf("ClonePack listening on %s", addr) + slog.Info("ClonePack listening", "addr", addr) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Printf("server error: %v", err) + slog.Error("server error", "error", err) } }() <-ctx.Done() - log.Printf("shutting down...") + slog.Info("shutting down...") shutdownCtx, cancel2 := context.WithTimeout(context.Background(), 30*time.Second) defer cancel2() return srv.Shutdown(shutdownCtx) }, } + +func setupLogger(cfg config.LogConfig) { + opts := &slog.HandlerOptions{Level: cfg.SlogLevel()} + var handler slog.Handler + if cfg.Format == "json" { + handler = slog.NewJSONHandler(os.Stderr, opts) + } else { + handler = slog.NewTextHandler(os.Stderr, opts) + } + slog.SetDefault(slog.New(handler)) +} diff --git a/config/config.go b/config/config.go index d3a4cb0..de1dcee 100644 --- a/config/config.go +++ b/config/config.go @@ -1,12 +1,16 @@ package config -import "time" +import ( + "log/slog" + "time" +) type Config struct { Server ServerConfig `mapstructure:"server"` DB DBConfig `mapstructure:"db"` DataDir string `mapstructure:"data_dir"` Sync SyncConfig `mapstructure:"sync"` + Log LogConfig `mapstructure:"log"` } type ServerConfig struct { @@ -22,6 +26,24 @@ type SyncConfig struct { Interval string `mapstructure:"interval"` } +type LogConfig struct { + Level string `mapstructure:"level"` + Format string `mapstructure:"format"` +} + +func (l LogConfig) SlogLevel() slog.Level { + switch l.Level { + case "debug": + return slog.LevelDebug + case "warn", "warning": + return slog.LevelWarn + case "error": + return slog.LevelError + default: + return slog.LevelInfo + } +} + func (s SyncConfig) IntervalDuration() time.Duration { d, err := time.ParseDuration(s.Interval) if err != nil || d <= 0 { diff --git a/config/loader.go b/config/loader.go index 2b00112..749822e 100644 --- a/config/loader.go +++ b/config/loader.go @@ -26,6 +26,8 @@ func Load(cfgFile string) (*Config, error) { viper.SetDefault("db.path", "./clonepack.db") viper.SetDefault("data_dir", "./data") viper.SetDefault("sync.interval", "1h") + viper.SetDefault("log.level", "info") + viper.SetDefault("log.format", "text") if err := viper.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { diff --git a/internal/api/sync_handler.go b/internal/api/sync_handler.go index 1b66735..987eb4d 100644 --- a/internal/api/sync_handler.go +++ b/internal/api/sync_handler.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "errors" - "log" + "log/slog" "net/http" "strconv" @@ -37,7 +37,7 @@ func (h *SyncHandler) Trigger(w http.ResponseWriter, r *http.Request) { } go func() { if err := h.svc.ScanRepo(context.Background(), repoID); err != nil { - log.Printf("scan repo %d: %v", repoID, err) + slog.Warn("scan repo failed", "repo_id", repoID, "error", err) } }() JSON(w, http.StatusAccepted, map[string]string{"status": "scan started"}) @@ -85,7 +85,7 @@ func (h *SyncHandler) Approve(w http.ResponseWriter, r *http.Request) { ids := req.IDs go func() { if err := h.svc.ApprovePending(context.Background(), repoID, ids); err != nil { - log.Printf("approve pending for repo %d: %v", repoID, err) + slog.Warn("approve pending failed", "repo_id", repoID, "error", err) } }() JSON(w, http.StatusAccepted, map[string]string{"status": "approval started"}) diff --git a/internal/core/clone.go b/internal/core/clone.go index 1901392..c023ccc 100644 --- a/internal/core/clone.go +++ b/internal/core/clone.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "log" + "log/slog" "path/filepath" aptclone "github.com/syonad/clonepack/internal/clone/apt" @@ -59,7 +59,7 @@ func (s *CloneService) runClone(repoID, jobID int64, repo *store.Repo) { ctx := context.Background() if err := s.jobStore.MarkCloneJobStarted(ctx, jobID); err != nil { - log.Printf("clone job %d: failed to mark started: %v", jobID, err) + slog.Error("clone job: failed to mark started", "job_id", jobID, "error", err) return } @@ -70,12 +70,12 @@ func (s *CloneService) runClone(repoID, jobID int64, repo *store.Repo) { case "rpm": cloner := rpm.New(repo.SourceURL, destDir) cloner.OnProgress = func(p rpm.Progress) { - log.Printf("clone job %d: %s (%d bytes)", jobID, p.File, p.BytesDone) + slog.Debug("clone progress", "job_id", jobID, "file", p.File, "bytes", p.BytesDone) } if err := cloner.Clone(ctx); err != nil { cloneErr = err } else if err := rpm.RegenerateMetadata(destDir); err != nil { - log.Printf("clone job %d: metadata regeneration failed: %v", jobID, err) + slog.Warn("clone job: metadata regeneration failed", "job_id", jobID, "error", err) } case "apt": @@ -86,7 +86,7 @@ func (s *CloneService) runClone(repoID, jobID int64, repo *store.Repo) { } cloner := aptclone.New(repo.SourceURL, destDir, cfg) cloner.OnProgress = func(file string, bytes int64) { - log.Printf("clone job %d: %s (%d bytes)", jobID, file, bytes) + slog.Debug("clone progress", "job_id", jobID, "file", file, "bytes", bytes) } cloneErr = cloner.Clone(ctx) } @@ -94,10 +94,10 @@ func (s *CloneService) runClone(repoID, jobID int64, repo *store.Repo) { if cloneErr != nil { errMsg := cloneErr.Error() _ = s.jobStore.MarkCloneJobFinished(ctx, jobID, store.CloneJobFailed, &errMsg) - log.Printf("clone job %d: failed: %v", jobID, cloneErr) + slog.Error("clone job failed", "job_id", jobID, "error", cloneErr) return } _ = s.jobStore.MarkCloneJobFinished(ctx, jobID, store.CloneJobCompleted, nil) - log.Printf("clone job %d: completed", jobID) + slog.Info("clone job completed", "job_id", jobID) } diff --git a/internal/core/repo.go b/internal/core/repo.go index 561ade9..6a83b42 100644 --- a/internal/core/repo.go +++ b/internal/core/repo.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" "fmt" - "log" + "log/slog" "path/filepath" aptclone "github.com/syonad/clonepack/internal/clone/apt" @@ -92,16 +92,16 @@ func (s *RepoService) initStorage(repo *store.Repo) { switch repo.Type { case "rpm": if err := rpmclone.InitEmptyRepo(localDir); err != nil { - log.Printf("init storage for repo %d: %v", repo.ID, err) + slog.Warn("init storage failed", "repo_id", repo.ID, "error", err) } case "apt": var cfg aptclone.Config if err := json.Unmarshal([]byte(repo.Config), &cfg); err != nil { - log.Printf("init storage for repo %d: parse config: %v", repo.ID, err) + slog.Warn("init storage: parse config failed", "repo_id", repo.ID, "error", err) return } if err := aptclone.InitEmptyRepo(localDir, cfg); err != nil { - log.Printf("init storage for repo %d: %v", repo.ID, err) + slog.Warn("init storage failed", "repo_id", repo.ID, "error", err) } } } diff --git a/internal/core/sync.go b/internal/core/sync.go index 0167371..6f7340f 100644 --- a/internal/core/sync.go +++ b/internal/core/sync.go @@ -5,7 +5,7 @@ import ( "encoding/json" "errors" "fmt" - "log" + "log/slog" "net/http" "os" "path/filepath" @@ -104,7 +104,7 @@ func (s *SyncService) ScanRepo(ctx context.Context, repoID int64) error { } if len(candidates) == 0 { - log.Printf("sync: repo %d is up to date", repoID) + slog.Debug("sync: repo up to date", "repo_id", repoID) return nil } @@ -129,10 +129,10 @@ func (s *SyncService) ScanRepo(ctx context.Context, repoID int64) error { candidates = filtered if len(candidates) == 0 { - log.Printf("sync: repo %d is up to date (all new packages are blocked)", repoID) + slog.Debug("sync: all new packages are blocked", "repo_id", repoID) return nil } - log.Printf("sync: repo %d has %d new package(s)", repoID, len(candidates)) + slog.Info("sync: new packages found", "repo_id", repoID, "count", len(candidates)) switch repo.SyncMode { case "auto": @@ -199,13 +199,16 @@ func (s *SyncService) ApprovePending(ctx context.Context, repoID int64, ids []in downloadErrors = append(downloadErrors, fmt.Errorf("mkdirall %s: %w", pkg.Location, err)) continue } + slog.Info("downloading package", "repo_id", repoID, "package", pkg.Name, "version", pkg.Version) if _, err := rpmclone.DownloadAndVerify(ctx, s.httpClient, url, dest, pkg.ChecksumType, pkg.Checksum); err != nil { + slog.Warn("download failed", "repo_id", repoID, "package", pkg.Name, "error", err) downloadErrors = append(downloadErrors, fmt.Errorf("download %s: %w", pkg.Name, err)) continue } + slog.Info("package downloaded", "repo_id", repoID, "package", pkg.Name, "version", pkg.Version) // Supprime immédiatement de la liste — même si la suite échoue, le paquet est acquis. if err := s.pendingStore.DeletePending(ctx, []int64{pkg.ID}); err != nil { - log.Printf("delete pending %d: %v", pkg.ID, err) + slog.Warn("delete pending failed", "pending_id", pkg.ID, "error", err) } anyApproved = true } @@ -214,18 +217,18 @@ func (s *SyncService) ApprovePending(ctx context.Context, repoID int64, ids []in switch repo.Type { case "rpm": if err := rpmclone.RegenerateMetadata(localDir); err != nil { - log.Printf("metadata regeneration for repo %d failed: %v", repoID, err) + slog.Warn("metadata regeneration failed", "repo_id", repoID, "error", err) } label := "auto-" + time.Now().UTC().Format(time.RFC3339) if _, err := s.snapshotSvc.TakeSnapshot(context.Background(), repoID, label); err != nil { - log.Printf("auto-snapshot for repo %d failed: %v", repoID, err) + slog.Warn("auto-snapshot failed", "repo_id", repoID, "error", err) } case "apt": var cfg aptclone.Config if err := json.Unmarshal([]byte(repo.Config), &cfg); err != nil { - log.Printf("parse apt config for repo %d: %v", repoID, err) + slog.Warn("parse apt config failed", "repo_id", repoID, "error", err) } else if err := aptclone.RegenerateMetadata(localDir, cfg); err != nil { - log.Printf("metadata regeneration for repo %d failed: %v", repoID, err) + slog.Warn("metadata regeneration failed", "repo_id", repoID, "error", err) } } } @@ -285,13 +288,13 @@ func (s *SyncService) RejectPending(ctx context.Context, repoID int64, ids []int func (s *SyncService) StartScheduler(ctx context.Context, interval time.Duration) { go func() { - log.Printf("sync scheduler started (interval=%s)", interval) + slog.Info("sync scheduler started", "interval", interval) ticker := time.NewTicker(interval) defer ticker.Stop() for { select { case <-ctx.Done(): - log.Printf("sync scheduler stopped") + slog.Info("sync scheduler stopped") return case <-ticker.C: s.runScheduledScan(ctx) @@ -303,12 +306,12 @@ func (s *SyncService) StartScheduler(ctx context.Context, interval time.Duration func (s *SyncService) runScheduledScan(ctx context.Context) { repos, err := s.repoStore.ListRepos(ctx) if err != nil { - log.Printf("sync scheduler: list repos error: %v", err) + slog.Error("sync scheduler: list repos failed", "error", err) return } for _, repo := range repos { if err := s.ScanRepo(ctx, repo.ID); err != nil { - log.Printf("sync scheduler: scan repo %d error: %v", repo.ID, err) + slog.Warn("sync scheduler: scan failed", "repo_id", repo.ID, "error", err) } } }