26a43708b7
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
83 lines
2.9 KiB
Go
83 lines
2.9 KiB
Go
package web
|
|
|
|
import (
|
|
"bytes"
|
|
"io"
|
|
"log"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// S1 capture-at-info (controller half): with the v0.116.0 writer layout —
|
|
// MultiWriter(LevelFilterWriter(stdout, "info"), LogBuffer) — a [DEBUG] line
|
|
// reaches the RING and is ABSENT from stdout, while [INFO] reaches both.
|
|
// Companion red-proof: gate the ring behind the same filter (the pre-fix shape,
|
|
// where the ring only existed at logging.level=debug) → the ring assertion fails.
|
|
func TestCaptureAtInfo_RingHoldsDebugStdoutDoesNot(t *testing.T) {
|
|
var stdout bytes.Buffer
|
|
lb := NewLogBuffer(50)
|
|
logger := log.New(io.MultiWriter(NewLevelFilterWriter(&stdout, "info"), lb), "", log.LstdFlags)
|
|
|
|
logger.Printf("[DEBUG] [web] netstorage add \"vids\" phase agent_add -> verifying")
|
|
logger.Printf("[INFO] [web] network storage added + verified: vids")
|
|
|
|
entries, total := lb.Entries("DEBUG", 0, time.Time{})
|
|
if total != 2 || len(entries) != 2 {
|
|
t.Fatalf("ring holds %d entries (returned %d), want 2 — DEBUG must be captured at logging.level=info", total, len(entries))
|
|
}
|
|
if entries[0].Level != "DEBUG" || !strings.Contains(entries[0].Message, "phase agent_add") {
|
|
t.Errorf("ring entry 0 = %+v, want the DEBUG phase line", entries[0])
|
|
}
|
|
if strings.Contains(stdout.String(), "phase agent_add") {
|
|
t.Errorf("stdout carries the DEBUG line at level info:\n%s", stdout.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), "added + verified") {
|
|
t.Errorf("stdout missing the INFO line:\n%s", stdout.String())
|
|
}
|
|
}
|
|
|
|
// The filter respects higher minimums too (warn drops INFO) and passes untagged lines at info.
|
|
func TestLevelFilterWriter_Thresholds(t *testing.T) {
|
|
cases := []struct {
|
|
min string
|
|
line string
|
|
wants bool
|
|
}{
|
|
{"info", "[DEBUG] x", false},
|
|
{"info", "[INFO] x", true},
|
|
{"info", "untagged line", true}, // parses as INFO
|
|
{"warn", "[INFO] x", false},
|
|
{"warn", "[ERROR] x", true},
|
|
{"debug", "[DEBUG] x", true},
|
|
}
|
|
for _, c := range cases {
|
|
var out bytes.Buffer
|
|
logger := log.New(NewLevelFilterWriter(&out, c.min), "", log.LstdFlags)
|
|
logger.Printf("%s", c.line)
|
|
got := strings.Contains(out.String(), c.line)
|
|
if got != c.wants {
|
|
t.Errorf("min=%s line=%q passed=%v want %v", c.min, c.line, got, c.wants)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lines renders chronological plain-text lines and honors the byte budget by
|
|
// keeping the NEWEST lines (the controller_log_tail 128 KB cap).
|
|
func TestLogBufferLines_ByteBudgetKeepsNewest(t *testing.T) {
|
|
lb := NewLogBuffer(10)
|
|
logger := log.New(lb, "", log.LstdFlags)
|
|
for i := 0; i < 5; i++ {
|
|
logger.Printf("[INFO] line-%d %s", i, strings.Repeat("x", 80))
|
|
}
|
|
all := lb.Lines(0)
|
|
if len(all) != 5 || !strings.Contains(all[4], "line-4") {
|
|
t.Fatalf("uncapped lines wrong: %d %v", len(all), all)
|
|
}
|
|
budget := len(all[3]) + len(all[4]) + 2
|
|
capped := lb.Lines(budget)
|
|
if len(capped) >= 5 || !strings.Contains(capped[len(capped)-1], "line-4") {
|
|
t.Errorf("byte cap kept %d lines, newest=%q — must drop oldest first", len(capped), capped[len(capped)-1])
|
|
}
|
|
}
|