Introduce slog for structured daemon logging (bd-u2sc.4)

- Replace custom daemonLogger with slog-based structured logging
- Add --log-level flag (debug, info, warn, error) with default "info"
- Add --log-json flag for JSON-formatted log output
- Update log calls to use appropriate levels (Error, Warn, Info, Debug)
- Add test helper functions (newTestLogger, newTestLoggerWithWriter)

Log output examples:
  Text: time=2025-12-23T12:30:00Z level=INFO msg="daemon started" socket=/tmp/bd.sock
  JSON: {"time":"...","level":"INFO","msg":"daemon started","socket":"/tmp/bd.sock"}

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Steve Yegge
2025-12-23 13:43:40 -08:00
parent d834e4f4d8
commit 05f37d2408
10 changed files with 237 additions and 160 deletions

View File

@@ -369,7 +369,7 @@ func stopAllDaemons() {
}
// startDaemon starts the daemon (in foreground if requested, otherwise background)
func startDaemon(interval time.Duration, autoCommit, autoPush, autoPull, localMode, foreground bool, logFile, pidFile string) {
func startDaemon(interval time.Duration, autoCommit, autoPush, autoPull, localMode, foreground bool, logFile, pidFile, logLevel string, logJSON bool) {
logPath, err := getLogFilePath(logFile)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
@@ -378,7 +378,7 @@ func startDaemon(interval time.Duration, autoCommit, autoPush, autoPull, localMo
// Run in foreground if --foreground flag set or if we're the forked child process
if foreground || os.Getenv("BD_DAEMON_FOREGROUND") == "1" {
runDaemonLoop(interval, autoCommit, autoPush, autoPull, localMode, logPath, pidFile)
runDaemonLoop(interval, autoCommit, autoPush, autoPull, localMode, logPath, pidFile, logLevel, logJSON)
return
}
@@ -406,6 +406,12 @@ func startDaemon(interval time.Duration, autoCommit, autoPush, autoPull, localMo
if logFile != "" {
args = append(args, "--log", logFile)
}
if logLevel != "" && logLevel != "info" {
args = append(args, "--log-level", logLevel)
}
if logJSON {
args = append(args, "--log-json")
}
cmd := exec.Command(exe, args...) // #nosec G204 - bd daemon command from trusted binary
cmd.Env = append(os.Environ(), "BD_DAEMON_FOREGROUND=1")
@@ -455,18 +461,18 @@ func setupDaemonLock(pidFile string, dbPath string, log daemonLogger) (*DaemonLo
// Detect nested .beads directories (e.g., .beads/.beads/.beads/)
cleanPath := filepath.Clean(beadsDir)
if strings.Contains(cleanPath, string(filepath.Separator)+".beads"+string(filepath.Separator)+".beads") {
log.log("Error: Nested .beads directory detected: %s", cleanPath)
log.log("Hint: Do not run 'bd daemon' from inside .beads/ directory")
log.log("Hint: Use absolute paths for BEADS_DB or run from workspace root")
log.Error("nested .beads directory detected", "path", cleanPath)
log.Info("hint: do not run 'bd daemon' from inside .beads/ directory")
log.Info("hint: use absolute paths for BEADS_DB or run from workspace root")
return nil, fmt.Errorf("nested .beads directory detected")
}
lock, err := acquireDaemonLock(beadsDir, dbPath)
if err != nil {
if err == ErrDaemonLocked {
log.log("Daemon already running (lock held), exiting")
log.Info("daemon already running (lock held), exiting")
} else {
log.log("Error acquiring daemon lock: %v", err)
log.Error("acquiring daemon lock", "error", err)
}
return nil, err
}
@@ -477,11 +483,11 @@ func setupDaemonLock(pidFile string, dbPath string, log daemonLogger) (*DaemonLo
if pid, err := strconv.Atoi(strings.TrimSpace(string(data))); err == nil && pid == myPID {
// PID file is correct, continue
} else {
log.log("PID file has wrong PID (expected %d, got %d), overwriting", myPID, pid)
log.Warn("PID file has wrong PID, overwriting", "expected", myPID, "got", pid)
_ = os.WriteFile(pidFile, []byte(fmt.Sprintf("%d\n", myPID)), 0600)
}
} else {
log.log("PID file missing after lock acquisition, creating")
log.Info("PID file missing after lock acquisition, creating")
_ = os.WriteFile(pidFile, []byte(fmt.Sprintf("%d\n", myPID)), 0600)
}