Files
xk-ai-agent/internal/service/memlog.go
2026-08-14 21:50:48 +08:00

97 lines
3.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
// ========================================================================
// MemLog —— 内存日志环形缓冲(/agent/view 面板「实时日志」Tab 数据源)
// ========================================================================
// 设计:
// - main.go 启动时 log.SetOutput(io.MultiWriter(os.Stdout, service.MemLog))
// 控制台/systemd 日志完全不受影响,只是多复制一份到内存
// - 环形缓冲 500 行,每行带自增 ID + 时间戳,面板用 since_id 增量拉取
// (轮询只传新增行,不用每次全量传 500 行)
// - 为什么不用文件尾随tail -f 方案):容器/Windows 部署时日志文件路径
// 不确定,而 log 包的输出流是唯一稳定的挂接点
//
// 安全提示debug 开关ai_agent_debug_log打开时日志含请求体PHI
// 因此 /agent/logs 接口必须走全局 Auth绝不放行
// ========================================================================
import (
"strings"
"sync"
"time"
)
// MemLogEntry 一行日志
type MemLogEntry struct {
ID int64 `json:"id"` // 自增 ID增量拉取的游标
Time int64 `json:"time"` // 写入时间unix 秒)
Line string `json:"line"` // 日志原文(含 log 包自带的日期前缀)
}
// MemLogWriter 实现 io.Writer把日志行写入环形缓冲
//
// 并发说明log 包对每次 Print 调用内部加锁后才调 Write
// 但我们仍自己加锁——因为 List 的读取和 Write 是并发的
type MemLogWriter struct {
mu sync.RWMutex
items []MemLogEntry // 定长环形数组
size int // 容量
count int // 已写入条数(<= size
head int // 下一个写入位置
nextID int64 // 自增 ID
}
// MemLog 全局单例:保留最近 500 行日志
var MemLog = &MemLogWriter{
items: make([]MemLogEntry, 500),
size: 500,
}
// Write 实现 io.Writer
//
// log 包每次调用传入完整的一条日志(含结尾 \n
// 但保险起见仍按 \n 拆分(防止某些直接写 writer 的多行输出粘连)
func (w *MemLogWriter) Write(p []byte) (int, error) {
now := time.Now().Unix()
w.mu.Lock()
for _, line := range strings.Split(strings.TrimRight(string(p), "\n"), "\n") {
if line == "" {
continue
}
w.nextID++
w.items[w.head] = MemLogEntry{ID: w.nextID, Time: now, Line: line}
w.head = (w.head + 1) % w.size
if w.count < w.size {
w.count++
}
}
w.mu.Unlock()
return len(p), nil
}
// List 增量拉取日志
//
// 参数:
// - sinceID只返回 ID > sinceID 的行0 表示从最旧开始全量拉)
// - limit :返回条数上限(<=0 时取 200
//
// 返回按 ID 升序(时间正序),面板直接 append 到滚动区尾部
func (w *MemLogWriter) List(sinceID int64, limit int) []MemLogEntry {
if limit <= 0 {
limit = 200
}
w.mu.RLock()
defer w.mu.RUnlock()
out := make([]MemLogEntry, 0, min(limit, w.count))
// 从最旧的一条开始正序遍历head 指向"下一个写入位",最旧 = head-count
start := (w.head - w.count + w.size*2) % w.size
for i := 0; i < w.count && len(out) < limit; i++ {
idx := (start + i) % w.size
if w.items[idx].ID > sinceID {
out = append(out, w.items[idx])
}
}
return out
}