package report import ( "log" "sync" "time" ) // Controller self-log tail (v0.116.0 observability) — the CONTROLLER's own debug // ring riding the report channel, the exact consume-once ACK-flag shape of // logtail.go (which stays byte-compatible; these are ADDITIVE fields): the hub // stores a pending per-customer request; the report ACK advertises it as // controller_log_requested; the NEXT report carries controller_log_tail; the hub // clears the pending request on arrival. A failed push leaves the hub request // pending — the next ACK re-arms it (fail-safe retry, no duplicate shipping). // controllerLogMaxBytes caps the shipped tail (newest lines kept — the ring's // Lines budget, mirrored agent-side at the same 128 KB). const controllerLogMaxBytes = 128 * 1024 // ControllerLogTail is the report's on-demand controller ring tail. type ControllerLogTail struct { CollectedAt time.Time `json:"collected_at"` Lines []string `json:"lines"` } var ( selfTailMu sync.Mutex selfTailPending bool selfTailSource func(maxBytes int) []string ) // SetPendingControllerLog records the ACK's controller_log_requested flag. The // hub is the source of truth: an ACK without the flag clears any stale pending. func SetPendingControllerLog(requested bool) { selfTailMu.Lock() defer selfTailMu.Unlock() selfTailPending = requested } // SetControllerLogSource wires the debug ring (web.LogBuffer.Lines) — set once by // main.go at startup. Unset means a pending request is silently unfulfillable // (drained but nothing shipped; the hub re-arms on the next ACK). func SetControllerLogSource(src func(maxBytes int) []string) { selfTailMu.Lock() defer selfTailMu.Unlock() selfTailSource = src } // drainPendingControllerLog takes + CLEARS the pending flag and returns the // source (consume-once controller-side, the drainPendingLogTails shape). func drainPendingControllerLog() (func(maxBytes int) []string, bool) { selfTailMu.Lock() defer selfTailMu.Unlock() pending := selfTailPending selfTailPending = false return selfTailSource, pending } // buildControllerLogTail attaches the ring tail when a pull is pending. The INFO // line is the CUSTOMER-VISIBLE transparency record (it lands in the ring/viewer): // an operator pull of this box's controller log is never silent. func buildControllerLogTail(logger *log.Logger) *ControllerLogTail { src, pending := drainPendingControllerLog() if !pending || src == nil { return nil } // Logged BEFORE collecting, so the transparency line rides in the tail it announces. if logger != nil { logger.Printf("[INFO] [report] operator log pull served (controller ring)") } lines := src(controllerLogMaxBytes) return &ControllerLogTail{CollectedAt: time.Now().UTC(), Lines: lines} }