diff options
| author | Bernhard Guillon <Bernhard.Guillon@begu.org> | 2026-07-06 22:16:37 +0200 |
|---|---|---|
| committer | Bernhard Guillon <Bernhard.Guillon@begu.org> | 2026-07-06 22:16:37 +0200 |
| commit | 913a974eca2522ba7c6d27066481bb2c6635db53 (patch) | |
| tree | f0cfc1ee173a762662b586fca5db9db422592093 | |
| parent | 5f2ba9c3d37a1c8658aea411188a6763e982b1ab (diff) | |
| download | sourcewatch-913a974eca2522ba7c6d27066481bb2c6635db53.tar.gz sourcewatch-913a974eca2522ba7c6d27066481bb2c6635db53.zip | |
Add log level system and sync logging
- log.go: log level system (DEBUG, INFO, WARN, ERROR, NONE)
- CLI: -log-level flag (default info), -verbose as shorthand for debug
- Watcher: logs sync/remove actions at INFO level
- Watcher: logs events at DEBUG level
- Errors: structured error messages (DISK FULL, PERMISSION DENIED, etc.)
- Updated README with -log-level flag
| -rw-r--r-- | README.md | 3 | ||||
| -rw-r--r-- | errors.go | 11 | ||||
| -rw-r--r-- | errors_test.go | 8 | ||||
| -rw-r--r-- | log.go | 72 | ||||
| -rw-r--r-- | main.go | 28 | ||||
| -rw-r--r-- | sync.go | 17 | ||||
| -rw-r--r-- | watcher.go | 61 | ||||
| -rw-r--r-- | watcher_test.go | 32 |
8 files changed, 161 insertions, 71 deletions
@@ -35,7 +35,8 @@ podman run -v c:\source:/host:ro -v watchdata:/data sourcewatch ``` -host string Host source directory (default "/host") -data string Data destination directory (default "/data") --verbose Enable verbose logging +-verbose Enable verbose logging (shorthand for -log-level debug) +-log-level string Log level: debug, info, warn, error, none (default "info") -dry-run Preview what would be synced without copying ``` @@ -3,7 +3,6 @@ package main import ( "errors" "io/fs" - "log" "os" "syscall" "time" @@ -51,7 +50,7 @@ func isRetryable(err error) bool { return false } -func logError(op, relPath string, err error) { +func logSyncError(op, relPath string, err error) { if err == nil { return } @@ -65,13 +64,13 @@ func logError(op, relPath string, err error) { switch { case errors.Is(err, syscall.ENOSPC): - log.Printf("DISK FULL: %s %s failed - no space left on device", op, relPath) + logErrorf("DISK FULL: %s %s failed - no space left on device", op, relPath) case errors.Is(err, syscall.EACCES), errors.Is(err, syscall.EPERM): - log.Printf("PERMISSION DENIED: %s %s - check volume mount permissions", op, relPath) + logErrorf("PERMISSION DENIED: %s %s - check volume mount permissions", op, relPath) case errors.Is(err, syscall.ELOOP): - log.Printf("SYMLINK LOOP: %s %s - too many symbolic links", op, relPath) + logErrorf("SYMLINK LOOP: %s %s - too many symbolic links", op, relPath) default: - log.Printf("ERROR: %s %s: %v", op, relPath, err) + logErrorf("%s %s: %v", op, relPath, err) } } diff --git a/errors_test.go b/errors_test.go index 553939d..e35e5b3 100644 --- a/errors_test.go +++ b/errors_test.go @@ -101,19 +101,19 @@ func TestIsRetryable(t *testing.T) { } func TestLogError_DiskFull(t *testing.T) { - logError("copy", "test.txt", syscall.ENOSPC) + logSyncError("copy", "test.txt", syscall.ENOSPC) } func TestLogError_PermissionDenied(t *testing.T) { - logError("write", "test.txt", syscall.EACCES) + logSyncError("write", "test.txt", syscall.EACCES) } func TestLogError_NotExist(t *testing.T) { - logError("stat", "test.txt", fs.ErrNotExist) + logSyncError("stat", "test.txt", fs.ErrNotExist) } func TestLogError_Nil(t *testing.T) { - logError("test", "file.txt", nil) + logSyncError("test", "file.txt", nil) } func TestSyncError(t *testing.T) { @@ -0,0 +1,72 @@ +package main + +import ( + "fmt" + "os" + "strings" +) + +type LogLevel int + +const ( + LevelDebug LogLevel = iota + LevelInfo + LevelWarn + LevelError + LevelNone +) + +var logLevelNames = map[LogLevel]string{ + LevelDebug: "DEBUG", + LevelInfo: "INFO", + LevelWarn: "WARN", + LevelError: "ERROR", + LevelNone: "NONE", +} + +var currentLevel = LevelInfo + +func SetLogLevel(level LogLevel) { + currentLevel = level +} + +func ParseLogLevel(s string) (LogLevel, error) { + switch strings.ToUpper(s) { + case "DEBUG": + return LevelDebug, nil + case "INFO": + return LevelInfo, nil + case "WARN": + return LevelWarn, nil + case "ERROR": + return LevelError, nil + case "NONE": + return LevelNone, nil + default: + return LevelInfo, fmt.Errorf("unknown log level: %s (valid: debug, info, warn, error, none)", s) + } +} + +func logDebug(format string, args ...interface{}) { + if currentLevel <= LevelDebug { + fmt.Fprintf(os.Stderr, "DEBUG: "+format+"\n", args...) + } +} + +func logInfo(format string, args ...interface{}) { + if currentLevel <= LevelInfo { + fmt.Fprintf(os.Stderr, format+"\n", args...) + } +} + +func logWarn(format string, args ...interface{}) { + if currentLevel <= LevelWarn { + fmt.Fprintf(os.Stderr, "WARN: "+format+"\n", args...) + } +} + +func logErrorf(format string, args ...interface{}) { + if currentLevel <= LevelError { + fmt.Fprintf(os.Stderr, "ERROR: "+format+"\n", args...) + } +} @@ -12,37 +12,47 @@ import ( var ( hostDir = flag.String("host", "/host", "Host source directory (read-only)") dataDir = flag.String("data", "/data", "Data destination directory") - verbose = flag.Bool("verbose", false, "Enable verbose logging") + verbose = flag.Bool("verbose", false, "Enable verbose logging (shorthand for -log-level debug)") + logLevel = flag.String("log-level", "info", "Log level: debug, info, warn, error, none") dryRun = flag.Bool("dry-run", false, "Preview what would be synced without copying") ) func main() { flag.Parse() - log.SetFlags(log.LstdFlags | log.Lshortfile) if *verbose { - log.Println("sourcewatch starting...") - log.Printf("host: %s, data: %s, dry-run: %v", *hostDir, *dataDir, *dryRun) + *logLevel = "debug" } + level, err := ParseLogLevel(*logLevel) + if err != nil { + log.Fatal(err) + } + SetLogLevel(level) + + log.SetFlags(0) + log.SetOutput(nil) + stack := NewGitignoreStack(*hostDir) manifest, err := initialSync(*hostDir, *dataDir, stack, *dryRun) if err != nil { - log.Fatalf("initial sync failed: %v", err) + logErrorf("initial sync failed: %v", err) + os.Exit(1) } if *dryRun { - log.Println("dry-run complete, exiting") + logInfo("dry-run complete, exiting") return } - log.Println("watcher: ready, monitoring for changes ...") + logInfo("watcher: ready, monitoring for changes ...") ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) defer cancel() - if err := watch(ctx, *hostDir, *dataDir, manifest, stack, *dryRun, *verbose); err != nil { - log.Fatalf("watcher failed: %v", err) + if err := watch(ctx, *hostDir, *dataDir, manifest, stack, *dryRun); err != nil { + logErrorf("watcher failed: %v", err) + os.Exit(1) } } @@ -3,7 +3,6 @@ package main import ( "fmt" "io" - "log" "os" "path/filepath" "time" @@ -32,12 +31,12 @@ func initialSync(hostDir, dataDir string, stack *GitignoreStack, dryRun bool) (* stats := &syncStats{} start := time.Now() - log.Printf("initial sync: scanning %s ...", hostDir) + logInfo("initial sync: scanning %s ...", hostDir) err := filepath.Walk(hostDir, func(path string, info os.FileInfo, err error) error { if err != nil { stats.errors++ - logError("walk", path, err) + logErrorf("walk %s: %v", path, err) return nil } @@ -86,7 +85,7 @@ func initialSync(hostDir, dataDir string, stack *GitignoreStack, dryRun bool) (* linkTarget, err = os.Readlink(path) if err != nil { stats.errors++ - logError("readlink", relPath, err) + logErrorf("readlink %s: %v", relPath, err) return nil } } @@ -106,35 +105,35 @@ func initialSync(hostDir, dataDir string, stack *GitignoreStack, dryRun bool) (* if info.IsDir() { if err := os.MkdirAll(destPath, info.Mode()); err != nil { stats.errors++ - logError("mkdir", relPath, err) + logErrorf("mkdir %s: %v", relPath, err) } return nil } if err := os.MkdirAll(filepath.Dir(destPath), 0755); err != nil { stats.errors++ - logError("mkdir", filepath.Dir(relPath), err) + logErrorf("mkdir %s: %v", filepath.Dir(relPath), err) return nil } if linkTarget != "" { if err := os.Symlink(linkTarget, destPath); err != nil { stats.errors++ - logError("symlink", relPath, err) + logErrorf("symlink %s: %v", relPath, err) } return nil } if err := copyFileWithRetry(path, destPath, info.Mode()); err != nil { stats.errors++ - logError("copy", relPath, err) + logErrorf("copy %s: %v", relPath, err) } return nil }) elapsed := time.Since(start) fmt.Fprintf(os.Stderr, "\r") - log.Printf("initial sync: done in %s — %d files, %d dirs, %d skipped, %d errors", + logInfo("initial sync: done in %s — %d files, %d dirs, %d skipped, %d errors", elapsed.Round(time.Millisecond), stats.files, stats.dirs, stats.skipped, stats.errors) return manifest, err @@ -2,7 +2,6 @@ package main import ( "context" - "log" "os" "path/filepath" "sync" @@ -11,7 +10,7 @@ import ( "github.com/fsnotify/fsnotify" ) -func watch(ctx context.Context, hostDir, dataDir string, manifest *Manifest, stack *GitignoreStack, dryRun, verbose bool) error { +func watch(ctx context.Context, hostDir, dataDir string, manifest *Manifest, stack *GitignoreStack, dryRun bool) error { watcher, err := fsnotify.NewWatcher() if err != nil { return err @@ -48,9 +47,7 @@ func watch(ctx context.Context, hostDir, dataDir string, manifest *Manifest, sta continue } - if verbose { - log.Printf("event: %s %s", event.Op, relPath) - } + logDebug("event: %s %s", event.Op, relPath) mu.Lock() if t, exists := debounce[relPath]; exists { @@ -60,7 +57,7 @@ func watch(ctx context.Context, hostDir, dataDir string, manifest *Manifest, sta mu.Lock() delete(debounce, relPath) mu.Unlock() - handleEvent(event.Op, hostDir, dataDir, relPath, manifest, stack, dryRun, verbose, watcher) + handleEvent(event.Op, hostDir, dataDir, relPath, manifest, stack, dryRun, watcher) }) mu.Unlock() @@ -68,37 +65,37 @@ func watch(ctx context.Context, hostDir, dataDir string, manifest *Manifest, sta if !ok { return nil } - log.Printf("watcher error: %v", err) + logErrorf("watcher: %v", err) } } } -func handleEvent(op fsnotify.Op, hostDir, dataDir, relPath string, manifest *Manifest, stack *GitignoreStack, dryRun, verbose bool, watcher *fsnotify.Watcher) { +func handleEvent(op fsnotify.Op, hostDir, dataDir, relPath string, manifest *Manifest, stack *GitignoreStack, dryRun bool, watcher *fsnotify.Watcher) { destPath := filepath.Join(dataDir, relPath) srcPath := filepath.Join(hostDir, relPath) switch { case op&fsnotify.Create != 0: - syncCreate(srcPath, destPath, relPath, manifest, stack, dryRun, verbose, watcher) + syncCreate(srcPath, destPath, relPath, manifest, stack, dryRun, watcher) case op&fsnotify.Write != 0: - syncWrite(srcPath, destPath, relPath, manifest, dryRun, verbose) + syncWrite(srcPath, destPath, relPath, manifest, dryRun) case op&fsnotify.Remove != 0: - syncRemove(destPath, relPath, manifest, dryRun, verbose) + syncRemove(destPath, relPath, manifest, dryRun) case op&fsnotify.Rename != 0: - syncRemove(destPath, relPath, manifest, dryRun, verbose) + syncRemove(destPath, relPath, manifest, dryRun) } } -func syncCreate(srcPath, destPath, relPath string, manifest *Manifest, stack *GitignoreStack, dryRun, verbose bool, watcher *fsnotify.Watcher) { +func syncCreate(srcPath, destPath, relPath string, manifest *Manifest, stack *GitignoreStack, dryRun bool, watcher *fsnotify.Watcher) { info, err := os.Lstat(srcPath) if err != nil { if os.IsNotExist(err) { return } - logError("stat", relPath, err) + logErrorf("stat %s: %v", relPath, err) return } @@ -107,13 +104,14 @@ func syncCreate(srcPath, destPath, relPath string, manifest *Manifest, stack *Gi stack.Push(srcPath) } if err := watcher.Add(srcPath); err != nil { - logError("watch", relPath, err) + logErrorf("watch %s: %v", relPath, err) } if dryRun { + logInfo("[dry-run] would create dir: %s", relPath) return } if err := os.MkdirAll(destPath, info.Mode()); err != nil { - logError("mkdir", relPath, err) + logErrorf("mkdir %s: %v", relPath, err) } return } @@ -122,7 +120,7 @@ func syncCreate(srcPath, destPath, relPath string, manifest *Manifest, stack *Gi if info.Mode()&os.ModeSymlink != 0 { linkTarget, err = os.Readlink(srcPath) if err != nil { - logError("readlink", relPath, err) + logErrorf("readlink %s: %v", relPath, err) return } } @@ -136,6 +134,7 @@ func syncCreate(srcPath, destPath, relPath string, manifest *Manifest, stack *Gi manifest.Files[relPath] = meta if dryRun { + logInfo("[dry-run] would sync: %s", relPath) return } @@ -144,23 +143,26 @@ func syncCreate(srcPath, destPath, relPath string, manifest *Manifest, stack *Gi if linkTarget != "" { if err := os.Symlink(linkTarget, destPath); err != nil { - logError("symlink", relPath, err) + logErrorf("symlink %s: %v", relPath, err) } + logInfo("sync: %s -> %s (symlink)", relPath, linkTarget) return } if err := copyFileWithRetry(srcPath, destPath, info.Mode()); err != nil { - logError("copy", relPath, err) + logErrorf("copy %s: %v", relPath, err) + } else { + logInfo("sync: %s", relPath) } } -func syncWrite(srcPath, destPath, relPath string, manifest *Manifest, dryRun, verbose bool) { +func syncWrite(srcPath, destPath, relPath string, manifest *Manifest, dryRun bool) { info, err := os.Lstat(srcPath) if err != nil { if os.IsNotExist(err) { return } - logError("stat", relPath, err) + logErrorf("stat %s: %v", relPath, err) return } @@ -173,7 +175,7 @@ func syncWrite(srcPath, destPath, relPath string, manifest *Manifest, dryRun, ve if info.Mode()&os.ModeSymlink != 0 { linkTarget, err = os.Readlink(srcPath) if err != nil { - logError("readlink", relPath, err) + logErrorf("readlink %s: %v", relPath, err) return } } @@ -186,6 +188,7 @@ func syncWrite(srcPath, destPath, relPath string, manifest *Manifest, dryRun, ve } if dryRun { + logInfo("[dry-run] would update: %s", relPath) return } @@ -193,25 +196,31 @@ func syncWrite(srcPath, destPath, relPath string, manifest *Manifest, dryRun, ve if linkTarget != "" { if err := os.Symlink(linkTarget, destPath); err != nil { - logError("symlink", relPath, err) + logErrorf("symlink %s: %v", relPath, err) } + logInfo("sync: %s -> %s (symlink)", relPath, linkTarget) return } if err := copyFileWithRetry(srcPath, destPath, info.Mode()); err != nil { - logError("copy", relPath, err) + logErrorf("copy %s: %v", relPath, err) + } else { + logInfo("sync: %s", relPath) } } -func syncRemove(destPath, relPath string, manifest *Manifest, dryRun, verbose bool) { +func syncRemove(destPath, relPath string, manifest *Manifest, dryRun bool) { delete(manifest.Files, relPath) if dryRun { + logInfo("[dry-run] would remove: %s", relPath) return } if err := os.Remove(destPath); err != nil && !os.IsNotExist(err) { - logError("remove", relPath, err) + logErrorf("remove %s: %v", relPath, err) + } else { + logInfo("remove: %s", relPath) } } diff --git a/watcher_test.go b/watcher_test.go index 8fac419..df16930 100644 --- a/watcher_test.go +++ b/watcher_test.go @@ -20,7 +20,7 @@ func TestWatcher_Create(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -42,7 +42,7 @@ func TestWatcher_CreateInSubdir(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -66,7 +66,7 @@ func TestWatcher_Write(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -90,7 +90,7 @@ func TestWatcher_Remove(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -114,7 +114,7 @@ func TestWatcher_Rename(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -139,7 +139,7 @@ func TestWatcher_IgnoredFileEvents(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -167,7 +167,7 @@ func TestWatcher_Debouncing(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -199,7 +199,7 @@ func TestWatcher_CreateDirectory(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -229,7 +229,7 @@ func TestWatcher_RemoveDirectory(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -255,7 +255,7 @@ func TestWatcher_VerifyManifestUpdated(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -282,7 +282,7 @@ func TestWatcher_NestedGitignore_CreateIgnored(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -310,7 +310,7 @@ func TestWatcher_NestedGitignore_CreateUnignore(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -332,7 +332,7 @@ func TestWatcher_CreateSymlink(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -382,7 +382,7 @@ func TestWatcher_UpdateSymlinkTarget(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -421,7 +421,7 @@ func TestWatcher_RemoveSymlink(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) @@ -447,7 +447,7 @@ func TestWatcher_ModifySymlinkTargetFile(t *testing.T) { ctx, cancel := context.WithCancel(context.Background()) defer cancel() - go watch(ctx, host, data, manifest, stack, false, false) + go watch(ctx, host, data, manifest, stack, false) time.Sleep(100 * time.Millisecond) |
