Centralize BD_DEBUG logging into internal/debug package

- Created internal/debug package with Enabled(), Logf(), Printf()
- Added comprehensive unit tests for debug package
- Replaced 50+ scattered os.Getenv("BD_DEBUG") checks across 9 files
- Centralized debug logic for easier maintenance and testing
- All tests passing, behavior unchanged

Closes bd-fb95094c.5
This commit is contained in:
Steve Yegge
2025-11-06 20:14:22 -08:00
parent 04621fe731
commit 95cbcf4fbc
16 changed files with 364 additions and 280 deletions

View File

@@ -15,6 +15,7 @@ import (
"github.com/fatih/color"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/debug"
"github.com/steveyegge/beads/internal/types"
"golang.org/x/mod/semver"
)
@@ -66,9 +67,7 @@ func autoImportIfNewer() {
jsonlData, err := os.ReadFile(jsonlPath)
if err != nil {
// JSONL doesn't exist or can't be accessed, skip import
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-import skipped, JSONL not found: %v\n", err)
}
debug.Logf("auto-import skipped, JSONL not found: %v", err)
return
}
@@ -83,24 +82,18 @@ func autoImportIfNewer() {
if err != nil {
// Metadata error - treat as first import rather than skipping (bd-663)
// This allows auto-import to recover from corrupt/missing metadata
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: metadata read failed (%v), treating as first import\n", err)
}
debug.Logf("metadata read failed (%v), treating as first import", err)
lastHash = ""
}
// Compare hashes
if currentHash == lastHash {
// Content unchanged, skip import
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-import skipped, JSONL unchanged (hash match)\n")
}
debug.Logf("auto-import skipped, JSONL unchanged (hash match)")
return
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-import triggered (hash changed)\n")
}
debug.Logf("auto-import triggered (hash changed)")
// Check for Git merge conflict markers (bd-270)
// Only match if they appear as standalone lines (not embedded in JSON strings)
@@ -254,9 +247,7 @@ func checkVersionMismatch() {
dbVersion, err := store.GetMetadata(ctx, "bd_version")
if err != nil {
// Metadata error - skip check (shouldn't happen, but be defensive)
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: version check skipped, metadata error: %v\n", err)
}
debug.Logf("version check skipped, metadata error: %v", err)
return
}
@@ -500,8 +491,8 @@ func writeJSONLAtomic(jsonlPath string, issues []*types.Issue) ([]string, error)
}
// Report skipped issues if any (helps debugging bd-159)
if skippedCount > 0 && os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-flush skipped %d issue(s) with timestamp-only changes\n", skippedCount)
if skippedCount > 0 {
debug.Logf("auto-flush skipped %d issue(s) with timestamp-only changes", skippedCount)
}
// Close temp file before renaming
@@ -520,9 +511,7 @@ func writeJSONLAtomic(jsonlPath string, issues []*types.Issue) ([]string, error)
// nolint:gosec // G302: JSONL needs to be readable by other tools
if err := os.Chmod(jsonlPath, 0644); err != nil {
// Non-fatal - file is already written
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: failed to set file permissions: %v\n", err)
}
debug.Logf("failed to set file permissions: %v", err)
}
return exportedIDs, nil

View File

@@ -10,6 +10,7 @@ import (
"github.com/fatih/color"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/debug"
"github.com/steveyegge/beads/internal/routing"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/types"
@@ -127,8 +128,8 @@ var createCmd = &cobra.Command{
} else {
// Auto-routing based on user role
userRole, err := routing.DetectUserRole(".")
if err != nil && os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Warning: failed to detect user role: %v\n", err)
if err != nil {
debug.Logf("Warning: failed to detect user role: %v\n", err)
}
routingConfig := &routing.RoutingConfig{
@@ -144,8 +145,8 @@ var createCmd = &cobra.Command{
// TODO: Switch to target repo for multi-repo support (bd-4ms)
// For now, we just log the target repo in debug mode
if os.Getenv("BD_DEBUG") != "" && repoPath != "." {
fmt.Fprintf(os.Stderr, "DEBUG: Target repo: %s\n", repoPath)
if repoPath != "." {
debug.Logf("DEBUG: Target repo: %s\n", repoPath)
}
// Check for conflicting flags

View File

@@ -10,6 +10,7 @@ import (
"time"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/debug"
"github.com/steveyegge/beads/internal/rpc"
)
@@ -49,9 +50,7 @@ func restartDaemonForVersionMismatch() bool {
// Use local daemon (global is deprecated)
pidFile, err := getPIDFilePath(false)
if err != nil {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: failed to get PID file path: %v\n", err)
}
debug.Logf("failed to get PID file path: %v", err)
return false
}
@@ -60,23 +59,17 @@ func restartDaemonForVersionMismatch() bool {
// Check if daemon is running and stop it
forcedKill := false
if isRunning, pid := isDaemonRunning(pidFile); isRunning {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: stopping old daemon (PID %d)\n", pid)
}
debug.Logf("stopping old daemon (PID %d)", pid)
process, err := os.FindProcess(pid)
if err != nil {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: failed to find process: %v\n", err)
}
debug.Logf("failed to find process: %v", err)
return false
}
// Send stop signal
if err := sendStopSignal(process); err != nil {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: failed to signal daemon: %v\n", err)
}
debug.Logf("failed to signal daemon: %v", err)
return false
}
@@ -84,18 +77,14 @@ func restartDaemonForVersionMismatch() bool {
for i := 0; i < 50; i++ {
time.Sleep(100 * time.Millisecond)
if isRunning, _ := isDaemonRunning(pidFile); !isRunning {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: old daemon stopped successfully\n")
}
debug.Logf("old daemon stopped successfully")
break
}
}
// Force kill if still running
if isRunning, _ := isDaemonRunning(pidFile); isRunning {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: force killing old daemon\n")
}
debug.Logf("force killing old daemon")
_ = process.Kill()
forcedKill = true
}
@@ -110,9 +99,7 @@ func restartDaemonForVersionMismatch() bool {
// Start new daemon with current binary version
exe, err := os.Executable()
if err != nil {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: failed to get executable path: %v\n", err)
}
debug.Logf("failed to get executable path: %v", err)
return false
}
@@ -136,9 +123,7 @@ func restartDaemonForVersionMismatch() bool {
}
if err := cmd.Start(); err != nil {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: failed to start new daemon: %v\n", err)
}
debug.Logf("failed to start new daemon: %v", err)
return false
}
@@ -147,15 +132,11 @@ func restartDaemonForVersionMismatch() bool {
// Wait for daemon to be ready using shared helper
if waitForSocketReadiness(socketPath, 5*time.Second) {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: new daemon started successfully\n")
}
debug.Logf("new daemon started successfully")
return true
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: new daemon failed to become ready\n")
}
debug.Logf("new daemon failed to become ready")
return false
}
@@ -197,9 +178,7 @@ func tryAutoStartDaemon(socketPath string) bool {
}
func debugLog(msg string, args ...interface{}) {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: "+msg+"\n", args...)
}
debug.Logf(msg, args...)
}
func isDaemonHealthy(socketPath string) bool {

View File

@@ -2,9 +2,9 @@ package main
import (
"fmt"
"os"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/debug"
"github.com/steveyegge/beads/internal/storage/sqlite"
)
@@ -43,8 +43,8 @@ func disableDaemonForFallback(reason string) {
daemonStatus.FallbackReason = FallbackDaemonUnsupported
}
if reason != "" && os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: %s\n", reason)
if reason != "" {
debug.Logf("Debug: %s\n", reason)
}
}

View File

@@ -12,6 +12,7 @@ import (
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/debug"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types"
)
@@ -98,9 +99,7 @@ Output to stdout by default, or use -o flag for file output.`,
// Export command requires direct database access for consistent snapshot
// If daemon is connected, close it and open direct connection
if daemonClient != nil {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: export command forcing direct mode (closes daemon connection)\n")
}
debug.Logf("Debug: export command forcing direct mode (closes daemon connection)\n")
_ = daemonClient.Close()
daemonClient = nil
}

View File

@@ -10,6 +10,7 @@ import (
"strings"
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/debug"
"github.com/steveyegge/beads/internal/storage/sqlite"
"github.com/steveyegge/beads/internal/types"
)
@@ -34,9 +35,7 @@ NOTE: Import requires direct database access and does not work with daemon mode.
// Import requires direct database access due to complex transaction handling
// and collision detection. Force direct mode regardless of daemon state.
if daemonClient != nil {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: import command forcing direct mode (closes daemon connection)\n")
}
debug.Logf("Debug: import command forcing direct mode (closes daemon connection)\n")
_ = daemonClient.Close()
daemonClient = nil

View File

@@ -11,6 +11,7 @@ import (
"github.com/spf13/cobra"
"github.com/steveyegge/beads/internal/beads"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/debug"
"github.com/steveyegge/beads/internal/rpc"
"github.com/steveyegge/beads/internal/storage"
"github.com/steveyegge/beads/internal/storage/memory"
@@ -229,9 +230,7 @@ var rootCmd = &cobra.Command{
// Try to connect to daemon first (unless --no-daemon flag is set)
if noDaemon {
daemonStatus.FallbackReason = FallbackFlagNoDaemon
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: --no-daemon flag set, using direct mode\n")
}
debug.Logf("--no-daemon flag set, using direct mode")
} else {
// Attempt daemon connection
client, err := rpc.TryConnect(socketPath)
@@ -247,10 +246,8 @@ var rootCmd = &cobra.Command{
if healthErr == nil && health.Status == statusHealthy {
// Check version compatibility
if !health.Compatible {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: daemon version mismatch (daemon: %s, client: %s), restarting daemon\n",
health.Version, Version)
}
debug.Logf("daemon version mismatch (daemon: %s, client: %s), restarting daemon",
health.Version, Version)
_ = client.Close()
// Kill old daemon and restart with new version
@@ -269,9 +266,7 @@ var rootCmd = &cobra.Command{
daemonStatus.Connected = true
daemonStatus.Degraded = false
daemonStatus.Health = health.Status
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: connected to restarted daemon (version: %s)\n", health.Version)
}
debug.Logf("connected to restarted daemon (version: %s)", health.Version)
warnWorktreeDaemon(dbPath)
return
}
@@ -288,9 +283,7 @@ var rootCmd = &cobra.Command{
daemonStatus.Connected = true
daemonStatus.Degraded = false
daemonStatus.Health = health.Status
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: connected to daemon at %s (health: %s)\n", socketPath, health.Status)
}
debug.Logf("connected to daemon at %s (health: %s)", socketPath, health.Status)
// Warn if using daemon with git worktrees
warnWorktreeDaemon(dbPath)
return // Skip direct storage initialization
@@ -301,15 +294,11 @@ var rootCmd = &cobra.Command{
daemonStatus.FallbackReason = FallbackHealthFailed
if healthErr != nil {
daemonStatus.Detail = healthErr.Error()
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: daemon health check failed: %v\n", healthErr)
}
debug.Logf("daemon health check failed: %v", healthErr)
} else {
daemonStatus.Health = health.Status
daemonStatus.Detail = health.Error
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: daemon unhealthy (status=%s): %s\n", health.Status, health.Error)
}
debug.Logf("daemon unhealthy (status=%s): %s", health.Status, health.Error)
}
}
} else {
@@ -317,18 +306,14 @@ var rootCmd = &cobra.Command{
daemonStatus.FallbackReason = FallbackConnectFailed
if err != nil {
daemonStatus.Detail = err.Error()
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: daemon connect failed at %s: %v\n", socketPath, err)
}
debug.Logf("daemon connect failed at %s: %v", socketPath, err)
}
}
// Daemon not running or unhealthy - try auto-start if enabled
if daemonStatus.AutoStartEnabled {
daemonStatus.AutoStartAttempted = true
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: attempting to auto-start daemon\n")
}
debug.Logf("attempting to auto-start daemon")
startTime := time.Now()
if tryAutoStartDaemon(socketPath) {
// Retry connection after auto-start
@@ -350,10 +335,8 @@ var rootCmd = &cobra.Command{
daemonStatus.AutoStartSucceeded = true
daemonStatus.Health = health.Status
daemonStatus.FallbackReason = FallbackNone
if os.Getenv("BD_DEBUG") != "" {
elapsed := time.Since(startTime).Milliseconds()
fmt.Fprintf(os.Stderr, "Debug: auto-start succeeded; connected at %s in %dms\n", socketPath, elapsed)
}
elapsed := time.Since(startTime).Milliseconds()
debug.Logf("auto-start succeeded; connected at %s in %dms", socketPath, elapsed)
// Warn if using daemon with git worktrees
warnWorktreeDaemon(dbPath)
return // Skip direct storage initialization
@@ -367,9 +350,7 @@ var rootCmd = &cobra.Command{
daemonStatus.Health = health.Status
daemonStatus.Detail = health.Error
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-started daemon is unhealthy; falling back to direct mode\n")
}
debug.Logf("auto-started daemon is unhealthy; falling back to direct mode")
}
} else {
// Auto-start completed but connection still failed
@@ -386,24 +367,18 @@ var rootCmd = &cobra.Command{
daemonStatus.Detail = string(errMsg)
}
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-start did not yield a running daemon; falling back to direct mode\n")
}
debug.Logf("auto-start did not yield a running daemon; falling back to direct mode")
}
} else {
// Auto-start itself failed
daemonStatus.FallbackReason = FallbackAutoStartFailed
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-start failed; falling back to direct mode\n")
}
debug.Logf("auto-start failed; falling back to direct mode")
}
} else {
// Auto-start disabled - preserve the actual failure reason
// Don't override connect_failed or health_failed with auto_start_disabled
// This preserves important diagnostic info (daemon crashed vs not running)
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-start disabled by BEADS_AUTO_START_DAEMON\n")
}
debug.Logf("auto-start disabled by BEADS_AUTO_START_DAEMON")
}
// Emit BD_VERBOSE warning if falling back to direct mode
@@ -411,9 +386,7 @@ var rootCmd = &cobra.Command{
emitVerboseWarning()
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: using direct mode (reason: %s)\n", daemonStatus.FallbackReason)
}
debug.Logf("using direct mode (reason: %s)", daemonStatus.FallbackReason)
}
// Fall back to direct storage access
@@ -443,9 +416,7 @@ var rootCmd = &cobra.Command{
if cmd.Name() == "sync" {
if dryRun, _ := cmd.Flags().GetBool("dry-run"); dryRun {
// Skip auto-import in dry-run mode
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: auto-import skipped for sync --dry-run\n")
}
debug.Logf("auto-import skipped for sync --dry-run")
} else {
autoImportIfNewer()
}

View File

@@ -10,6 +10,7 @@ import (
"strings"
"github.com/steveyegge/beads/internal/config"
"github.com/steveyegge/beads/internal/debug"
"github.com/steveyegge/beads/internal/storage/memory"
"github.com/steveyegge/beads/internal/types"
"github.com/steveyegge/beads/internal/utils"
@@ -54,13 +55,9 @@ func initializeNoDbMode() error {
return fmt.Errorf("failed to load issues into memory: %w", err)
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: loaded %d issues from %s\n", len(issues), jsonlPath)
}
debug.Logf("loaded %d issues from %s", len(issues), jsonlPath)
} else {
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: no existing %s, starting with empty database\n", jsonlPath)
}
debug.Logf("no existing %s, starting with empty database", jsonlPath)
}
// Detect and set prefix
@@ -74,9 +71,7 @@ func initializeNoDbMode() error {
return fmt.Errorf("failed to set prefix: %w", err)
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: using prefix '%s'\n", prefix)
}
debug.Logf("using prefix '%s'", prefix)
// Set global store
store = memStore
@@ -203,9 +198,7 @@ func writeIssuesToJSONL(memStore *memory.MemoryStorage, beadsDir string) error {
return err
}
if os.Getenv("BD_DEBUG") != "" {
fmt.Fprintf(os.Stderr, "Debug: wrote %d issues to %s\n", len(issues), jsonlPath)
}
debug.Logf("wrote %d issues to %s", len(issues), jsonlPath)
return nil
}