feat: add debug log stream support
This commit is contained in:
@@ -37,11 +37,17 @@ func NewWK(username, password, host string, cookies []*http.Cookie) *WK {
|
||||
return nil
|
||||
}
|
||||
|
||||
req := request.NewClient(&request.Config{
|
||||
reqCfg := &request.Config{
|
||||
UserAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36 Edg/140.0.0.0",
|
||||
// Proxy: "http://127.0.0.1:9000",
|
||||
// VerifySSL: false,
|
||||
})
|
||||
VerifySSL: true,
|
||||
Debug: conf.IsDebugMode(),
|
||||
}
|
||||
if conf.IsDebugMode() {
|
||||
reqCfg.Proxy = conf.DebugProxy
|
||||
reqCfg.VerifySSL = !conf.DebugSkipSSLVerify
|
||||
}
|
||||
|
||||
req := request.NewClient(reqCfg)
|
||||
if len(cookies) > 0 {
|
||||
req.SetCookies(cookies)
|
||||
}
|
||||
|
||||
@@ -37,8 +37,14 @@ func (m *SessionManager) Store(wk *WK) string {
|
||||
userKey := wk.Host + ":" + wk.Username
|
||||
if oldID, exists := m.userToSession[userKey]; exists {
|
||||
item := m.sessions[oldID]
|
||||
if item.cancel != nil {
|
||||
item.cancel()
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
item.LastValue = time.Now()
|
||||
item.Instance = wk
|
||||
item.cancel = cancel
|
||||
m.sessions[oldID] = item
|
||||
|
||||
log.Info("用户已存在,复用旧 Session",
|
||||
@@ -46,6 +52,8 @@ func (m *SessionManager) Store(wk *WK) string {
|
||||
zap.String("user", userKey),
|
||||
)
|
||||
|
||||
go m.KeepAlive(ctx, oldID)
|
||||
|
||||
return oldID
|
||||
}
|
||||
|
||||
@@ -61,18 +69,20 @@ func (m *SessionManager) Store(wk *WK) string {
|
||||
|
||||
log.Info("创建新 Session", zap.String("id", sessionID))
|
||||
|
||||
go m.KeepAlive(ctx, sessionID, wk)
|
||||
go m.KeepAlive(ctx, sessionID)
|
||||
|
||||
return sessionID
|
||||
}
|
||||
|
||||
// Get: 获取指定 session id 的 wk
|
||||
func (m *SessionManager) Get(sessionID string) (*WK, bool) {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
item, ok := m.sessions[sessionID]
|
||||
if ok {
|
||||
item.LastValue = time.Now()
|
||||
m.sessions[sessionID] = item
|
||||
return item.Instance, true
|
||||
}
|
||||
return nil, false
|
||||
@@ -96,7 +106,7 @@ func (m *SessionManager) Del(sessionID string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *SessionManager) KeepAlive(ctx context.Context, id string, wk *WK) {
|
||||
func (m *SessionManager) KeepAlive(ctx context.Context, id string) {
|
||||
ticker := time.NewTicker(2 * time.Minute)
|
||||
defer ticker.Stop()
|
||||
|
||||
@@ -108,7 +118,15 @@ func (m *SessionManager) KeepAlive(ctx context.Context, id string, wk *WK) {
|
||||
log.Info("KeepAlive 已停止", zap.String("id", id))
|
||||
return
|
||||
case <-ticker.C:
|
||||
_, err := wk.Online()
|
||||
m.mu.RLock()
|
||||
item, ok := m.sessions[id]
|
||||
m.mu.RUnlock()
|
||||
if !ok || item.Instance == nil {
|
||||
log.Info("Session 已不存在,停止 KeepAlive", zap.String("id", id))
|
||||
return
|
||||
}
|
||||
|
||||
_, err := item.Instance.Online()
|
||||
if err != nil {
|
||||
log.Error("自动保活请求失败", zap.Error(err))
|
||||
}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
package conf
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 构建信息
|
||||
var (
|
||||
Mode string = "debug"
|
||||
@@ -9,4 +14,32 @@ var (
|
||||
GitAuthor string = "unknown"
|
||||
GitEmail string = "unknown"
|
||||
GitCommit string = "unknown"
|
||||
|
||||
DebugProxy string = ""
|
||||
DebugSkipSSLVerify bool = false
|
||||
)
|
||||
|
||||
func init() {
|
||||
if !IsDebugMode() {
|
||||
return
|
||||
}
|
||||
|
||||
if proxy := os.Getenv("CKWK_DEBUG_PROXY"); proxy != "" {
|
||||
DebugProxy = proxy
|
||||
}
|
||||
DebugSkipSSLVerify = parseEnvBool("CKWK_DEBUG_SKIP_SSL_VERIFY")
|
||||
}
|
||||
|
||||
func IsDebugMode() bool {
|
||||
return !strings.EqualFold(Mode, "release")
|
||||
}
|
||||
|
||||
func parseEnvBool(key string) bool {
|
||||
value := strings.TrimSpace(os.Getenv(key))
|
||||
switch strings.ToLower(value) {
|
||||
case "1", "true", "yes", "on":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
65
internal/handler/debug_log.go
Normal file
65
internal/handler/debug_log.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"ckwk/pkg/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
var debugLogUpgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
func DebugLogWS(ctx *gin.Context) {
|
||||
conn, err := debugLogUpgrader.Upgrade(ctx.Writer, ctx.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
subID, ch := log.Subscribe()
|
||||
defer log.Unsubscribe(subID)
|
||||
|
||||
for _, entry := range log.Entries() {
|
||||
if err := conn.WriteJSON(entry); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
for {
|
||||
if _, _, err := conn.ReadMessage(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-done:
|
||||
return
|
||||
case entry, ok := <-ch:
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if err := conn.WriteJSON(entry); err != nil {
|
||||
return
|
||||
}
|
||||
case <-ticker.C:
|
||||
if err := conn.WriteMessage(websocket.PingMessage, []byte("ping")); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
89
internal/middleware/debug_http_log.go
Normal file
89
internal/middleware/debug_http_log.go
Normal file
@@ -0,0 +1,89 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ckwk/pkg/log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
const maxDebugBodySize = 4 * 1024
|
||||
|
||||
type debugBodyWriter struct {
|
||||
gin.ResponseWriter
|
||||
body bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *debugBodyWriter) Write(data []byte) (int, error) {
|
||||
w.body.Write(data)
|
||||
return w.ResponseWriter.Write(data)
|
||||
}
|
||||
|
||||
func (w *debugBodyWriter) WriteString(data string) (int, error) {
|
||||
w.body.WriteString(data)
|
||||
return w.ResponseWriter.WriteString(data)
|
||||
}
|
||||
|
||||
func DebugHTTPLog() gin.HandlerFunc {
|
||||
return func(ctx *gin.Context) {
|
||||
if isDebugLogRoute(ctx.Request.URL.Path) {
|
||||
ctx.Next()
|
||||
return
|
||||
}
|
||||
|
||||
startAt := time.Now()
|
||||
requestBody := readRequestBody(ctx.Request)
|
||||
|
||||
writer := &debugBodyWriter{ResponseWriter: ctx.Writer}
|
||||
ctx.Writer = writer
|
||||
ctx.Next()
|
||||
|
||||
fields := map[string]any{
|
||||
"method": ctx.Request.Method,
|
||||
"path": ctx.Request.URL.Path,
|
||||
"rawQuery": ctx.Request.URL.RawQuery,
|
||||
"status": writer.Status(),
|
||||
"durationMs": time.Since(startAt).Milliseconds(),
|
||||
"clientIP": ctx.ClientIP(),
|
||||
"requestHeader": log.SanitizeHeaders(ctx.Request.Header),
|
||||
"requestBody": truncate(log.SanitizeBody(ctx.ContentType(), requestBody), maxDebugBodySize),
|
||||
"responseHeader": log.SanitizeHeaders(http.Header(writer.Header().Clone())),
|
||||
"responseBody": truncate(log.SanitizeBody(writer.Header().Get("Content-Type"), writer.body.String()), maxDebugBodySize),
|
||||
"responseSize": writer.Size(),
|
||||
"handler": ctx.HandlerName(),
|
||||
"abortWithErrors": ctx.Errors.ByType(gin.ErrorTypeAny).String(),
|
||||
}
|
||||
|
||||
log.Capture(zapcore.DebugLevel, "http", "incoming exchange", fields)
|
||||
}
|
||||
}
|
||||
|
||||
func readRequestBody(r *http.Request) string {
|
||||
if r == nil || r.Body == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(body))
|
||||
return string(body)
|
||||
}
|
||||
|
||||
func truncate(value string, limit int) string {
|
||||
if len(value) <= limit {
|
||||
return value
|
||||
}
|
||||
return value[:limit] + "...(truncated)"
|
||||
}
|
||||
|
||||
func isDebugLogRoute(path string) bool {
|
||||
return strings.HasPrefix(path, "/api/debug/ws/logs")
|
||||
}
|
||||
@@ -48,6 +48,9 @@ func SetupRouter() *gin.Engine {
|
||||
AllowCredentials: true,
|
||||
MaxAge: 12 * time.Hour,
|
||||
}))
|
||||
if conf.IsDebugMode() {
|
||||
r.Use(middleware.DebugHTTPLog())
|
||||
}
|
||||
wkHandler := handler.NewWKHandler()
|
||||
sessionMiddleware := middleware.SessionMiddleware(wkHandler.Session)
|
||||
// schedule.StartCron(wkHandler.Session)
|
||||
@@ -65,6 +68,12 @@ func SetupRouter() *gin.Engine {
|
||||
{
|
||||
api.POST("/login", wkHandler.Login)
|
||||
api.Any("/version", handler.Version)
|
||||
if conf.IsDebugMode() {
|
||||
debug := api.Group("/debug")
|
||||
{
|
||||
debug.GET("/ws/logs", handler.DebugLogWS)
|
||||
}
|
||||
}
|
||||
v1 := api.Group("/v1")
|
||||
{
|
||||
v1.GET("/host", wkHandler.Host)
|
||||
|
||||
Reference in New Issue
Block a user