add logging

Signed-off-by: GnomeZworc <nicolas.boufidjeline@g3e.fr>
This commit is contained in:
GnomeZworc 2026-04-25 20:00:00 +02:00
commit e1aff8d16b
Signed by: nicolas.boufideline
GPG key ID: 4406BBBF8845D632
7 changed files with 73 additions and 33 deletions

View file

@ -3,12 +3,12 @@ package main
import ( import (
"context" "context"
"fmt" "fmt"
"log" "log/slog"
"net/http" "net/http"
"os" "os"
"time"
"os/signal" "os/signal"
"syscall" "syscall"
"time"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/syonad/clonepack/config" "github.com/syonad/clonepack/config"
@ -26,6 +26,8 @@ var serveCmd = &cobra.Command{
return fmt.Errorf("load config: %w", err) return fmt.Errorf("load config: %w", err)
} }
setupLogger(cfg.Log)
db, err := store.Open(cfg.DB) db, err := store.Open(cfg.DB)
if err != nil { if err != nil {
return fmt.Errorf("open store: %w", err) return fmt.Errorf("open store: %w", err)
@ -58,17 +60,28 @@ var serveCmd = &cobra.Command{
srv := &http.Server{Addr: addr, Handler: router} srv := &http.Server{Addr: addr, Handler: router}
go func() { go func() {
log.Printf("ClonePack listening on %s", addr) slog.Info("ClonePack listening", "addr", addr)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Printf("server error: %v", err) slog.Error("server error", "error", err)
} }
}() }()
<-ctx.Done() <-ctx.Done()
log.Printf("shutting down...") slog.Info("shutting down...")
shutdownCtx, cancel2 := context.WithTimeout(context.Background(), 30*time.Second) shutdownCtx, cancel2 := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel2() defer cancel2()
return srv.Shutdown(shutdownCtx) 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))
}

View file

@ -1,12 +1,16 @@
package config package config
import "time" import (
"log/slog"
"time"
)
type Config struct { type Config struct {
Server ServerConfig `mapstructure:"server"` Server ServerConfig `mapstructure:"server"`
DB DBConfig `mapstructure:"db"` DB DBConfig `mapstructure:"db"`
DataDir string `mapstructure:"data_dir"` DataDir string `mapstructure:"data_dir"`
Sync SyncConfig `mapstructure:"sync"` Sync SyncConfig `mapstructure:"sync"`
Log LogConfig `mapstructure:"log"`
} }
type ServerConfig struct { type ServerConfig struct {
@ -22,6 +26,24 @@ type SyncConfig struct {
Interval string `mapstructure:"interval"` 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 { func (s SyncConfig) IntervalDuration() time.Duration {
d, err := time.ParseDuration(s.Interval) d, err := time.ParseDuration(s.Interval)
if err != nil || d <= 0 { if err != nil || d <= 0 {

View file

@ -26,6 +26,8 @@ func Load(cfgFile string) (*Config, error) {
viper.SetDefault("db.path", "./clonepack.db") viper.SetDefault("db.path", "./clonepack.db")
viper.SetDefault("data_dir", "./data") viper.SetDefault("data_dir", "./data")
viper.SetDefault("sync.interval", "1h") viper.SetDefault("sync.interval", "1h")
viper.SetDefault("log.level", "info")
viper.SetDefault("log.format", "text")
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
if _, ok := err.(viper.ConfigFileNotFoundError); !ok { if _, ok := err.(viper.ConfigFileNotFoundError); !ok {

View file

@ -4,7 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
"log" "log/slog"
"net/http" "net/http"
"strconv" "strconv"
@ -37,7 +37,7 @@ func (h *SyncHandler) Trigger(w http.ResponseWriter, r *http.Request) {
} }
go func() { go func() {
if err := h.svc.ScanRepo(context.Background(), repoID); err != nil { 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"}) 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 ids := req.IDs
go func() { go func() {
if err := h.svc.ApprovePending(context.Background(), repoID, ids); err != nil { 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"}) JSON(w, http.StatusAccepted, map[string]string{"status": "approval started"})

View file

@ -5,7 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"log" "log/slog"
"path/filepath" "path/filepath"
aptclone "github.com/syonad/clonepack/internal/clone/apt" 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() ctx := context.Background()
if err := s.jobStore.MarkCloneJobStarted(ctx, jobID); err != nil { 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 return
} }
@ -70,12 +70,12 @@ func (s *CloneService) runClone(repoID, jobID int64, repo *store.Repo) {
case "rpm": case "rpm":
cloner := rpm.New(repo.SourceURL, destDir) cloner := rpm.New(repo.SourceURL, destDir)
cloner.OnProgress = func(p rpm.Progress) { 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 { if err := cloner.Clone(ctx); err != nil {
cloneErr = err cloneErr = err
} else if err := rpm.RegenerateMetadata(destDir); err != nil { } 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": case "apt":
@ -86,7 +86,7 @@ func (s *CloneService) runClone(repoID, jobID int64, repo *store.Repo) {
} }
cloner := aptclone.New(repo.SourceURL, destDir, cfg) cloner := aptclone.New(repo.SourceURL, destDir, cfg)
cloner.OnProgress = func(file string, bytes int64) { 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) cloneErr = cloner.Clone(ctx)
} }
@ -94,10 +94,10 @@ func (s *CloneService) runClone(repoID, jobID int64, repo *store.Repo) {
if cloneErr != nil { if cloneErr != nil {
errMsg := cloneErr.Error() errMsg := cloneErr.Error()
_ = s.jobStore.MarkCloneJobFinished(ctx, jobID, store.CloneJobFailed, &errMsg) _ = 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 return
} }
_ = s.jobStore.MarkCloneJobFinished(ctx, jobID, store.CloneJobCompleted, nil) _ = s.jobStore.MarkCloneJobFinished(ctx, jobID, store.CloneJobCompleted, nil)
log.Printf("clone job %d: completed", jobID) slog.Info("clone job completed", "job_id", jobID)
} }

View file

@ -4,7 +4,7 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log/slog"
"path/filepath" "path/filepath"
aptclone "github.com/syonad/clonepack/internal/clone/apt" aptclone "github.com/syonad/clonepack/internal/clone/apt"
@ -92,16 +92,16 @@ func (s *RepoService) initStorage(repo *store.Repo) {
switch repo.Type { switch repo.Type {
case "rpm": case "rpm":
if err := rpmclone.InitEmptyRepo(localDir); err != nil { 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": case "apt":
var cfg aptclone.Config var cfg aptclone.Config
if err := json.Unmarshal([]byte(repo.Config), &cfg); err != nil { 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 return
} }
if err := aptclone.InitEmptyRepo(localDir, cfg); err != nil { 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)
} }
} }
} }

View file

@ -5,7 +5,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"log" "log/slog"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -104,7 +104,7 @@ func (s *SyncService) ScanRepo(ctx context.Context, repoID int64) error {
} }
if len(candidates) == 0 { 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 return nil
} }
@ -129,10 +129,10 @@ func (s *SyncService) ScanRepo(ctx context.Context, repoID int64) error {
candidates = filtered candidates = filtered
if len(candidates) == 0 { 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 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 { switch repo.SyncMode {
case "auto": 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)) downloadErrors = append(downloadErrors, fmt.Errorf("mkdirall %s: %w", pkg.Location, err))
continue 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 { 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)) downloadErrors = append(downloadErrors, fmt.Errorf("download %s: %w", pkg.Name, err))
continue 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. // 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 { 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 anyApproved = true
} }
@ -214,18 +217,18 @@ func (s *SyncService) ApprovePending(ctx context.Context, repoID int64, ids []in
switch repo.Type { switch repo.Type {
case "rpm": case "rpm":
if err := rpmclone.RegenerateMetadata(localDir); err != nil { 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) label := "auto-" + time.Now().UTC().Format(time.RFC3339)
if _, err := s.snapshotSvc.TakeSnapshot(context.Background(), repoID, label); err != nil { 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": case "apt":
var cfg aptclone.Config var cfg aptclone.Config
if err := json.Unmarshal([]byte(repo.Config), &cfg); err != nil { 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 { } 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) { func (s *SyncService) StartScheduler(ctx context.Context, interval time.Duration) {
go func() { go func() {
log.Printf("sync scheduler started (interval=%s)", interval) slog.Info("sync scheduler started", "interval", interval)
ticker := time.NewTicker(interval) ticker := time.NewTicker(interval)
defer ticker.Stop() defer ticker.Stop()
for { for {
select { select {
case <-ctx.Done(): case <-ctx.Done():
log.Printf("sync scheduler stopped") slog.Info("sync scheduler stopped")
return return
case <-ticker.C: case <-ticker.C:
s.runScheduledScan(ctx) s.runScheduledScan(ctx)
@ -303,12 +306,12 @@ func (s *SyncService) StartScheduler(ctx context.Context, interval time.Duration
func (s *SyncService) runScheduledScan(ctx context.Context) { func (s *SyncService) runScheduledScan(ctx context.Context) {
repos, err := s.repoStore.ListRepos(ctx) repos, err := s.repoStore.ListRepos(ctx)
if err != nil { if err != nil {
log.Printf("sync scheduler: list repos error: %v", err) slog.Error("sync scheduler: list repos failed", "error", err)
return return
} }
for _, repo := range repos { for _, repo := range repos {
if err := s.ScanRepo(ctx, repo.ID); err != nil { 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)
} }
} }
} }