package agent import ( "context" "log/slog" "strings" "sync" ) // agentLogLines is how much of its own log an agent keeps for the admin page. // Enough to cover an engine install or a failed start; the journal keeps the // rest for anyone who can reach it. const agentLogLines = 2000 // logRing keeps the most recent lines written to it. type logRing struct { mu sync.Mutex lines []string next int full bool } func newLogRing(n int) *logRing { return &logRing{lines: make([]string, n)} } // Write takes whole lines, as a text handler writes them. func (r *logRing) Write(p []byte) (int, error) { r.mu.Lock() defer r.mu.Unlock() for _, line := range strings.Split(strings.TrimRight(string(p), "\n"), "\n") { r.lines[r.next] = line r.next = (r.next + 1) % len(r.lines) if r.next == 0 { r.full = true } } return len(p), nil } // tail returns up to n of the most recent lines, oldest first. func (r *logRing) tail(n int) []string { r.mu.Lock() defer r.mu.Unlock() size := r.next if r.full { size = len(r.lines) } n = min(n, size) out := make([]string, 0, n) for i := size - n; i < size; i++ { idx := i if r.full { idx = (r.next + i) % len(r.lines) } out = append(out, r.lines[idx]) } return out } // teeHandler sends every record to two handlers: wherever the agent already // logs, and the ring the admin page reads. type teeHandler struct{ a, b slog.Handler } func (t teeHandler) Enabled(ctx context.Context, l slog.Level) bool { return t.a.Enabled(ctx, l) || t.b.Enabled(ctx, l) } func (t teeHandler) Handle(ctx context.Context, r slog.Record) error { var err error if t.a.Enabled(ctx, r.Level) { err = t.a.Handle(ctx, r.Clone()) } if t.b.Enabled(ctx, r.Level) { _ = t.b.Handle(ctx, r.Clone()) } return err } func (t teeHandler) WithAttrs(as []slog.Attr) slog.Handler { return teeHandler{t.a.WithAttrs(as), t.b.WithAttrs(as)} } func (t teeHandler) WithGroup(name string) slog.Handler { return teeHandler{t.a.WithGroup(name), t.b.WithGroup(name)} }