Files
EvoBGP/internal/runtimelogs/tail.go
T
DenozordecandCursor 3f0dd6c234 docs(runtime-logs): implement runtime log management features
Добавлены новые возможности для работы с файловыми логами Docker-сервисов:
- Эндпоинты для получения списка логов и хвоста лог-файла.
- Очистка лог-файлов с возможностью выбора режима (truncate или delete) и запись в аудит очистки.
- Обновлена документация и конфигурация для поддержки новых функций.

Co-authored-by: Cursor <[email protected]>
2026-06-12 19:04:53 +07:00

137 lines
2.7 KiB
Go

package runtimelogs
import (
"bufio"
"bytes"
"io"
"os"
"strings"
"evobgp/internal/store"
)
// TailOptions controls tail/preview reads.
type TailOptions struct {
Lines int
Bytes int
Grep string
}
// Tail reads the end of a runtime log file.
func (s *Service) Tail(filename string, opts TailOptions) (*store.RuntimeLogTail, error) {
if !s.Available() {
return nil, ErrUnavailable
}
path, err := ResolveLogPath(s.cfg.RootDir, filename)
if err != nil {
return nil, err
}
st, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
return nil, ErrNotFound
}
return nil, err
}
if st.IsDir() {
return nil, ErrNotAFile
}
lines := opts.Lines
if lines <= 0 {
lines = DefaultTailLines
}
if lines > MaxTailLines {
lines = MaxTailLines
}
maxRead := MaxTailBytes
if opts.Bytes > 0 && opts.Bytes < maxRead {
maxRead = opts.Bytes
}
raw, truncated, err := readTailBytes(path, int64(maxRead))
if err != nil {
return nil, err
}
grep := strings.TrimSpace(opts.Grep)
if len(grep) > MaxGrepLen {
grep = grep[:MaxGrepLen]
}
contentLines := splitLines(raw)
if grep != "" {
filtered := contentLines[:0]
for _, line := range contentLines {
if strings.Contains(line, grep) {
filtered = append(filtered, line)
}
}
contentLines = filtered
}
if len(contentLines) > lines {
contentLines = contentLines[len(contentLines)-lines:]
truncated = true
}
return &store.RuntimeLogTail{
Filename: filename,
Content: strings.Join(contentLines, "\n"),
Truncated: truncated,
LinesReturned: len(contentLines),
}, nil
}
func readTailBytes(path string, maxRead int64) ([]byte, bool, error) {
f, err := os.Open(path)
if err != nil {
return nil, false, err
}
defer func() { _ = f.Close() }()
st, err := f.Stat()
if err != nil {
return nil, false, err
}
size := st.Size()
truncated := size > maxRead
start := int64(0)
if size > maxRead {
start = size - maxRead
}
if _, err := f.Seek(start, io.SeekStart); err != nil {
return nil, false, err
}
buf := make([]byte, size-start)
n, err := io.ReadFull(f, buf)
if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
return nil, false, err
}
buf = buf[:n]
if start > 0 {
// Drop partial first line when reading from middle of file.
if idx := bytes.IndexByte(buf, '\n'); idx >= 0 && idx+1 < len(buf) {
buf = buf[idx+1:]
truncated = true
} else if start > 0 {
truncated = true
}
}
return buf, truncated, nil
}
func splitLines(b []byte) []string {
if len(b) == 0 {
return nil
}
sc := bufio.NewScanner(bytes.NewReader(b))
var lines []string
for sc.Scan() {
lines = append(lines, sc.Text())
}
if len(lines) == 0 {
return []string{string(b)}
}
return lines
}