v0.116.0: observability pass — always-on debug ring + leveled sweep + agent tab + self-log pull — MinAgent: 0.81.0

Capture layer: LogBuffer always exists; logger = MultiWriter(LevelFilterWriter
(stdout, logging.level), ring) so DEBUG detail exists remotely without a config
flip while docker logs keep respecting the level. New internal/logx leveled
helpers. Report ACK gains controller_log_requested (additive); next report
ships controller_log_tail (128KB, consume-once, app-tail wire byte-compatible).
Debug page: Vezérlő|Ügynök tabs; agent tab proxies agent /debug/logs with the
pre-0.83 notice on typed 404. Sweep: netstorage_job phases, netprobe, handler
validation refusals + orphan WARN, SupportsWithSource gate line, agentapi
per-call DEBUG, migrate phase lines, tier2/offbox unswallowed persists.
Red-proofs: filter-disabled, drain-removed, dropped-phase-line all FAIL.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-11 16:45:57 +02:00
parent 647116480a
commit 26a43708b7
25 changed files with 797 additions and 31 deletions
+107
View File
@@ -0,0 +1,107 @@
package report
import (
"bytes"
"encoding/json"
"log"
"strings"
"testing"
"time"
)
func resetSelfTail() {
SetPendingControllerLog(false)
SetControllerLogSource(nil)
}
// S2 (controller half): the ACK's controller_log_requested arms the pull; the next
// build attaches the ring tail; the build after that attaches nothing (consume-once).
// Companion red-proof: remove the `selfTailPending = false` drain → the second build
// ships again → the nil assertion fails.
func TestControllerLogTail_ConsumeOnce(t *testing.T) {
t.Cleanup(resetSelfTail)
SetControllerLogSource(func(maxBytes int) []string { return []string{"l1", "l2", "l3"} })
SetPendingControllerLog(true)
var logbuf bytes.Buffer
logger := log.New(&logbuf, "", 0)
first := buildControllerLogTail(logger)
if first == nil || len(first.Lines) != 3 || first.CollectedAt.IsZero() {
t.Fatalf("first build tail = %+v, want the 3 ring lines + collected_at", first)
}
// Customer transparency: the pull is announced in the box's own log.
if !strings.Contains(logbuf.String(), "operator log pull served") {
t.Errorf("transparency INFO line missing: %q", logbuf.String())
}
if second := buildControllerLogTail(logger); second != nil {
t.Errorf("second build shipped again — consume-once broken: %+v", second)
}
}
// The hub is the source of truth: an ACK without the flag clears a stale pending.
func TestControllerLogTail_AckClears(t *testing.T) {
t.Cleanup(resetSelfTail)
SetControllerLogSource(func(int) []string { return []string{"x"} })
SetPendingControllerLog(true)
SetPendingControllerLog(false)
if tail := buildControllerLogTail(nil); tail != nil {
t.Errorf("cleared pending still shipped: %+v", tail)
}
}
// No source wired (defensive) → pending is drained, nothing shipped, no panic.
func TestControllerLogTail_NoSource(t *testing.T) {
t.Cleanup(resetSelfTail)
SetPendingControllerLog(true)
if tail := buildControllerLogTail(nil); tail != nil {
t.Errorf("tail shipped with no source: %+v", tail)
}
}
// S3 app-tail byte-compatibility: the v0.111.0 log_tails wire shape is unchanged by
// the additive controller_log_tail — absent in the steady state (omitempty), and the
// existing keys marshal identically. Companion red-proof: drop `omitempty` from
// ControllerLogTail → the steady-state JSON gains a null key → FAIL.
func TestReportSchema_AppTailUnchangedAndSelfTailAdditive(t *testing.T) {
r := &Report{
Version: 1,
CustomerID: "c1",
LogTails: []LogTail{{
App: "gokapi", CollectedAt: time.Date(2026, 7, 11, 10, 0, 0, 0, time.UTC),
Lines: []string{"a", "b"},
}},
}
raw, err := json.Marshal(r)
if err != nil {
t.Fatal(err)
}
var m map[string]json.RawMessage
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
if _, present := m["controller_log_tail"]; present {
t.Errorf("controller_log_tail present in the steady state — must be omitted (additive)")
}
var tails []map[string]json.RawMessage
if err := json.Unmarshal(m["log_tails"], &tails); err != nil || len(tails) != 1 {
t.Fatalf("log_tails shape changed: %s", m["log_tails"])
}
for _, key := range []string{"app", "collected_at", "lines"} {
if _, ok := tails[0][key]; !ok {
t.Errorf("log_tails entry lost key %q — app-tail flow must stay byte-compatible", key)
}
}
// The ACK parse: old fields + the new flag coexist; absent flag = false.
var pr PushResponse
if err := json.Unmarshal([]byte(`{"status":"ok","log_tail_requests":["gokapi"],"controller_log_requested":true}`), &pr); err != nil {
t.Fatal(err)
}
if len(pr.LogTailRequests) != 1 || !pr.ControllerLogRequested {
t.Errorf("ACK parse = %+v", pr)
}
var old PushResponse
if err := json.Unmarshal([]byte(`{"status":"ok"}`), &old); err != nil || old.ControllerLogRequested {
t.Errorf("old ACK must parse with the flag false: %+v err=%v", old, err)
}
}