183 lines
4.5 KiB
Go
183 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// newMemoryLogStore 创建内存日志缓冲,最多保留 max 条最新日志。
|
|
func newMemoryLogStore(max int) *memoryLogStore {
|
|
if max <= 0 {
|
|
max = 500
|
|
}
|
|
return &memoryLogStore{
|
|
max: max,
|
|
entries: make([]LogEntry, 0, max),
|
|
}
|
|
}
|
|
|
|
// initLogging 初始化日志输出:终端、日志文件、内存缓冲三处同时写入。
|
|
func initLogging(store *memoryLogStore, logDir string) func() {
|
|
if logDir == "" {
|
|
logDir = defaultLogDir
|
|
}
|
|
if err := os.MkdirAll(logDir, 0755); err != nil {
|
|
log.SetOutput(io.MultiWriter(os.Stdout, store))
|
|
log.Printf("日志目录创建失败,仅写入终端和内存: %v", err)
|
|
return func() {}
|
|
}
|
|
|
|
filePath := filepath.Join(logDir, "douyu_"+time.Now().In(cst).Format("20060102_150405")+".log")
|
|
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
|
if err != nil {
|
|
log.SetOutput(io.MultiWriter(os.Stdout, store))
|
|
log.Printf("日志文件创建失败,仅写入终端和内存: %v", err)
|
|
return func() {}
|
|
}
|
|
|
|
store.setFilePath(filePath)
|
|
log.SetOutput(io.MultiWriter(os.Stdout, file, store))
|
|
log.Printf("日志文件已启用: %s", filePath)
|
|
return func() {
|
|
_ = file.Close()
|
|
}
|
|
}
|
|
|
|
// Write 实现 io.Writer,把标准 log 输出转存为内存日志。
|
|
func (s *memoryLogStore) Write(p []byte) (int, error) {
|
|
text := strings.TrimRight(string(p), "\r\n")
|
|
if text == "" {
|
|
return len(p), nil
|
|
}
|
|
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
for _, line := range strings.Split(text, "\n") {
|
|
line = strings.TrimRight(line, "\r")
|
|
if strings.TrimSpace(line) == "" {
|
|
continue
|
|
}
|
|
category, level := parseLogMeta(line)
|
|
if level == "" {
|
|
level = detectLogLevel(line)
|
|
}
|
|
s.nextID++
|
|
s.entries = append(s.entries, LogEntry{
|
|
ID: s.nextID,
|
|
Time: time.Now().In(cst).Format("2006-01-02 15:04:05.000"),
|
|
Level: level,
|
|
Category: category,
|
|
Message: line,
|
|
})
|
|
if len(s.entries) > s.max {
|
|
copy(s.entries, s.entries[len(s.entries)-s.max:])
|
|
s.entries = s.entries[:s.max]
|
|
}
|
|
}
|
|
return len(p), nil
|
|
}
|
|
|
|
// logEvent 输出带分类和级别的结构化文本日志。
|
|
func logEvent(category, level, format string, args ...any) {
|
|
if category == "" {
|
|
category = "系统"
|
|
}
|
|
if level == "" {
|
|
level = "info"
|
|
}
|
|
log.Printf("[%s][%s] %s", category, level, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
// parseLogMeta 从标准日志行中提取类似 [网络][warn] 的分类和级别。
|
|
func parseLogMeta(line string) (string, string) {
|
|
idx := strings.Index(line, "[")
|
|
if idx < 0 {
|
|
return "", ""
|
|
}
|
|
category, rest, ok := cutBracket(line[idx:])
|
|
if !ok {
|
|
return "", ""
|
|
}
|
|
level, _, ok := cutBracket(rest)
|
|
if !ok || !isLogLevel(level) {
|
|
return category, ""
|
|
}
|
|
return category, level
|
|
}
|
|
|
|
func cutBracket(s string) (string, string, bool) {
|
|
if !strings.HasPrefix(s, "[") {
|
|
return "", s, false
|
|
}
|
|
end := strings.Index(s, "]")
|
|
if end <= 1 {
|
|
return "", s, false
|
|
}
|
|
return s[1:end], s[end+1:], true
|
|
}
|
|
|
|
func isLogLevel(level string) bool {
|
|
switch strings.ToLower(level) {
|
|
case "success", "error", "warn", "info":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
// setFilePath 保存当前日志文件路径,供前端展示或排查使用。
|
|
func (s *memoryLogStore) setFilePath(path string) {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
s.filePath = path
|
|
}
|
|
|
|
// Since 返回 ID 大于 since 的日志。
|
|
func (s *memoryLogStore) Since(since int64) []LogEntry {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
|
|
out := make([]LogEntry, 0, len(s.entries))
|
|
for _, entry := range s.entries {
|
|
if entry.ID > since {
|
|
out = append(out, entry)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// FilePath 返回当前日志文件路径。
|
|
func (s *memoryLogStore) FilePath() string {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
return s.filePath
|
|
}
|
|
|
|
// detectLogLevel 根据日志文本粗略分类,方便前端染色。
|
|
func detectLogLevel(line string) string {
|
|
lower := strings.ToLower(line)
|
|
switch {
|
|
case strings.Contains(lower, "[success]"):
|
|
return "success"
|
|
case strings.Contains(lower, "[error]"):
|
|
return "error"
|
|
case strings.Contains(lower, "[warn]"):
|
|
return "warn"
|
|
case strings.Contains(lower, "[info]"):
|
|
return "info"
|
|
case strings.Contains(line, "成功"):
|
|
return "success"
|
|
case strings.Contains(line, "失败") || strings.Contains(line, "错误") || strings.Contains(lower, "error") || strings.Contains(lower, "panic"):
|
|
return "error"
|
|
case strings.Contains(line, "降级") || strings.Contains(line, "跳过") || strings.Contains(line, "不存在") || strings.Contains(lower, "warn"):
|
|
return "warn"
|
|
default:
|
|
return "info"
|
|
}
|
|
}
|