Files
Enterprise-Knowledge-Base/internal/middleware/logger.go
T
wizardchen fba6d7d145 refactor: Update wiki prompts and ingestion logic for improved content handling
- Renamed `WikiPageUpdatePrompt` to `WikiPageModifyPrompt` to better reflect its functionality of adding new information and removing outdated content in a single operation.
- Enhanced the instructions for both `WikiPageModifyPrompt` and `WikiPageRetractPrompt` to clarify the handling of additions and retractions, ensuring accurate content updates.
- Consolidated the logic for enqueuing retraction tasks in the `cleanupWikiOnKnowledgeDelete` method to streamline the process of managing affected pages.
- Removed the `rebuildWikiIndexSimple` function to simplify the codebase, as its functionality is no longer needed with the new ingestion approach.

These changes improve the clarity and efficiency of the wiki content management process, enhancing overall system performance.
2026-04-22 21:17:31 +08:00

236 lines
6.6 KiB
Go

package middleware
import (
"bytes"
"context"
"io"
"regexp"
"strings"
"time"
"github.com/Tencent/WeKnora/internal/logger"
"github.com/Tencent/WeKnora/internal/types"
secutils "github.com/Tencent/WeKnora/internal/utils"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
)
const (
maxBodySize = 1024 * 10 // 最大记录10KB的body内容
)
// loggerResponseBodyWriter 自定义ResponseWriter用于捕获响应内容(用于logger中间件)
type loggerResponseBodyWriter struct {
gin.ResponseWriter
body *bytes.Buffer
}
// Write 重写Write方法,同时写入buffer和原始writer
// 限制buffer大小,避免SSE等流式响应导致内存无限增长
func (r loggerResponseBodyWriter) Write(b []byte) (int, error) {
if r.body.Len() < maxBodySize {
remaining := maxBodySize - r.body.Len()
if len(b) <= remaining {
r.body.Write(b)
} else {
r.body.Write(b[:remaining])
}
}
return r.ResponseWriter.Write(b)
}
// sanitizeBody 清理敏感信息
func sanitizeBody(body string) string {
result := body
// 替换常见的敏感字段(JSON格式)
sensitivePatterns := []struct {
pattern string
replacement string
}{
{`"password"\s*:\s*"[^"]*"`, `"password":"***"`},
{`"token"\s*:\s*"[^"]*"`, `"token":"***"`},
{`"access_token"\s*:\s*"[^"]*"`, `"access_token":"***"`},
{`"refresh_token"\s*:\s*"[^"]*"`, `"refresh_token":"***"`},
{`"authorization"\s*:\s*"[^"]*"`, `"authorization":"***"`},
{`"api_key"\s*:\s*"[^"]*"`, `"api_key":"***"`},
{`"secret"\s*:\s*"[^"]*"`, `"secret":"***"`},
{`"apikey"\s*:\s*"[^"]*"`, `"apikey":"***"`},
{`"apisecret"\s*:\s*"[^"]*"`, `"apisecret":"***"`},
}
for _, p := range sensitivePatterns {
re := regexp.MustCompile(p.pattern)
result = re.ReplaceAllString(result, p.replacement)
}
return result
}
// readRequestBody 读取请求体(限制大小用于日志,但完整读取用于重置)
func readRequestBody(c *gin.Context) string {
if c.Request.Body == nil {
return ""
}
// 检查Content-Type,只记录JSON类型
contentType := c.GetHeader("Content-Type")
if !strings.Contains(contentType, "application/json") &&
!strings.Contains(contentType, "application/x-www-form-urlencoded") &&
!strings.Contains(contentType, "text/") {
return "[非文本类型,已跳过]"
}
// 完整读取body内容(不限制大小),因为需要完整重置给后续handler使用
bodyBytes, err := io.ReadAll(c.Request.Body)
if err != nil {
return "[读取请求体失败]"
}
// 重置request body,使用完整内容,确保后续handler能读取到完整数据
c.Request.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
// 用于日志的body(限制大小)
var logBodyBytes []byte
if len(bodyBytes) > maxBodySize {
logBodyBytes = bodyBytes[:maxBodySize]
} else {
logBodyBytes = bodyBytes
}
bodyStr := string(logBodyBytes)
if len(bodyBytes) > maxBodySize {
bodyStr += "... [内容过长,已截断]"
}
return sanitizeBody(bodyStr)
}
// RequestID middleware adds a unique request ID to the context
func RequestID() gin.HandlerFunc {
return func(c *gin.Context) {
// Get request ID from header or generate a new one
requestID := c.GetHeader("X-Request-ID")
if requestID == "" {
requestID = uuid.New().String()
}
safeRequestID := secutils.SanitizeForLog(requestID)
// Set request ID in header
c.Header("X-Request-ID", requestID)
// Set request ID in context
c.Set(types.RequestIDContextKey.String(), requestID)
// Set logger in context
requestLogger := logger.GetLogger(c)
requestLogger = requestLogger.WithField("request_id", safeRequestID)
c.Set(types.LoggerContextKey.String(), requestLogger)
// Set request ID in the global context for logging
c.Request = c.Request.WithContext(
context.WithValue(
context.WithValue(c.Request.Context(), types.RequestIDContextKey, requestID),
types.LoggerContextKey, requestLogger,
),
)
c.Next()
}
}
// Logger middleware logs request details with request ID, input and output
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
raw := c.Request.URL.RawQuery
isWikiStats := strings.HasPrefix(path, "/api/v1/knowledgebase/") && strings.HasSuffix(path, "/wiki/stats")
if strings.HasPrefix(path, "/assets/") || isWikiStats {
c.Next()
return
}
// 读取请求体(在Next之前读取,因为Next会消费body)
var requestBody string
if c.Request.Method == "POST" || c.Request.Method == "PUT" || c.Request.Method == "PATCH" {
requestBody = readRequestBody(c)
}
// 创建响应体捕获器
responseBody := &bytes.Buffer{}
responseWriter := &loggerResponseBodyWriter{
ResponseWriter: c.Writer,
body: responseBody,
}
c.Writer = responseWriter
// Process request
c.Next()
// Get request ID from context
requestID, exists := c.Get(types.RequestIDContextKey.String())
requestIDStr := "unknown"
if exists {
if idStr, ok := requestID.(string); ok && idStr != "" {
requestIDStr = idStr
}
}
safeRequestID := secutils.SanitizeForLog(requestIDStr)
// Calculate latency
latency := time.Since(start)
// Get client IP and status code
clientIP := c.ClientIP()
statusCode := c.Writer.Status()
method := c.Request.Method
if raw != "" {
path = path + "?" + raw
}
// 读取响应体
responseBodyStr := ""
if responseBody.Len() > 0 {
contentType := c.Writer.Header().Get("Content-Type")
if strings.Contains(contentType, "text/event-stream") {
responseBodyStr = "[SSE流式响应,已跳过]"
} else if strings.Contains(contentType, "application/json") ||
strings.Contains(contentType, "text/") {
bodyBytes := responseBody.Bytes()
if len(bodyBytes) >= maxBodySize {
responseBodyStr = string(bodyBytes[:maxBodySize]) + "... [内容过长,已截断]"
} else {
responseBodyStr = string(bodyBytes)
}
responseBodyStr = sanitizeBody(responseBodyStr)
} else {
responseBodyStr = "[非文本类型,已跳过]"
}
}
// 构建日志消息
logMsg := logger.GetLogger(c)
logMsg = logMsg.WithFields(map[string]interface{}{
"request_id": safeRequestID,
"method": method,
"path": secutils.SanitizeForLog(path),
"status_code": statusCode,
"size": c.Writer.Size(),
"latency": latency.String(),
"client_ip": secutils.SanitizeForLog(clientIP),
})
// 添加请求体(如果有)
if requestBody != "" {
logMsg = logMsg.WithField("request_body", secutils.SanitizeForLog(requestBody))
}
// 添加响应体(如果有)
if responseBodyStr != "" {
logMsg = logMsg.WithField("response_body", secutils.SanitizeForLog(responseBodyStr))
}
logMsg.Info()
}
}