Files
GoTunnel/internal/client/tunnel/logger.go
Flik 2f98e1ac7d
All checks were successful
Build Multi-Platform Binaries / build-frontend (push) Successful in 30s
Build Multi-Platform Binaries / build-binaries (amd64, darwin, server, false) (push) Successful in 1m16s
Build Multi-Platform Binaries / build-binaries (amd64, linux, client, true) (push) Successful in 1m4s
Build Multi-Platform Binaries / build-binaries (amd64, linux, server, true) (push) Successful in 2m29s
Build Multi-Platform Binaries / build-binaries (amd64, windows, client, true) (push) Successful in 54s
Build Multi-Platform Binaries / build-binaries (amd64, windows, server, true) (push) Successful in 2m35s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, client, true) (push) Successful in 55s
Build Multi-Platform Binaries / build-binaries (arm, 7, linux, server, true) (push) Successful in 2m21s
Build Multi-Platform Binaries / build-binaries (arm64, darwin, server, false) (push) Successful in 1m35s
Build Multi-Platform Binaries / build-binaries (arm64, linux, client, true) (push) Successful in 1m1s
Build Multi-Platform Binaries / build-binaries (arm64, linux, server, true) (push) Successful in 1m55s
Build Multi-Platform Binaries / build-binaries (arm64, windows, server, false) (push) Successful in 1m39s
feat(logging): implement client log streaming and management
- Added log streaming functionality for clients, allowing real-time log access via SSE.
- Introduced LogHandler to manage log streaming requests and responses.
- Implemented LogSessionManager to handle active log sessions and listeners.
- Enhanced protocol with log-related message types and structures.
- Created Logger for client-side logging, supporting various log levels and file output.
- Developed LogViewer component for the web interface to display and filter logs.
- Updated API to support log stream creation and management.
- Added support for querying logs by level and searching through log messages.
2026-01-03 16:19:52 +08:00

241 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,
}
// 输出到标准输出
fmt.Printf("%s [%s] [%s] %s\n",
time.Now().Format("2006-01-02 15:04:05"),
entry.Level,
entry.Source,
entry.Message)
// 添加到环形缓冲区
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"
}
}