v0.111.0: remote app-log diagnostics — error context capture (±5 lines, capped+redacted) + on-demand log tails via ACK pull pattern

- metrics: LogIssue.Context on first-occurrence errors (≤11 lines, ≤400 chars/line, warns carry none); RedactLine sanitizer (password/token/api-key/bearer/64-hex) applied to everything shipped; FetchContainerLogTail
- report: 16KB per-report context budget (lowest-count issues dropped first); log_tail_requests ACK flag → next report ships log_tails (200 lines, ≤64KB/app head-truncated, ordered, redacted); consume-once drain
- tests: synthetic-window context capture, caps, redaction, budget order, consume-once, fetch-error skip

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PSK5g6qYLknKj8u3QAFEr6
This commit is contained in:
2026-07-10 15:43:29 +02:00
parent 332725a024
commit 544c42a618
12 changed files with 670 additions and 8 deletions
+43
View File
@@ -59,9 +59,52 @@ func buildAppTelemetrySection(stackMgr *stacks.Manager, metricsStore *metrics.Me
if result == nil {
result = []AppTelemetry{}
}
// 7. Enforce the per-report context budget (v0.111.0): drop context from the
// lowest-count issues first until under 16KB. Warns never carry context (scanner).
enforceContextBudget(result, contextBudgetBytes)
return result
}
// contextBudgetBytes is the HARD per-report total for all LogIssue.Context lines.
const contextBudgetBytes = 16 * 1024
// enforceContextBudget drops Context from the lowest-count issues (across all apps)
// until the summed context bytes fit the budget. Mutates apps in place.
func enforceContextBudget(apps []AppTelemetry, budget int) {
type ref struct {
app, issue, size, count int
}
var refs []ref
total := 0
for ai := range apps {
for ii := range apps[ai].Issues {
ctx := apps[ai].Issues[ii].Context
if len(ctx) == 0 {
continue
}
sz := 0
for _, l := range ctx {
sz += len(l)
}
refs = append(refs, ref{app: ai, issue: ii, size: sz, count: apps[ai].Issues[ii].Count})
total += sz
}
}
if total <= budget {
return
}
sort.Slice(refs, func(i, j int) bool { return refs[i].count < refs[j].count })
for _, r := range refs {
if total <= budget {
break
}
apps[r.app].Issues[r.issue].Context = nil
total -= r.size
}
}
// buildAppTelemetry aggregates container-level telemetry and log data into per-stack AppTelemetry entries.
func buildAppTelemetry(allStacks []stacks.Stack, telemetry []metrics.ContainerTelemetry, logs []metrics.ContainerLogSummary) []AppTelemetry {
// Build lookup maps