Host detail v2: full pipeline + per-stage logs + WoL diagnostics
CI / Lint + build + test (push) Has been cancelled
CI / Lint + build + test (push) Has been cancelled
Pipeline now always renders all 13 nodes (3 pre-stage + 9 stage +
Completed), synthesising ghosts from run state when stage rows
aren't seeded yet. Makes a WaitingWoL host show the full timeline
ahead of it instead of just 4 dots.
Agent tags each log line with its stage; logs.Hub fans out to both
log-{runID} and log-{runID}-{stage} SSE events so the detail page
can show per-stage tabs with a pure-CSS radio-sibling switch. Flat
run log prepends [stage] so grep still works.
Dispatcher writes picked/sent-WoL/heartbeat lines into the per-run
log — the operator opens the detail page, sees WaitingWoL stuck,
and reads exactly what the dispatcher did and why nothing's
progressing, instead of having to tail journalctl on the LXC.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -134,6 +134,7 @@ type HeartbeatResponse struct {
|
||||
type LogLine struct {
|
||||
TS string `json:"ts,omitempty"`
|
||||
Level string `json:"level,omitempty"`
|
||||
Stage string `json:"stage,omitempty"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
|
||||
+21
-1
@@ -120,6 +120,8 @@ func Run(ctx context.Context, p *bootstate.Params) error {
|
||||
// (the orchestrator persists it as an artifact). Every other stage
|
||||
// returns a tests.Outcome which postResult marshals generically.
|
||||
func runStage(ctx context.Context, stage string, claim *ClaimResponse, fwd *logForwarder, c *Client, ovr overrideFlags) stageOutcome {
|
||||
fwd.SetStage(stage)
|
||||
defer fwd.ClearStage()
|
||||
deps := newDeps(ctx, c, fwd, ovr, claim)
|
||||
switch stage {
|
||||
case "Inventory":
|
||||
@@ -436,6 +438,7 @@ type logForwarder struct {
|
||||
c *Client
|
||||
mu sync.Mutex
|
||||
buf []LogLine
|
||||
stage string // set via SetStage; empties via ClearStage
|
||||
wg sync.WaitGroup
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
@@ -467,7 +470,7 @@ func (f *logForwarder) push(level, text string) {
|
||||
stamp := time.Now().UTC().Format(time.RFC3339Nano)
|
||||
log.Printf("[%s] %s", level, text)
|
||||
f.mu.Lock()
|
||||
f.buf = append(f.buf, LogLine{TS: stamp, Level: level, Text: text})
|
||||
f.buf = append(f.buf, LogLine{TS: stamp, Level: level, Stage: f.stage, Text: text})
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
@@ -475,6 +478,23 @@ func (f *logForwarder) info(s string) { f.push("info", s) }
|
||||
func (f *logForwarder) warn(s string) { f.push("warn", s) }
|
||||
func (f *logForwarder) error(s string) { f.push("error", s) }
|
||||
|
||||
// SetStage tags subsequent log lines with a stage name so the orchestrator
|
||||
// can fan them out on a per-stage SSE event. Safe to call concurrently
|
||||
// with push — we take the same mutex.
|
||||
func (f *logForwarder) SetStage(stage string) {
|
||||
f.mu.Lock()
|
||||
f.stage = stage
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
// ClearStage reverts to untagged (framing-level) logging. Defer this
|
||||
// on entry to runStage so hold/override paths don't leak stage context.
|
||||
func (f *logForwarder) ClearStage() {
|
||||
f.mu.Lock()
|
||||
f.stage = ""
|
||||
f.mu.Unlock()
|
||||
}
|
||||
|
||||
func (f *logForwarder) flush() {
|
||||
f.mu.Lock()
|
||||
if len(f.buf) == 0 {
|
||||
|
||||
Reference in New Issue
Block a user