All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 3m24s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 4m55s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 8m31s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 7m19s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 5m20s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 3m45s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 7m42s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 6m59s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 8m41s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 4m54s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 7m23s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 10m48s
237 lines
4.9 KiB
Go
237 lines
4.9 KiB
Go
package tunnel
|
||
|
||
import (
|
||
"container/ring"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/gotunnel/pkg/protocol"
|
||
)
|
||
|
||
const (
|
||
maxBufferSize = 1000 // 环形缓冲区最大条目数
|
||
logFilePattern = "client.%s.log" // 日志文件名模式
|
||
)
|
||
|
||
// LogLevel 日志级别
|
||
type LogLevel int
|
||
|
||
const (
|
||
LevelDebug LogLevel = iota
|
||
LevelInfo
|
||
LevelWarn
|
||
LevelError
|
||
)
|
||
|
||
// Logger 客户端日志收集器
|
||
type Logger struct {
|
||
dataDir string
|
||
buffer *ring.Ring
|
||
bufferMu sync.RWMutex
|
||
file *os.File
|
||
fileMu sync.Mutex
|
||
fileDate string
|
||
subscribers map[string]chan protocol.LogEntry
|
||
subMu sync.RWMutex
|
||
}
|
||
|
||
// NewLogger 创建新的日志收集器
|
||
func NewLogger(dataDir string) (*Logger, error) {
|
||
l := &Logger{
|
||
dataDir: dataDir,
|
||
buffer: ring.New(maxBufferSize),
|
||
subscribers: make(map[string]chan protocol.LogEntry),
|
||
}
|
||
|
||
// 确保日志目录存在
|
||
logDir := filepath.Join(dataDir, "logs")
|
||
if err := os.MkdirAll(logDir, 0755); err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
return l, nil
|
||
}
|
||
|
||
// Printf 记录日志 (兼容 log.Printf)
|
||
func (l *Logger) Printf(format string, args ...interface{}) {
|
||
l.log(LevelInfo, "client", format, args...)
|
||
}
|
||
|
||
// Infof 记录信息日志
|
||
func (l *Logger) Infof(format string, args ...interface{}) {
|
||
l.log(LevelInfo, "client", format, args...)
|
||
}
|
||
|
||
// Warnf 记录警告日志
|
||
func (l *Logger) Warnf(format string, args ...interface{}) {
|
||
l.log(LevelWarn, "client", format, args...)
|
||
}
|
||
|
||
// Errorf 记录错误日志
|
||
func (l *Logger) Errorf(format string, args ...interface{}) {
|
||
l.log(LevelError, "client", format, args...)
|
||
}
|
||
|
||
// Debugf 记录调试日志
|
||
func (l *Logger) Debugf(format string, args ...interface{}) {
|
||
l.log(LevelDebug, "client", format, args...)
|
||
}
|
||
|
||
// PluginLog 记录插件日志
|
||
func (l *Logger) PluginLog(pluginName, level, format string, args ...interface{}) {
|
||
var lvl LogLevel
|
||
switch level {
|
||
case "debug":
|
||
lvl = LevelDebug
|
||
case "warn":
|
||
lvl = LevelWarn
|
||
case "error":
|
||
lvl = LevelError
|
||
default:
|
||
lvl = LevelInfo
|
||
}
|
||
l.log(lvl, "plugin:"+pluginName, format, args...)
|
||
}
|
||
|
||
func (l *Logger) log(level LogLevel, source, format string, args ...interface{}) {
|
||
entry := protocol.LogEntry{
|
||
Timestamp: time.Now().UnixMilli(),
|
||
Level: levelToString(level),
|
||
Message: fmt.Sprintf(format, args...),
|
||
Source: source,
|
||
}
|
||
|
||
// 注意:不在这里输出到标准输出,因为调用方(logf/logErrorf/logWarnf)已经调用了 log.Print
|
||
// 这里只负责:缓冲区存储、文件写入、订阅者通知
|
||
|
||
// 添加到环形缓冲区
|
||
l.bufferMu.Lock()
|
||
l.buffer.Value = entry
|
||
l.buffer = l.buffer.Next()
|
||
l.bufferMu.Unlock()
|
||
|
||
// 写入文件
|
||
l.writeToFile(entry)
|
||
|
||
// 通知订阅者
|
||
l.notifySubscribers(entry)
|
||
}
|
||
|
||
// Subscribe 订阅日志流
|
||
func (l *Logger) Subscribe(sessionID string) <-chan protocol.LogEntry {
|
||
ch := make(chan protocol.LogEntry, 100)
|
||
l.subMu.Lock()
|
||
l.subscribers[sessionID] = ch
|
||
l.subMu.Unlock()
|
||
return ch
|
||
}
|
||
|
||
// Unsubscribe 取消订阅
|
||
func (l *Logger) Unsubscribe(sessionID string) {
|
||
l.subMu.Lock()
|
||
if ch, ok := l.subscribers[sessionID]; ok {
|
||
close(ch)
|
||
delete(l.subscribers, sessionID)
|
||
}
|
||
l.subMu.Unlock()
|
||
}
|
||
|
||
// GetRecentLogs 获取最近的日志
|
||
func (l *Logger) GetRecentLogs(lines int, level string) []protocol.LogEntry {
|
||
l.bufferMu.RLock()
|
||
defer l.bufferMu.RUnlock()
|
||
|
||
var entries []protocol.LogEntry
|
||
l.buffer.Do(func(v interface{}) {
|
||
if v == nil {
|
||
return
|
||
}
|
||
entry := v.(protocol.LogEntry)
|
||
// 应用级别过滤
|
||
if level != "" && entry.Level != level {
|
||
return
|
||
}
|
||
entries = append(entries, entry)
|
||
})
|
||
|
||
// 如果指定了行数,返回最后 N 行
|
||
if lines > 0 && len(entries) > lines {
|
||
entries = entries[len(entries)-lines:]
|
||
}
|
||
|
||
return entries
|
||
}
|
||
|
||
// Close 关闭日志收集器
|
||
func (l *Logger) Close() {
|
||
l.fileMu.Lock()
|
||
if l.file != nil {
|
||
l.file.Close()
|
||
l.file = nil
|
||
}
|
||
l.fileMu.Unlock()
|
||
|
||
l.subMu.Lock()
|
||
for _, ch := range l.subscribers {
|
||
close(ch)
|
||
}
|
||
l.subscribers = make(map[string]chan protocol.LogEntry)
|
||
l.subMu.Unlock()
|
||
}
|
||
|
||
func (l *Logger) writeToFile(entry protocol.LogEntry) {
|
||
l.fileMu.Lock()
|
||
defer l.fileMu.Unlock()
|
||
|
||
today := time.Now().Format("2006-01-02")
|
||
if l.fileDate != today {
|
||
if l.file != nil {
|
||
l.file.Close()
|
||
}
|
||
|
||
logPath := filepath.Join(l.dataDir, "logs", fmt.Sprintf(logFilePattern, today))
|
||
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0644)
|
||
if err != nil {
|
||
return
|
||
}
|
||
l.file = f
|
||
l.fileDate = today
|
||
}
|
||
|
||
if l.file != nil {
|
||
fmt.Fprintf(l.file, "%d|%s|%s|%s\n",
|
||
entry.Timestamp, entry.Level, entry.Source, entry.Message)
|
||
}
|
||
}
|
||
|
||
func (l *Logger) notifySubscribers(entry protocol.LogEntry) {
|
||
l.subMu.RLock()
|
||
defer l.subMu.RUnlock()
|
||
|
||
for _, ch := range l.subscribers {
|
||
select {
|
||
case ch <- entry:
|
||
default:
|
||
// 订阅者太慢,丢弃日志
|
||
}
|
||
}
|
||
}
|
||
|
||
func levelToString(level LogLevel) string {
|
||
switch level {
|
||
case LevelDebug:
|
||
return "debug"
|
||
case LevelInfo:
|
||
return "info"
|
||
case LevelWarn:
|
||
return "warn"
|
||
case LevelError:
|
||
return "error"
|
||
default:
|
||
return "info"
|
||
}
|
||
}
|