Files
Flik c1f6e0bdcf
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
feat: refactor logger to avoid standard output in log method and update JSPlugin HTTP handler to use goja.Callable
2026-01-05 20:13:06 +08:00

237 lines
4.9 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 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"
}
}