docs(runtime-logs): implement runtime log management features
Добавлены новые возможности для работы с файловыми логами Docker-сервисов: - Эндпоинты для получения списка логов и хвоста лог-файла. - Очистка лог-файлов с возможностью выбора режима (truncate или delete) и запись в аудит очистки. - Обновлена документация и конфигурация для поддержки новых функций. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user