feat: publish standalone worker
Separate worker packaging and service lifecycle from the control plane.
Этот коммит содержится в:
99
internal/webapp/logbuffer.go
Обычный файл
99
internal/webapp/logbuffer.go
Обычный файл
@@ -0,0 +1,99 @@
|
||||
package webapp
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// LogBuffer is a small in-memory ring buffer the webapp uses to
|
||||
// expose the last N worker log lines on the Logs page. The worker
|
||||
// process feeds it via slog/JSON or by calling Append directly.
|
||||
//
|
||||
// Capacity is bounded; old entries are evicted FIFO.
|
||||
type LogBuffer struct {
|
||||
mu sync.RWMutex
|
||||
buf []string
|
||||
capN int
|
||||
offset int
|
||||
full bool
|
||||
}
|
||||
|
||||
// NewLogBuffer returns an empty LogBuffer that holds up to capN
|
||||
// lines. capN <= 0 falls back to a sensible default.
|
||||
func NewLogBuffer(capN int) *LogBuffer {
|
||||
if capN <= 0 {
|
||||
capN = 5000
|
||||
}
|
||||
return &LogBuffer{buf: make([]string, 0, capN), capN: capN}
|
||||
}
|
||||
|
||||
// Append adds a single line to the buffer. Newlines are stripped so
|
||||
// multi-line log records do not split into separate rows.
|
||||
func (l *LogBuffer) Append(line string) {
|
||||
if l == nil {
|
||||
return
|
||||
}
|
||||
l.mu.Lock()
|
||||
defer l.mu.Unlock()
|
||||
line = strings.TrimRight(line, "\n")
|
||||
if line == "" {
|
||||
return
|
||||
}
|
||||
if len(l.buf) < l.capN {
|
||||
l.buf = append(l.buf, line)
|
||||
return
|
||||
}
|
||||
l.full = true
|
||||
l.buf[l.offset] = line
|
||||
l.offset = (l.offset + 1) % l.capN
|
||||
}
|
||||
|
||||
// Tail returns the most recent n lines in chronological order. If n
|
||||
// is larger than the buffer capacity, only the held lines are
|
||||
// returned. n <= 0 returns an empty slice.
|
||||
func (l *LogBuffer) Tail(n int) []string {
|
||||
if l == nil || n <= 0 {
|
||||
return nil
|
||||
}
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
size := len(l.buf)
|
||||
if size == 0 {
|
||||
return nil
|
||||
}
|
||||
if n > size {
|
||||
n = size
|
||||
}
|
||||
out := make([]string, n)
|
||||
if !l.full {
|
||||
// Buffer not yet wrapped: just slice the tail.
|
||||
copy(out, l.buf[size-n:])
|
||||
return out
|
||||
}
|
||||
// Buffer is wrapped. The oldest line lives at l.offset; the
|
||||
// newest line lives at (offset - 1 + capN) % capN.
|
||||
idx := (l.offset - n + l.capN) % l.capN
|
||||
for i := 0; i < n; i++ {
|
||||
out[i] = l.buf[idx]
|
||||
idx = (idx + 1) % l.capN
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// Size reports the current number of stored lines.
|
||||
func (l *LogBuffer) Size() int {
|
||||
if l == nil {
|
||||
return 0
|
||||
}
|
||||
l.mu.RLock()
|
||||
defer l.mu.RUnlock()
|
||||
return len(l.buf)
|
||||
}
|
||||
|
||||
// Cap reports the maximum number of lines the buffer holds.
|
||||
func (l *LogBuffer) Cap() int {
|
||||
if l == nil {
|
||||
return 0
|
||||
}
|
||||
return l.capN
|
||||
}
|
||||
Ссылка в новой задаче
Block a user