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

- 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:
2026-01-03 16:19:52 +08:00
parent d2ca3fa2b9
commit 2f98e1ac7d
15 changed files with 1311 additions and 12 deletions

View File

@@ -0,0 +1,155 @@
package tunnel
import (
"net"
"sync"
"github.com/gotunnel/pkg/protocol"
)
// LogSessionManager 管理所有活跃的日志会话
type LogSessionManager struct {
sessions map[string]*LogSession
mu sync.RWMutex
}
// LogSession 日志流会话
type LogSession struct {
ID string
ClientID string
Stream net.Conn
listeners []chan protocol.LogEntry
mu sync.Mutex
closed bool
}
// NewLogSessionManager 创建日志会话管理器
func NewLogSessionManager() *LogSessionManager {
return &LogSessionManager{
sessions: make(map[string]*LogSession),
}
}
// CreateSession 创建日志会话
func (m *LogSessionManager) CreateSession(clientID, sessionID string, stream net.Conn) *LogSession {
session := &LogSession{
ID: sessionID,
ClientID: clientID,
Stream: stream,
listeners: make([]chan protocol.LogEntry, 0),
}
m.mu.Lock()
m.sessions[sessionID] = session
m.mu.Unlock()
return session
}
// GetSession 获取会话
func (m *LogSessionManager) GetSession(sessionID string) *LogSession {
m.mu.RLock()
defer m.mu.RUnlock()
return m.sessions[sessionID]
}
// RemoveSession 移除会话
func (m *LogSessionManager) RemoveSession(sessionID string) {
m.mu.Lock()
if session, ok := m.sessions[sessionID]; ok {
session.Close()
delete(m.sessions, sessionID)
}
m.mu.Unlock()
}
// GetSessionsByClient 获取客户端的所有会话
func (m *LogSessionManager) GetSessionsByClient(clientID string) []*LogSession {
m.mu.RLock()
defer m.mu.RUnlock()
var sessions []*LogSession
for _, session := range m.sessions {
if session.ClientID == clientID {
sessions = append(sessions, session)
}
}
return sessions
}
// CleanupClientSessions 清理客户端的所有会话
func (m *LogSessionManager) CleanupClientSessions(clientID string) {
m.mu.Lock()
defer m.mu.Unlock()
for id, session := range m.sessions {
if session.ClientID == clientID {
session.Close()
delete(m.sessions, id)
}
}
}
// AddListener 添加监听器
func (s *LogSession) AddListener() <-chan protocol.LogEntry {
ch := make(chan protocol.LogEntry, 100)
s.mu.Lock()
s.listeners = append(s.listeners, ch)
s.mu.Unlock()
return ch
}
// RemoveListener 移除监听器
func (s *LogSession) RemoveListener(ch <-chan protocol.LogEntry) {
s.mu.Lock()
defer s.mu.Unlock()
for i, listener := range s.listeners {
if listener == ch {
close(listener)
s.listeners = append(s.listeners[:i], s.listeners[i+1:]...)
break
}
}
}
// Broadcast 广播日志条目到所有监听器
func (s *LogSession) Broadcast(entry protocol.LogEntry) {
s.mu.Lock()
defer s.mu.Unlock()
for _, ch := range s.listeners {
select {
case ch <- entry:
default:
// 监听器太慢,丢弃日志
}
}
}
// Close 关闭会话
func (s *LogSession) Close() {
s.mu.Lock()
defer s.mu.Unlock()
if s.closed {
return
}
s.closed = true
for _, ch := range s.listeners {
close(ch)
}
s.listeners = nil
if s.Stream != nil {
s.Stream.Close()
}
}
// IsClosed 检查会话是否已关闭
func (s *LogSession) IsClosed() bool {
s.mu.Lock()
defer s.mu.Unlock()
return s.closed
}

View File

@@ -65,6 +65,7 @@ type Server struct {
listener net.Listener // 主监听器
shutdown chan struct{} // 关闭信号
wg sync.WaitGroup // 等待所有连接关闭
logSessions *LogSessionManager // 日志会话管理器
}
// JSPluginEntry JS 插件条目
@@ -102,6 +103,7 @@ func NewServer(cs db.ClientStore, bindAddr string, bindPort int, token string, h
clients: make(map[string]*ClientSession),
connSem: make(chan struct{}, maxConnections),
shutdown: make(chan struct{}),
logSessions: NewLogSessionManager(),
}
}
@@ -1474,3 +1476,96 @@ func (s *Server) SendUpdateToClient(clientID, downloadURL string) error {
log.Printf("[Server] Update command sent to client %s: %s", clientID, downloadURL)
return nil
}
// StartClientLogStream 启动客户端日志流
func (s *Server) StartClientLogStream(clientID, sessionID string, lines int, follow bool, level string) (<-chan protocol.LogEntry, error) {
s.mu.RLock()
cs, ok := s.clients[clientID]
s.mu.RUnlock()
if !ok {
return nil, fmt.Errorf("client %s not found or not online", clientID)
}
// 打开到客户端的流
stream, err := cs.Session.Open()
if err != nil {
return nil, err
}
// 发送日志请求
req := protocol.LogRequest{
SessionID: sessionID,
Lines: lines,
Follow: follow,
Level: level,
}
msg, _ := protocol.NewMessage(protocol.MsgTypeLogRequest, req)
if err := protocol.WriteMessage(stream, msg); err != nil {
stream.Close()
return nil, err
}
// 创建会话
session := s.logSessions.CreateSession(clientID, sessionID, stream)
listener := session.AddListener()
// 启动 goroutine 读取客户端日志
go s.readClientLogs(session, stream)
return listener, nil
}
// readClientLogs 读取客户端日志并广播到监听器
func (s *Server) readClientLogs(session *LogSession, stream net.Conn) {
defer s.logSessions.RemoveSession(session.ID)
for {
msg, err := protocol.ReadMessage(stream)
if err != nil {
return
}
if msg.Type != protocol.MsgTypeLogData {
continue
}
var data protocol.LogData
if err := msg.ParsePayload(&data); err != nil {
continue
}
for _, entry := range data.Entries {
session.Broadcast(entry)
}
if data.EOF {
return
}
}
}
// StopClientLogStream 停止客户端日志流
func (s *Server) StopClientLogStream(sessionID string) {
session := s.logSessions.GetSession(sessionID)
if session == nil {
return
}
// 发送停止请求到客户端
s.mu.RLock()
cs, ok := s.clients[session.ClientID]
s.mu.RUnlock()
if ok {
stream, err := cs.Session.Open()
if err == nil {
req := protocol.LogStopRequest{SessionID: sessionID}
msg, _ := protocol.NewMessage(protocol.MsgTypeLogStop, req)
protocol.WriteMessage(stream, msg)
stream.Close()
}
}
s.logSessions.RemoveSession(sessionID)
}