Smail/backend/services/runtimelog/query.go
smail bb4ac4d5f5
Some checks are pending
Security Checks / security (push) Waiting to run
过滤无价值日志并实现最近24小时倒序分页查询
2026-09-09 01:06:45 +08:00

106 lines
2.6 KiB
Go

package runtimelog
import (
"context"
"errors"
"sort"
"strings"
"time"
"git.pcux.cn/xuans/Smail/utils/logpolicy"
)
type File struct {
Name string `json:"name"`
Bytes int64 `json:"bytes"`
Compressed bool `json:"compressed"`
}
type Page struct {
Lines []string `json:"lines"`
HasMore bool `json:"hasMore"`
}
func (w *Writer) Files() ([]File, error) {
w.archiveMu.Lock()
defer w.archiveMu.Unlock()
return w.files()
}
func (w *Writer) files() ([]File, error) {
if w.closed {
return nil, ErrInvalid
}
directory, err := w.root.Open(".")
if err != nil {
return nil, err
}
defer directory.Close()
entries, err := directory.ReadDir(-1)
if err != nil {
return nil, err
}
files := make([]File, 0)
for _, entry := range entries {
day, ok := fileDay(entry.Name())
if !ok || !day.Add(24*time.Hour).After(logpolicy.Cutoff(w.now())) {
continue
}
if err := w.regular(entry.Name(), false); err != nil {
return nil, err
}
// Windows 目录枚举可能缓存仍打开文件的旧长度,必须通过文件句柄读取。
handle, err := w.root.Open(entry.Name())
if err != nil {
return nil, err
}
info, statErr := handle.Stat()
if err := errors.Join(statErr, handle.Close()); err != nil {
return nil, err
}
files = append(files, File{Name: entry.Name(), Bytes: info.Size(), Compressed: strings.HasSuffix(entry.Name(), ".gz")})
}
sort.Slice(files, func(i, j int) bool { return files[i].Name > files[j].Name })
return files, nil
}
// Read 仅允许固定名称的受管日志,不接受任意路径;压缩内容在服务端流式读取。
func (w *Writer) Read(ctx context.Context, name string, offset, limit int) (Page, error) {
w.archiveMu.Lock()
defer w.archiveMu.Unlock()
if _, ok := fileDay(name); !ok || offset < 0 || offset > 1000000 || limit < 1 || limit > 500 || w.closed {
return Page{}, ErrInvalid
}
page := Page{Lines: make([]string, 0, limit)}
cutoff := logpolicy.Cutoff(w.now())
position := 0
const maxResponseBytes = 2 * 1024 * 1024
bytes := 0
finished := errors.New("当前页读取完成")
err := w.scan(ctx, name, func(line []byte, at time.Time) error {
if at.Before(cutoff) {
return nil
}
// 先过滤再分页,偏移、统计和下一页判断均按实际可见记录计算。
visible, keep := presentLine(string(line))
if !keep {
return nil
}
position++
if position <= offset {
return nil
}
if len(page.Lines) >= limit || bytes+len(visible) > maxResponseBytes {
page.HasMore = true
return finished
}
page.Lines = append(page.Lines, visible)
bytes += len(visible)
return nil
})
if errors.Is(err, finished) {
err = nil
}
return page, err
}