feat: add Docker support and logging enhancements

- Introduced Dockerfile and .dockerignore for containerization of the backend service.
- Added logging configuration with support for daily rolling logs and customizable log levels.
- Enhanced the server to utilize structured logging with zap, including detailed request and error logging.
- Updated configuration to include logging parameters and proxy settings.
- Implemented tests for logging configuration and proxy settings.
This commit is contained in:
2026-02-15 18:24:48 +08:00
parent 3284ce07c7
commit eab464060b
9 changed files with 870 additions and 27 deletions

View File

@@ -4,9 +4,11 @@ import (
"context"
"errors"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"monica-go-completion-backend/internal/completion"
)
@@ -67,6 +69,13 @@ func RegisterRoutes(router *gin.Engine, service CompletionService, options ...Ro
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, opts.MaxBodyBytes)
var req completion.Request
if err := c.ShouldBindJSON(&req); err != nil {
zap.L().Warn(
"invalid completion json payload",
zap.String("traceId", traceIDFromGin(c)),
zap.String("routeLanguage", c.Param("language")),
zap.String("clientIP", c.ClientIP()),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid JSON payload"})
return
}
@@ -84,19 +93,51 @@ func RegisterRoutes(router *gin.Engine, service CompletionService, options ...Ro
resp, err := service.Complete(ctx, req)
if err != nil {
if errors.Is(err, completion.ErrInvalidRequest) {
zap.L().Warn(
"completion request invalid",
zap.String("traceId", traceIDFromGin(c)),
zap.String("clientIP", c.ClientIP()),
zap.String("language", req.Language),
zap.String("uri", req.URI),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if errors.Is(err, completion.ErrUnsupportedLanguage) {
zap.L().Warn(
"completion language not supported",
zap.String("traceId", traceIDFromGin(c)),
zap.String("clientIP", c.ClientIP()),
zap.String("language", req.Language),
zap.Error(err),
)
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if errors.Is(err, completion.ErrTooManySessions) {
zap.L().Warn(
"completion rejected due to session limit",
zap.String("traceId", traceIDFromGin(c)),
zap.String("clientIP", c.ClientIP()),
zap.String("language", req.Language),
zap.String("sessionId", req.SessionID),
zap.Error(err),
)
c.JSON(http.StatusTooManyRequests, gin.H{"error": err.Error()})
return
}
var ownedErr *completion.ErrSessionOwnedByOtherInstance
if errors.As(err, &ownedErr) {
zap.L().Info(
"completion routed to session owner",
zap.String("traceId", traceIDFromGin(c)),
zap.String("clientIP", c.ClientIP()),
zap.String("language", req.Language),
zap.String("sessionId", req.SessionID),
zap.String("ownerId", ownedErr.OwnerID),
zap.String("routeTo", ownedErr.OwnerEndpoint),
)
if ownedErr.OwnerEndpoint != "" {
c.Header("X-LSP-Route-To", ownedErr.OwnerEndpoint)
}
@@ -107,9 +148,28 @@ func RegisterRoutes(router *gin.Engine, service CompletionService, options ...Ro
})
return
}
zap.L().Error(
"completion failed",
zap.String("traceId", traceIDFromGin(c)),
zap.String("clientIP", c.ClientIP()),
zap.String("language", req.Language),
zap.String("sessionId", req.SessionID),
zap.String("uri", req.URI),
zap.Error(err),
)
c.JSON(http.StatusInternalServerError, gin.H{"error": "completion failed"})
return
}
zap.L().Info(
"completion success",
zap.String("traceId", traceIDFromGin(c)),
zap.String("clientIP", c.ClientIP()),
zap.String("language", req.Language),
zap.String("sessionId", req.SessionID),
zap.String("uri", req.URI),
zap.Int("items", len(resp.Items)),
zap.Bool("isIncomplete", resp.IsIncomplete),
)
c.JSON(http.StatusOK, resp)
}
@@ -118,3 +178,14 @@ func RegisterRoutes(router *gin.Engine, service CompletionService, options ...Ro
handleCompletion(c)
})
}
func traceIDFromGin(c *gin.Context) string {
if c == nil {
return ""
}
traceID := strings.TrimSpace(c.GetString("traceId"))
if traceID != "" {
return traceID
}
return strings.TrimSpace(c.GetString("requestId"))
}

View File

@@ -9,6 +9,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"monica-go-completion-backend/internal/completion"
)
@@ -62,10 +63,35 @@ func registerWSRoutes(router *gin.Engine, service CompletionService, opts RouteO
handler := func(c *gin.Context, defaultLanguage string) {
conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
zap.L().Warn(
"ws upgrade failed",
zap.String("traceId", traceIDFromGin(c)),
zap.String("path", c.Request.URL.Path),
zap.String("clientIP", c.ClientIP()),
zap.Error(err),
)
return
}
defer conn.Close()
traceID := traceIDFromGin(c)
zap.L().Info(
"ws connection opened",
zap.String("traceId", traceID),
zap.String("path", c.Request.URL.Path),
zap.String("language", defaultLanguage),
zap.String("clientIP", c.ClientIP()),
)
defer func() {
zap.L().Info(
"ws connection closed",
zap.String("traceId", traceID),
zap.String("path", c.Request.URL.Path),
zap.String("language", defaultLanguage),
zap.String("clientIP", c.ClientIP()),
)
}()
conn.SetReadLimit(opts.MaxBodyBytes)
var writeMu sync.Mutex
@@ -74,9 +100,15 @@ func registerWSRoutes(router *gin.Engine, service CompletionService, opts RouteO
// 单连接串行读取消息,写操作通过 writeMu 保证并发安全。
_, payload, err := conn.ReadMessage()
if err != nil {
zap.L().Info(
"ws read loop ended",
zap.String("traceId", traceID),
zap.String("path", c.Request.URL.Path),
zap.Error(err),
)
break
}
handleWSMessage(conn, &writeMu, service, payload, defaultLanguage, opts)
handleWSMessage(conn, &writeMu, service, payload, defaultLanguage, traceID, c.ClientIP(), opts)
}
}
@@ -95,14 +127,22 @@ func handleWSMessage(
service CompletionService,
payload []byte,
defaultLanguage string,
traceID string,
clientIP string,
opts RouteOptions,
) {
if tryHandleRPCMessage(conn, writeMu, service, payload, defaultLanguage, opts) {
if tryHandleRPCMessage(conn, writeMu, service, payload, defaultLanguage, traceID, clientIP, opts) {
return
}
var req wsCompletionRequest
if err := json.Unmarshal(payload, &req); err != nil {
zap.L().Warn(
"ws invalid json payload",
zap.String("traceId", traceID),
zap.String("clientIP", clientIP),
zap.Error(err),
)
sendWSResponse(conn, writeMu, wsCompletionResponse{
ID: "",
Error: "invalid JSON payload",
@@ -113,7 +153,7 @@ func handleWSMessage(
req.Language = defaultLanguage
}
processWSCompletion(conn, writeMu, service, req, opts)
processWSCompletion(conn, writeMu, service, req, traceID, clientIP, opts)
}
// tryHandleRPCMessage 处理 JSON-RPC 2.0 请求,返回 true 表示消息已消费。
@@ -123,6 +163,8 @@ func tryHandleRPCMessage(
service CompletionService,
payload []byte,
defaultLanguage string,
traceID string,
clientIP string,
opts RouteOptions,
) bool {
var rpcReq wsRPCRequest
@@ -148,6 +190,13 @@ func tryHandleRPCMessage(
var req wsCompletionRequest
if err := json.Unmarshal(rpcReq.Params, &req); err != nil {
zap.L().Warn(
"ws rpc invalid params",
zap.String("traceId", traceID),
zap.String("clientIP", clientIP),
zap.String("method", rpcReq.Method),
zap.Error(err),
)
// 参数反序列化失败按 invalid params 处理。
sendWSRPCResponse(conn, writeMu, wsRPCResponse{
JSONRPC: "2.0",
@@ -179,6 +228,16 @@ func tryHandleRPCMessage(
Character: req.Character,
})
if err != nil {
zap.L().Warn(
"ws rpc completion failed",
zap.String("traceId", traceID),
zap.String("clientIP", clientIP),
zap.String("method", rpcReq.Method),
zap.String("language", req.Language),
zap.String("sessionId", req.SessionID),
zap.String("uri", req.URI),
zap.Error(err),
)
sendWSRPCResponse(conn, writeMu, wsRPCResponse{
JSONRPC: "2.0",
ID: rpcReq.ID,
@@ -204,6 +263,8 @@ func processWSCompletion(
writeMu *sync.Mutex,
service CompletionService,
req wsCompletionRequest,
traceID string,
clientIP string,
opts RouteOptions,
) {
ctx, cancel := context.WithTimeout(context.Background(), opts.RequestTimeout)
@@ -237,6 +298,17 @@ func processWSCompletion(
ownerID = ownedErr.OwnerID
}
}
zap.L().Warn(
"ws completion failed",
zap.String("traceId", traceID),
zap.String("clientIP", clientIP),
zap.String("language", req.Language),
zap.String("sessionId", req.SessionID),
zap.String("uri", req.URI),
zap.String("routeTo", routeTo),
zap.String("ownerId", ownerID),
zap.Error(err),
)
sendWSResponse(conn, writeMu, wsCompletionResponse{
ID: req.ID,
RouteTo: routeTo,
@@ -245,6 +317,15 @@ func processWSCompletion(
})
return
}
zap.L().Info(
"ws completion success",
zap.String("traceId", traceID),
zap.String("clientIP", clientIP),
zap.String("language", req.Language),
zap.String("sessionId", req.SessionID),
zap.String("uri", req.URI),
zap.Int("items", len(resp.Items)),
)
sendWSResponse(conn, writeMu, wsCompletionResponse{
ID: req.ID,