初步优化成全部go+wails
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"douyu_go/internal/constants"
|
||||
"douyu_go/internal/types"
|
||||
)
|
||||
|
||||
// MemoryLogStore 内存日志缓冲,实现 io.Writer 接口。
|
||||
type MemoryLogStore struct {
|
||||
mu sync.RWMutex
|
||||
nextID int64
|
||||
max int
|
||||
filePath string
|
||||
entries []types.LogEntry
|
||||
}
|
||||
|
||||
// NewMemoryLogStore 创建内存日志缓冲,最多保留 max 条最新日志。
|
||||
func NewMemoryLogStore(max int) *MemoryLogStore {
|
||||
if max <= 0 {
|
||||
max = 500
|
||||
}
|
||||
return &MemoryLogStore{
|
||||
max: max,
|
||||
entries: make([]types.LogEntry, 0, max),
|
||||
}
|
||||
}
|
||||
|
||||
// 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, types.LogEntry{
|
||||
ID: s.nextID,
|
||||
Time: time.Now().In(constants.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
|
||||
}
|
||||
|
||||
// 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) []types.LogEntry {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
out := make([]types.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
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"douyu_go/internal/constants"
|
||||
)
|
||||
|
||||
// AsyncLogWriter 异步日志写入器,队列满时丢弃日志以保护主链路。
|
||||
type AsyncLogWriter struct {
|
||||
dst io.Writer
|
||||
ch chan []byte
|
||||
wg sync.WaitGroup
|
||||
closed uint32
|
||||
dropped uint64
|
||||
}
|
||||
|
||||
// NewAsyncLogWriter 创建异步日志写入器。
|
||||
func NewAsyncLogWriter(dst io.Writer, buffer int) *AsyncLogWriter {
|
||||
if buffer <= 0 {
|
||||
buffer = 1024
|
||||
}
|
||||
w := &AsyncLogWriter{
|
||||
dst: dst,
|
||||
ch: make(chan []byte, buffer),
|
||||
}
|
||||
w.wg.Add(1)
|
||||
go w.run()
|
||||
return w
|
||||
}
|
||||
|
||||
// Write 异步写日志;队列满时丢弃,避免抢购请求被磁盘 I/O 阻塞。
|
||||
func (w *AsyncLogWriter) Write(p []byte) (int, error) {
|
||||
if w == nil {
|
||||
return len(p), nil
|
||||
}
|
||||
if atomic.LoadUint32(&w.closed) == 1 {
|
||||
return len(p), nil
|
||||
}
|
||||
buf := append([]byte(nil), p...)
|
||||
select {
|
||||
case w.ch <- buf:
|
||||
default:
|
||||
atomic.AddUint64(&w.dropped, 1)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *AsyncLogWriter) run() {
|
||||
defer w.wg.Done()
|
||||
for p := range w.ch {
|
||||
_, _ = w.dst.Write(p)
|
||||
}
|
||||
if dropped := atomic.LoadUint64(&w.dropped); dropped > 0 {
|
||||
_, _ = fmt.Fprintf(w.dst, "%s [日志][warn] 日志队列已丢弃 %d 条,已优先保护抢购链路\n",
|
||||
time.Now().In(constants.CST).Format("2006/01/02 15:04:05.000000"), dropped)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭写入器,等待队列清空。
|
||||
func (w *AsyncLogWriter) Close() error {
|
||||
if w == nil {
|
||||
return nil
|
||||
}
|
||||
if atomic.CompareAndSwapUint32(&w.closed, 0, 1) {
|
||||
close(w.ch)
|
||||
w.wg.Wait()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// InitLogging 初始化日志输出:终端、日志文件、内存缓冲三处同时写入。
|
||||
func InitLogging(store *MemoryLogStore, logDir string) func() {
|
||||
if logDir == "" {
|
||||
logDir = constants.DefaultLogDir
|
||||
}
|
||||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||||
async := NewAsyncLogWriter(io.MultiWriter(os.Stdout, store), 2048)
|
||||
log.SetOutput(async)
|
||||
log.Printf("日志目录创建失败,仅写入终端和内存: %v", err)
|
||||
return func() { _ = async.Close() }
|
||||
}
|
||||
|
||||
filePath := filepath.Join(logDir, "douyu_"+time.Now().In(constants.CST).Format("20060102_150405")+".log")
|
||||
file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
async := NewAsyncLogWriter(io.MultiWriter(os.Stdout, store), 2048)
|
||||
log.SetOutput(async)
|
||||
log.Printf("日志文件创建失败,仅写入终端和内存: %v", err)
|
||||
return func() { _ = async.Close() }
|
||||
}
|
||||
|
||||
store.SetFilePath(filePath)
|
||||
async := NewAsyncLogWriter(io.MultiWriter(os.Stdout, file, store), 2048)
|
||||
log.SetOutput(async)
|
||||
log.Printf("日志文件已启用: %s", filePath)
|
||||
return func() {
|
||||
_ = async.Close()
|
||||
_ = file.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// IsLogLevel 判断是否为有效日志级别。
|
||||
func IsLogLevel(level string) bool {
|
||||
switch strings.ToLower(level) {
|
||||
case "success", "error", "warn", "info":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// 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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user