feat(logging): implement client log streaming and management
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
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
- 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.
This commit is contained in:
@@ -47,6 +47,7 @@ type Client struct {
|
||||
runningPlugins map[string]plugin.ClientPlugin
|
||||
versionStore *PluginVersionStore
|
||||
pluginMu sync.RWMutex
|
||||
logger *Logger // 日志收集器
|
||||
}
|
||||
|
||||
// NewClient 创建客户端
|
||||
@@ -59,12 +60,19 @@ func NewClient(serverAddr, token, id string) *Client {
|
||||
home, _ := os.UserHomeDir()
|
||||
dataDir := filepath.Join(home, ".gotunnel")
|
||||
|
||||
// 初始化日志收集器
|
||||
logger, err := NewLogger(dataDir)
|
||||
if err != nil {
|
||||
log.Printf("[Client] Failed to initialize logger: %v", err)
|
||||
}
|
||||
|
||||
return &Client{
|
||||
ServerAddr: serverAddr,
|
||||
Token: token,
|
||||
ID: id,
|
||||
DataDir: dataDir,
|
||||
runningPlugins: make(map[string]plugin.ClientPlugin),
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,6 +244,10 @@ func (c *Client) handleStream(stream net.Conn) {
|
||||
c.handlePluginConfigUpdate(stream, msg)
|
||||
case protocol.MsgTypeUpdateDownload:
|
||||
c.handleUpdateDownload(stream, msg)
|
||||
case protocol.MsgTypeLogRequest:
|
||||
go c.handleLogRequest(stream, msg)
|
||||
case protocol.MsgTypeLogStop:
|
||||
c.handleLogStop(stream, msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -900,3 +912,79 @@ func restartClientProcess(path, serverAddr, token, id string) {
|
||||
cmd.Start()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
// handleLogRequest 处理日志请求
|
||||
func (c *Client) handleLogRequest(stream net.Conn, msg *protocol.Message) {
|
||||
if c.logger == nil {
|
||||
stream.Close()
|
||||
return
|
||||
}
|
||||
|
||||
var req protocol.LogRequest
|
||||
if err := msg.ParsePayload(&req); err != nil {
|
||||
stream.Close()
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Printf("Log request received: session=%s, follow=%v", req.SessionID, req.Follow)
|
||||
|
||||
// 发送历史日志
|
||||
entries := c.logger.GetRecentLogs(req.Lines, req.Level)
|
||||
if len(entries) > 0 {
|
||||
data := protocol.LogData{
|
||||
SessionID: req.SessionID,
|
||||
Entries: entries,
|
||||
EOF: !req.Follow,
|
||||
}
|
||||
respMsg, _ := protocol.NewMessage(protocol.MsgTypeLogData, data)
|
||||
if err := protocol.WriteMessage(stream, respMsg); err != nil {
|
||||
stream.Close()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不需要持续推送,关闭流
|
||||
if !req.Follow {
|
||||
stream.Close()
|
||||
return
|
||||
}
|
||||
|
||||
// 订阅新日志
|
||||
ch := c.logger.Subscribe(req.SessionID)
|
||||
defer c.logger.Unsubscribe(req.SessionID)
|
||||
defer stream.Close()
|
||||
|
||||
// 持续推送新日志
|
||||
for entry := range ch {
|
||||
// 应用级别过滤
|
||||
if req.Level != "" && entry.Level != req.Level {
|
||||
continue
|
||||
}
|
||||
|
||||
data := protocol.LogData{
|
||||
SessionID: req.SessionID,
|
||||
Entries: []protocol.LogEntry{entry},
|
||||
EOF: false,
|
||||
}
|
||||
respMsg, _ := protocol.NewMessage(protocol.MsgTypeLogData, data)
|
||||
if err := protocol.WriteMessage(stream, respMsg); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleLogStop 处理停止日志流请求
|
||||
func (c *Client) handleLogStop(stream net.Conn, msg *protocol.Message) {
|
||||
defer stream.Close()
|
||||
|
||||
if c.logger == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var req protocol.LogStopRequest
|
||||
if err := msg.ParsePayload(&req); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
c.logger.Unsubscribe(req.SessionID)
|
||||
}
|
||||
|
||||
240
internal/client/tunnel/logger.go
Normal file
240
internal/client/tunnel/logger.go
Normal file
@@ -0,0 +1,240 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user