Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion backup/cmd/check-coverage.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"fmt"
"log/slog"
"os"

"backup-rsync/backup/internal"
Expand All @@ -24,7 +25,7 @@ func buildCheckCoverageCommand(fs afero.Fs) *cobra.Command {
}

checker := &internal.CoverageChecker{
Logger: internal.NewUTCLogger(os.Stderr),
Logger: slog.New(internal.NewUTCTextHandler(os.Stderr)),
Fs: fs,
}

Expand Down
8 changes: 4 additions & 4 deletions backup/cmd/job.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"backup-rsync/backup/internal"
"fmt"
"io"
"log"
"log/slog"
"strings"
"time"

Expand All @@ -13,10 +13,10 @@ import (
)

// LoggerFactory creates a logger, returning the logger, log directory path, cleanup function, and any error.
type LoggerFactory func(fs afero.Fs, configPath string, now time.Time) (*log.Logger, string, func() error, error)
type LoggerFactory func(fs afero.Fs, configPath string, now time.Time) (*slog.Logger, string, func() error, error)

func discardLoggerFactory(_ afero.Fs, _ string, _ time.Time) (*log.Logger, string, func() error, error) {
return log.New(io.Discard, "", 0), "", func() error { return nil }, nil
func discardLoggerFactory(_ afero.Fs, _ string, _ time.Time) (*slog.Logger, string, func() error, error) {
return slog.New(slog.DiscardHandler), "", func() error { return nil }, nil
}

type jobCommandOptions struct {
Expand Down
4 changes: 2 additions & 2 deletions backup/cmd/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package cmd
import (
"backup-rsync/backup/internal"
"io"
"log"
"log/slog"
"time"

"github.com/spf13/afero"
Expand All @@ -14,7 +14,7 @@ func buildRunCommand(fs afero.Fs, shell internal.Exec) *cobra.Command {
return buildJobCommand(fs, jobCommandOptions{
use: "run",
short: "Execute the sync jobs",
createLogger: func(fs afero.Fs, configPath string, now time.Time) (*log.Logger, string, func() error, error) {
createLogger: func(fs afero.Fs, configPath string, now time.Time) (*slog.Logger, string, func() error, error) {
logPath := internal.GetLogPath(configPath, now)

logger, cleanup, err := internal.CreateMainLogger(fs, logPath)
Expand Down
4 changes: 2 additions & 2 deletions backup/cmd/simulate.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package cmd
import (
"backup-rsync/backup/internal"
"io"
"log"
"log/slog"
"time"

"github.com/spf13/afero"
Expand All @@ -14,7 +14,7 @@ func buildSimulateCommand(fs afero.Fs, shell internal.Exec) *cobra.Command {
return buildJobCommand(fs, jobCommandOptions{
use: "simulate",
short: "Simulate the sync jobs",
createLogger: func(fs afero.Fs, configPath string, now time.Time) (*log.Logger, string, func() error, error) {
createLogger: func(fs afero.Fs, configPath string, now time.Time) (*slog.Logger, string, func() error, error) {
logPath := internal.GetLogPath(configPath, now) + "-sim"

logger, cleanup, err := internal.CreateMainLogger(fs, logPath)
Expand Down
30 changes: 15 additions & 15 deletions backup/internal/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package internal

import (
"fmt"
"log"
"log/slog"
"path/filepath"
"slices"
"strings"
Expand All @@ -12,7 +12,7 @@ import (

// CoverageChecker analyzes path coverage against a configuration.
type CoverageChecker struct {
Logger *log.Logger
Logger *slog.Logger
Fs afero.Fs
}

Expand All @@ -21,8 +21,8 @@ func (c *CoverageChecker) IsExcludedGlobally(path string, mappings []Mapping) bo
for _, exclusion := range mapping.Exclusions {
exclusionPath := filepath.Join(mapping.Source, exclusion)
if strings.HasPrefix(NormalizePath(path), exclusionPath) {
c.Logger.Printf("EXCLUDED: Path '%s' is globally excluded by '%s' in source '%s'",
path, exclusion, mapping.Source)
c.Logger.Info(fmt.Sprintf("EXCLUDED: Path '%s' is globally excluded by '%s' in source '%s'",
path, exclusion, mapping.Source))

return true
}
Expand Down Expand Up @@ -56,13 +56,13 @@ func (c *CoverageChecker) isExcluded(path string, job Job) bool {

func (c *CoverageChecker) isCoveredByJob(path string, job Job) bool {
if NormalizePath(job.Source) == NormalizePath(path) {
c.Logger.Printf("COVERED: Path '%s' is covered by job '%s'", path, job.Name)
c.Logger.Info(fmt.Sprintf("COVERED: Path '%s' is covered by job '%s'", path, job.Name))

return true
}

if c.isExcluded(path, job) {
c.Logger.Printf("EXCLUDED: Path '%s' is excluded by job '%s'", path, job.Name)
c.Logger.Info(fmt.Sprintf("EXCLUDED: Path '%s' is excluded by job '%s'", path, job.Name))

return true
}
Expand All @@ -86,7 +86,7 @@ func (c *CoverageChecker) checkPath(
path string, mappings []Mapping, result *[]string, seen map[string]bool,
) {
if seen[path] {
c.Logger.Printf("SKIP: Path '%s' already seen", path)
c.Logger.Info(fmt.Sprintf("SKIP: Path '%s' already seen", path))

return
}
Expand All @@ -95,27 +95,27 @@ func (c *CoverageChecker) checkPath(

// Skip if globally excluded
if c.IsExcludedGlobally(path, mappings) {
c.Logger.Printf("SKIP: Path '%s' is globally excluded", path)
c.Logger.Info(fmt.Sprintf("SKIP: Path '%s' is globally excluded", path))

return
}

// Skip if covered by a job
if c.isCovered(path, mappings) {
c.Logger.Printf("SKIP: Path '%s' is covered by a job", path)
c.Logger.Info(fmt.Sprintf("SKIP: Path '%s' is covered by a job", path))

return
}

// Check if it's effectively covered through descendants
if c.isEffectivelyCovered(path, mappings) {
c.Logger.Printf("SKIP: Path '%s' is effectively covered", path)
c.Logger.Info(fmt.Sprintf("SKIP: Path '%s' is effectively covered", path))

return
}

// Add uncovered path
c.Logger.Printf("ADD: Path '%s' is uncovered", path)
c.Logger.Info(fmt.Sprintf("ADD: Path '%s' is uncovered", path))
*result = append(*result, path)
}

Expand All @@ -124,13 +124,13 @@ func (c *CoverageChecker) checkPath(
func (c *CoverageChecker) isEffectivelyCovered(path string, mappings []Mapping) bool {
children, err := getChildDirectories(c.Fs, path)
if err != nil {
c.Logger.Printf("ERROR: could not get child directories of '%s': %v", path, err)
c.Logger.Info(fmt.Sprintf("ERROR: could not get child directories of '%s': %v", path, err))

return false
}

if len(children) == 0 {
c.Logger.Printf("NOT COVERED: Path '%s' has no children", path)
c.Logger.Info(fmt.Sprintf("NOT COVERED: Path '%s' has no children", path))

return false // Leaf directories are not effectively covered unless directly covered
}
Expand All @@ -141,14 +141,14 @@ func (c *CoverageChecker) isEffectivelyCovered(path string, mappings []Mapping)
covered := c.IsExcludedGlobally(child, mappings) || c.isCovered(child, mappings) ||
c.isEffectivelyCovered(child, mappings)
if !covered {
c.Logger.Printf("UNCOVERED CHILD: Path '%s' has uncovered child '%s'", path, child)
c.Logger.Info(fmt.Sprintf("UNCOVERED CHILD: Path '%s' has uncovered child '%s'", path, child))

allCovered = false
}
}

if allCovered {
c.Logger.Printf("COVERED: Path '%s' is effectively covered", path)
c.Logger.Info(fmt.Sprintf("COVERED: Path '%s' is effectively covered", path))
}

return allCovered
Expand Down
10 changes: 5 additions & 5 deletions backup/internal/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"io"
"log"
"log/slog"
"maps"
"os"
"path/filepath"
Expand Down Expand Up @@ -71,13 +71,13 @@ func (cfg Config) String() string {
return string(out)
}

func (cfg Config) Apply(rsync JobCommand, logger *log.Logger) error {
func (cfg Config) Apply(rsync JobCommand, logger *slog.Logger) error {
versionInfo, fullpath, err := rsync.GetVersionInfo()
if err != nil {
logger.Printf("Failed to fetch rsync version: %v", err)
logger.Info(fmt.Sprintf("Failed to fetch rsync version: %v", err))
} else {
logger.Printf("Rsync Binary Path: %s", fullpath)
logger.Printf("Rsync Version Info: %s", versionInfo)
logger.Info("Rsync Binary Path: " + fullpath)
logger.Info("Rsync Version Info: " + versionInfo)
}

counts := make(map[JobStatus]int)
Expand Down
37 changes: 14 additions & 23 deletions backup/internal/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ package internal
import (
"fmt"
"io"
"log"
"log/slog"
"os"
"path/filepath"
"strings"
Expand All @@ -13,26 +13,17 @@ import (
"github.com/spf13/afero"
)

// UTCLogWriter wraps an io.Writer and prepends an ISO 8601 UTC timestamp to each write.
type UTCLogWriter struct {
W io.Writer
Now func() time.Time
}

func (u *UTCLogWriter) Write(data []byte) (int, error) {
now := u.Now().UTC().Format(time.RFC3339)

_, err := fmt.Fprintf(u.W, "%s %s", now, data)
if err != nil {
return 0, fmt.Errorf("writing log entry: %w", err)
}

return len(data), nil
}

// NewUTCLogger creates a *log.Logger that writes ISO 8601 UTC timestamps.
func NewUTCLogger(w io.Writer) *log.Logger {
return log.New(&UTCLogWriter{W: w, Now: time.Now}, "", 0)
// NewUTCTextHandler creates a slog.Handler that writes text logs with UTC timestamps.
func NewUTCTextHandler(w io.Writer) slog.Handler {
return slog.NewTextHandler(w, &slog.HandlerOptions{
ReplaceAttr: func(_ []string, a slog.Attr) slog.Attr {
if a.Key == slog.TimeKey {
a.Value = slog.StringValue(a.Value.Time().UTC().Format(time.RFC3339))
}

return a
},
})
}

func NormalizePath(path string) string {
Expand All @@ -51,7 +42,7 @@ func GetLogPath(configPath string, now time.Time) string {

func CreateMainLogger(
fs afero.Fs, logPath string,
) (*log.Logger, func() error, error) {
) (*slog.Logger, func() error, error) {
overallLogPath := logPath + "/summary.log"

err := fs.MkdirAll(logPath, LogDirPermission)
Expand All @@ -64,7 +55,7 @@ func CreateMainLogger(
return nil, nil, fmt.Errorf("failed to open overall log file: %w", err)
}

logger := NewUTCLogger(overallLogFile)
logger := slog.New(NewUTCTextHandler(overallLogFile))

cleanup := func() error {
return overallLogFile.Close()
Expand Down
6 changes: 3 additions & 3 deletions backup/internal/job_command.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package internal

import "log"
import "log/slog"

// JobStatus represents the outcome of a job execution.
type JobStatus string
Expand All @@ -18,6 +18,6 @@ const (
type JobCommand interface {
Run(job Job) JobStatus
GetVersionInfo() (string, string, error)
ReportJobStatus(jobName string, status JobStatus, logger *log.Logger)
ReportSummary(counts map[JobStatus]int, logger *log.Logger)
ReportJobStatus(jobName string, status JobStatus, logger *slog.Logger)
ReportSummary(counts map[JobStatus]int, logger *slog.Logger)
}
10 changes: 5 additions & 5 deletions backup/internal/rsync.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"io"
"log"
"log/slog"
"os"
"path/filepath"
"strings"
Expand Down Expand Up @@ -43,15 +43,15 @@ func (c SharedCommand) PrintArgs(job Job, args []string) {
fmt.Fprintf(c.Output, "Command: %s %s\n", c.BinPath, strings.Join(args, " "))
}

func (c SharedCommand) ReportJobStatus(jobName string, status JobStatus, logger *log.Logger) {
logger.Printf("STATUS [%s]: %s", jobName, status)
func (c SharedCommand) ReportJobStatus(jobName string, status JobStatus, logger *slog.Logger) {
logger.Info(fmt.Sprintf("STATUS [%s]: %s", jobName, status))
fmt.Fprintf(c.Output, "Status [%s]: %s\n", jobName, status)
}

func (c SharedCommand) ReportSummary(counts map[JobStatus]int, logger *log.Logger) {
func (c SharedCommand) ReportSummary(counts map[JobStatus]int, logger *slog.Logger) {
summary := fmt.Sprintf("Summary: %d succeeded, %d failed, %d skipped",
counts[Success], counts[Failure], counts[Skipped])
logger.Print(summary)
logger.Info(summary)
fmt.Fprintln(c.Output, summary)
}

Expand Down
6 changes: 3 additions & 3 deletions backup/internal/rsync_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package internal

import (
"io"
"log"
"log/slog"
)

// ListCommand prints the rsync commands that would be executed without running them.
Expand All @@ -17,9 +17,9 @@ func NewListCommand(binPath string, shell Exec, output io.Writer) ListCommand {
}
}

func (ListCommand) ReportJobStatus(_ string, _ JobStatus, _ *log.Logger) {}
func (ListCommand) ReportJobStatus(_ string, _ JobStatus, _ *slog.Logger) {}

func (ListCommand) ReportSummary(_ map[JobStatus]int, _ *log.Logger) {}
func (ListCommand) ReportSummary(_ map[JobStatus]int, _ *slog.Logger) {}

func (c ListCommand) Run(job Job) JobStatus {
logPath := c.JobLogPath(job)
Expand Down
6 changes: 3 additions & 3 deletions backup/internal/test/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,27 @@ package internal_test
import (
"bytes"
"io"
"log"
"path/filepath"
"sort"
"testing"

. "backup-rsync/backup/internal"
"backup-rsync/backup/internal/testutil"

"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
)

func newTestChecker(fs afero.Fs, logBuf *bytes.Buffer) *CoverageChecker {
return &CoverageChecker{
Logger: log.New(logBuf, "", 0),
Logger: testutil.NewTestLogger(logBuf),
Fs: fs,
}
}

func newSilentChecker(fs afero.Fs) *CoverageChecker {
return &CoverageChecker{
Logger: log.New(io.Discard, "", 0),
Logger: testutil.NewTestLogger(io.Discard),
Fs: fs,
}
}
Expand Down
Loading
Loading