package cluster import ( "log" "os" "sync/atomic" ) // logf is the package's logging seam: every cluster log line funnels through it // so the debug gate in debug.go has a single place to hook. func logf(format string, args ...any) { log.Printf(format, args...) } // Token rotation is the ring's heartbeat: with a 2s round delay it fires // continuously, and the default three log lines per round // ("OnToken cycle=N" / "forward cycle=N to X" / "token-send ... -> 200") // carry no information — no cycle number, address, timing or payload ever // changes in the steady state. Measured on the live 3-node cluster that was // ~510k lines/day on one node (3.5M lines in a week), which drowns every real // event in the journal and fills the disk for a signal that is already // available in structured form via GET /api/manager/cluster/ring (cycle, // lastSync, roundDelayMs, node aliveness). // // So the steady-state lines are demoted to a debug level, off by default and // enabled with W4F_DEBUG=token (or 1/all/true for every debug line). Failure // paths are NOT demoted: a send error, a stale token, a timeout or a leader // change is exactly what someone is grepping for, and losing those to a quiet // default would be a bad trade. const debugEnv = "W4F_DEBUG" // debugToken logs a per-token-round heartbeat line. Suppressed unless // W4F_DEBUG selects "token" (or a catch-all value). func debugToken(format string, args ...any) { if debugTokenOn.Load() { logf(format, args...) } } // debugAll logs an ad-hoc diagnostic line. Suppressed unless W4F_DEBUG is set // to a catch-all value (1/all/true/*). func debugAll(format string, args ...any) { if debugAllOn.Load() { logf(format, args...) } } var ( debugTokenOn atomic.Bool debugAllOn atomic.Bool ) func init() { ReloadDebug() } // ReloadDebug re-reads W4F_DEBUG. Called once at init so tests can flip it // without restarting, and available at runtime for an operator who wants to // watch the ring without a redeploy. func ReloadDebug() { v := os.Getenv(debugEnv) switch normalized := normalizeDebugValue(v); normalized { case "token": debugTokenOn.Store(true) debugAllOn.Store(false) case "all": debugTokenOn.Store(true) debugAllOn.Store(true) default: debugTokenOn.Store(false) debugAllOn.Store(false) } } func normalizeDebugValue(v string) string { // Compare case-insensitively without pulling in strings just for this. out := make([]rune, 0, len(v)) for _, r := range v { if r >= 'A' && r <= 'Z' { r += 'a' - 'A' } out = append(out, r) } s := string(out) switch s { case "": return "" case "token", "tokens", "ring": return "token" } // Any other non-empty value is a deliberate request for more output, so it // is treated as a catch-all rather than silently muting the operator who // set it. "0" lands here too: it was asked for, so honour it. return "all" }