You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

204 lines
5.4 KiB

package api
import (
"yunyan/comm"
"yunyan/pb"
"io"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
// 运行日志根目录(相对于 home 服务的工作目录 bin/)
const runtimeLogDir = "./log"
// 服务名白名单校验:仅允许字母数字与下划线
var runtimeLogServiceRe = regexp.MustCompile(`^[A-Za-z0-9_]+$`)
// 列出可查询的运行日志服务
func (this *apiComp) ListRuntimeLogs(session comm.IUserSession, req *pb.ApiListRuntimeLogsReq) (resp *pb.ApiListRuntimeLogsResp, errdata *pb.ErrorData) {
if session.GetMateToString("identity") != "1" {
errdata = &pb.ErrorData{Code: pb.ErrorCode_InsufficientPermissions, Message: "仅超管可访问"}
return
}
resp = &pb.ApiListRuntimeLogsResp{Services: []string{}}
entries, err := os.ReadDir(runtimeLogDir)
if err != nil {
if os.IsNotExist(err) {
return
}
errdata = &pb.ErrorData{Code: pb.ErrorCode_SystemError, Message: err.Error()}
return
}
for _, e := range entries {
if e.IsDir() {
continue
}
name := e.Name()
if !strings.HasSuffix(name, ".log") {
continue
}
base := strings.TrimSuffix(name, ".log")
if !runtimeLogServiceRe.MatchString(base) {
continue
}
resp.Services = append(resp.Services, base)
}
sort.Strings(resp.Services)
return
}
// 查询某服务运行日志(从尾部倒序读取,支持 load more)
func (this *apiComp) GetRuntimeLog(session comm.IUserSession, req *pb.ApiGetRuntimeLogReq) (resp *pb.ApiGetRuntimeLogResp, errdata *pb.ErrorData) {
if session.GetMateToString("identity") != "1" {
errdata = &pb.ErrorData{Code: pb.ErrorCode_InsufficientPermissions, Message: "仅超管可访问"}
return
}
if !runtimeLogServiceRe.MatchString(req.Service) {
errdata = &pb.ErrorData{Code: pb.ErrorCode_ReqParameterError, Message: "非法服务名"}
return
}
limit := int(req.Limit)
if limit <= 0 {
limit = 500
}
if limit > 2000 {
limit = 2000
}
// 路径穿越防护
path := filepath.Join(runtimeLogDir, req.Service+".log")
absLog, _ := filepath.Abs(runtimeLogDir)
absFile, _ := filepath.Abs(path)
if !strings.HasPrefix(absFile, absLog) {
errdata = &pb.ErrorData{Code: pb.ErrorCode_ReqParameterError, Message: "非法路径"}
return
}
f, err := os.Open(path)
if err != nil {
if os.IsNotExist(err) {
resp = &pb.ApiGetRuntimeLogResp{Lines: []string{}}
return
}
errdata = &pb.ErrorData{Code: pb.ErrorCode_SystemError, Message: err.Error()}
return
}
defer f.Close()
info, err := f.Stat()
if err != nil {
errdata = &pb.ErrorData{Code: pb.ErrorCode_SystemError, Message: err.Error()}
return
}
fileSize := info.Size()
end := req.Before
if end <= 0 || end > fileSize {
end = fileSize
}
lines, earliest, err := readTailLines(f, end, limit, req.Keyword)
if err != nil && err != io.EOF {
errdata = &pb.ErrorData{Code: pb.ErrorCode_SystemError, Message: err.Error()}
return
}
resp = &pb.ApiGetRuntimeLogResp{
Lines: lines,
Earliest: earliest,
FileSize: fileSize,
HasMore: earliest > 0,
}
return
}
// readTailLines 从 end 位置向前读取最多 limit 行(命中 keyword 过滤),返回旧→新顺序
// earliest = 所返回内容在文件中的起始字节位置
func readTailLines(f *os.File, end int64, limit int, keyword string) (lines []string, earliest int64, err error) {
const chunkSize = 64 * 1024
if end <= 0 {
return []string{}, 0, nil
}
var tail []byte // 未处理的尾部缓冲
pos := end
collected := make([]string, 0, limit)
reachedStart := false
for len(collected) < limit {
if pos <= 0 {
reachedStart = true
break
}
readSize := int64(chunkSize)
if pos < readSize {
readSize = pos
}
offset := pos - readSize
buf := make([]byte, readSize)
if _, rerr := f.ReadAt(buf, offset); rerr != nil && rerr != io.EOF {
return nil, 0, rerr
}
// 把本次 chunk 拼到尾部缓冲前面
combined := append(buf, tail...)
pos = offset
// 从后往前按 \n 切。最前面一段可能是跨界的半行,留下来
idx := len(combined)
for i := len(combined) - 1; i >= 0; i-- {
if combined[i] == '\n' {
line := string(combined[i+1 : idx])
idx = i
if line == "" {
continue
}
if keyword != "" && !strings.Contains(line, keyword) {
continue
}
collected = append(collected, line)
if len(collected) >= limit {
break
}
}
}
// idx 之前是半行(还未遇到 \n),保留到下一轮
tail = combined[:idx]
if pos == 0 {
// 已读到文件头,tail 就是最开始那一行
if len(collected) < limit && len(tail) > 0 {
line := string(tail)
if keyword == "" || strings.Contains(line, keyword) {
collected = append(collected, line)
}
tail = nil
}
reachedStart = true
break
}
}
// collected 是从新到旧累积的,反转成 旧→新
lines = make([]string, 0, len(collected))
for i := len(collected) - 1; i >= 0; i-- {
lines = append(lines, collected[i])
}
if reachedStart {
earliest = 0
} else {
// 下一次 before 应该指向 "本批最早一行的起始字节"。
// pos 指向下一个未读 chunk 的起点;tail 是跨界半行,最早行就在 pos + 0 处之后。
// 简化:直接取 pos(指向未处理 chunk 的起点),因为 tail 是半行属于上一 chunk,
// 而最早返回的整行恰好是上一个 \n 之后。为了下次能完整读到该行,返回 pos 即可。
earliest = pos + int64(len(tail))
if earliest < 0 {
earliest = 0
}
}
return
}