优化
This commit is contained in:
+22
-2
@@ -6,6 +6,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -20,10 +21,10 @@ import (
|
||||
kratoshttp "github.com/go-kratos/kratos/v3/transport/http"
|
||||
)
|
||||
|
||||
func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, handlers *handler.Set, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger) *gin.Engine {
|
||||
func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, handlers *handler.Set, auth *service.AuthService, security *service.SecurityService, audit *service.AuditRecorder, logger *slog.Logger, version string) *gin.Engine {
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
engine := gin.New()
|
||||
engine.Use(servermiddleware.RequestMeta(), servermiddleware.Recovery(audit, logger), servermiddleware.AccessLog(runtime, logger), servermiddleware.ErrorAudit(audit), servermiddleware.SecurityRateLimit(security))
|
||||
engine.Use(servermiddleware.RequestMeta(), servermiddleware.Recovery(audit, logger), servermiddleware.AccessLog(runtime, logger, version), servermiddleware.CORS(runtime), servermiddleware.ErrorAudit(audit), servermiddleware.SecurityRateLimit(security))
|
||||
|
||||
prefix := ""
|
||||
config := runtime.Admin()
|
||||
@@ -66,9 +67,28 @@ func NewGinEngine(runtime *conf.Runtime, access *service.AccessControlService, h
|
||||
}
|
||||
httpx.Fail(c, "请求的接口不存在")
|
||||
})
|
||||
logRegisteredRoutes(engine, logger)
|
||||
return engine
|
||||
}
|
||||
|
||||
func logRegisteredRoutes(engine *gin.Engine, logger *slog.Logger) {
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
routes := append([]gin.RouteInfo(nil), engine.Routes()...)
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
if routes[i].Path == routes[j].Path {
|
||||
return routes[i].Method < routes[j].Method
|
||||
}
|
||||
return routes[i].Path < routes[j].Path
|
||||
})
|
||||
systemLogger := logger.With("mod", "system")
|
||||
for _, route := range routes {
|
||||
systemLogger.Info("router registered", "method", route.Method, "path", route.Path)
|
||||
}
|
||||
systemLogger.Info("router register success", "route_count", len(routes))
|
||||
}
|
||||
|
||||
func NewGinServer(c *conf.Server, engine *gin.Engine) *kratoshttp.Server {
|
||||
network, address := "tcp", ":8000"
|
||||
if c != nil && c.Http != nil {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"kra/internal/conf"
|
||||
"kra/internal/server/handler"
|
||||
)
|
||||
|
||||
func TestGinRouteContract(t *testing.T) {
|
||||
handlers := &handler.Set{
|
||||
Authority: &handler.Authority{}, Menu: &handler.Menu{}, API: &handler.API{},
|
||||
Permission: &handler.Permission{}, Organization: &handler.Organization{},
|
||||
Announcement: &handler.Announcement{}, Email: &handler.Email{}, Task: &handler.Task{},
|
||||
Media: &handler.Media{}, Audit: &handler.Audit{}, Export: &handler.Export{},
|
||||
Version: &handler.Version{}, Dictionary: &handler.Dictionary{}, Parameter: &handler.Parameter{},
|
||||
APIToken: &handler.APIToken{}, SystemConfig: &handler.SystemConfig{}, Public: &handler.Public{},
|
||||
User: &handler.User{}, Navigation: &handler.Navigation{}, Session: &handler.Session{},
|
||||
}
|
||||
engine := NewGinEngine(conf.NewRuntime(nil, &conf.AdminBackend{}), nil, handlers, nil, nil, nil, nil, "test")
|
||||
routes := engine.Routes()
|
||||
if len(routes) != 177 {
|
||||
t.Fatalf("route contract changed: got %d routes, want 177", len(routes))
|
||||
}
|
||||
seen := make(map[string]struct{}, len(routes))
|
||||
for _, route := range routes {
|
||||
key := route.Method + " " + route.Path
|
||||
if _, exists := seen[key]; exists {
|
||||
t.Fatalf("duplicate route %s", key)
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package handler
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
@@ -18,10 +19,11 @@ type Audit struct {
|
||||
service *service.AuditService
|
||||
recorder *service.AuditRecorder
|
||||
logs *service.LogViewerService
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
func NewAudit(service *service.AuditService, recorder *service.AuditRecorder, logs *service.LogViewerService) *Audit {
|
||||
return &Audit{service: service, recorder: recorder, logs: logs}
|
||||
func NewAudit(service *service.AuditService, recorder *service.AuditRecorder, logs *service.LogViewerService, logger *slog.Logger) *Audit {
|
||||
return &Audit{service: service, recorder: recorder, logs: logs, logger: logger}
|
||||
}
|
||||
|
||||
func page(c *gin.Context) (int, int) {
|
||||
@@ -53,8 +55,8 @@ func IDsFromQuery(c *gin.Context) []uint {
|
||||
return ids
|
||||
}
|
||||
|
||||
// auditID accepts both forms used by the GVA web client over time: DELETE
|
||||
// requests may carry the identifier in the query string or in a JSON body.
|
||||
// auditID accepts both DELETE encodings supported by the administration API:
|
||||
// the identifier may be carried in the query string or in a JSON body.
|
||||
func auditID(c *gin.Context) (uint, error) {
|
||||
if raw := c.Query("ID"); raw != "" {
|
||||
value, err := strconv.ParseUint(raw, 10, 64)
|
||||
@@ -68,7 +70,7 @@ func auditID(c *gin.Context) (uint, error) {
|
||||
}
|
||||
|
||||
// auditIDs mirrors auditID for bulk deletes and supports both query-array and
|
||||
// JSON-body encodings used by compatible GVA pages.
|
||||
// JSON-body encodings used by compatible administration pages.
|
||||
func auditIDs(c *gin.Context) ([]uint, error) {
|
||||
if ids := IDsFromQuery(c); len(ids) > 0 {
|
||||
return ids, nil
|
||||
@@ -230,7 +232,7 @@ func (h *Audit) LogDates(c *gin.Context) {
|
||||
}
|
||||
data, err := h.logs.LogDates(c.Request.Context(), month)
|
||||
if err != nil {
|
||||
failLogViewer(c, err)
|
||||
failLogViewer(c, err, h.logger)
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
|
||||
@@ -243,7 +245,7 @@ func (h *Audit) LogFiles(c *gin.Context) {
|
||||
}
|
||||
data, err := h.logs.LogFiles(c.Request.Context(), date)
|
||||
if err != nil {
|
||||
failLogViewer(c, err)
|
||||
failLogViewer(c, err, h.logger)
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
|
||||
@@ -267,13 +269,16 @@ func (h *Audit) LogContent(c *gin.Context) {
|
||||
// "日志文件路径不合法" error rather than rejecting them as binding errors.
|
||||
data, err := h.logs.LogContent(c.Request.Context(), date, path, cursor)
|
||||
if err != nil {
|
||||
failLogViewer(c, err)
|
||||
failLogViewer(c, err, h.logger)
|
||||
return
|
||||
}
|
||||
httpx.Write(c, httpx.CodeSuccess, data, "获取成功")
|
||||
}
|
||||
|
||||
func failLogViewer(c *gin.Context, err error) {
|
||||
func failLogViewer(c *gin.Context, err error, logger *slog.Logger) {
|
||||
if logger != nil {
|
||||
logger.ErrorContext(c.Request.Context(), "日志查看失败", "mod", "log-viewer", "error", err)
|
||||
}
|
||||
message := "读取日志失败"
|
||||
switch {
|
||||
case errors.Is(err, biz.ErrInvalidLogMonth):
|
||||
|
||||
@@ -149,6 +149,10 @@ func (h *Dictionary) FindDetail(c *gin.Context) {
|
||||
httpx.Fail(c, err.Error())
|
||||
return
|
||||
}
|
||||
if req.ID == 0 {
|
||||
httpx.Fail(c, "ID值不能为空")
|
||||
return
|
||||
}
|
||||
item, err := h.service.DictionaryDetail(c.Request.Context(), req.ID)
|
||||
if err != nil {
|
||||
httpx.Fail(c, "查询失败")
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -14,38 +15,42 @@ import (
|
||||
|
||||
// AccessLog is the single global request/response capture point, matching
|
||||
// the reference middleware ordering and making every HTTP request observable.
|
||||
func AccessLog(runtime *conf.Runtime, logger *slog.Logger) gin.HandlerFunc {
|
||||
func AccessLog(runtime *conf.Runtime, logger *slog.Logger, version string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
started := time.Now()
|
||||
var requestBody []byte
|
||||
multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
|
||||
bytesIn := c.Request.ContentLength
|
||||
if c.Request.Body != nil && !multipart {
|
||||
requestBody, _ = io.ReadAll(c.Request.Body)
|
||||
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
|
||||
bytesIn = int64(len(requestBody))
|
||||
}
|
||||
if bytesIn < 0 {
|
||||
bytesIn = 0
|
||||
}
|
||||
maxBytes := 1 << 20
|
||||
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes}
|
||||
c.Writer = writer
|
||||
started := time.Now()
|
||||
c.Header("X-Kra-Version", version)
|
||||
config := runtime.Admin()
|
||||
logLimit := 1024
|
||||
if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
|
||||
logLimit = int(config.Zap.AccessLogMaxBytes)
|
||||
}
|
||||
requestText := ""
|
||||
if multipart {
|
||||
requestText = "[文件]"
|
||||
} else {
|
||||
requestText = redactJSON(requestBody, c.GetHeader("Content-Type"), logLimit)
|
||||
}
|
||||
c.Set(ctxReqBodyKey, requestText)
|
||||
c.Set(ctxRespBufferKey, &writer.body)
|
||||
c.Next()
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
requestText, responseText := "", ""
|
||||
config := runtime.Admin()
|
||||
logLimit := 32768
|
||||
if config != nil && config.Zap != nil && config.Zap.AccessLogMaxBytes > 0 {
|
||||
logLimit = int(config.Zap.AccessLogMaxBytes)
|
||||
}
|
||||
if config == nil || config.Zap == nil || config.Zap.AccessReqBody {
|
||||
if multipart {
|
||||
requestText = "[文件]"
|
||||
} else {
|
||||
requestText = redactJSONLimit(requestBody, logLimit)
|
||||
}
|
||||
}
|
||||
if config == nil || config.Zap == nil || config.Zap.AccessRespData {
|
||||
responseText = redactJSONLimit(writer.body.Bytes(), logLimit)
|
||||
}
|
||||
responseText := redactJSON(writer.body.Bytes(), c.Writer.Header().Get("Content-Type"), logLimit)
|
||||
userID, authorityID := uint(0), uint(0)
|
||||
if claims := Claims(c); claims != nil {
|
||||
userID, authorityID = claims.ID, claims.AuthorityID
|
||||
@@ -54,27 +59,41 @@ func AccessLog(runtime *conf.Runtime, logger *slog.Logger) gin.HandlerFunc {
|
||||
if route == "" {
|
||||
route = "unmatched"
|
||||
}
|
||||
attributes := []any{
|
||||
"ip", c.ClientIP(), "method", c.Request.Method, "path", c.Request.URL.Path, "http_route", route,
|
||||
"status", c.Writer.Status(), "latency_ms", time.Since(started).Milliseconds(),
|
||||
"request_id", stringValueFromContext(c, "request_id"), "trace_id", stringValueFromContext(c, "trace_id"),
|
||||
"bytes_in", len(requestBody), "bytes_out", c.Writer.Size(), "user_id", userID, "authority_id", authorityID,
|
||||
"ua", c.Request.UserAgent(), "query", c.Request.URL.RawQuery, "request", requestText, "response", responseText}
|
||||
if config != nil && config.Zap != nil && config.Zap.AccessReqHeaders {
|
||||
attributes = append(attributes, "headers", redactHeaders(c.Request.Header))
|
||||
bytesOut := int64(c.Writer.Size())
|
||||
if bytesOut < 0 {
|
||||
bytesOut = 0
|
||||
}
|
||||
logger.InfoContext(c.Request.Context(), "http access", attributes...)
|
||||
privateErrors := strings.TrimRight(c.Errors.ByType(gin.ErrorTypePrivate).String(), "\n")
|
||||
attributes := []any{
|
||||
"mod", "http", "ip", c.ClientIP(), "method", c.Request.Method, "http_path", c.Request.URL.Path, "http_route", route,
|
||||
"http_status", c.Writer.Status(), "latency_ms", time.Since(started).Milliseconds(),
|
||||
"request_id", stringValueFromContext(c, "request_id"), "trace_id", stringValueFromContext(c, "trace_id"),
|
||||
"bytes_in", bytesIn, "bytes_out", bytesOut, "user_id", userID, "authority_id", authorityID,
|
||||
"error", c.Writer.Status() >= http.StatusInternalServerError || privateErrors != "", "ua", c.Request.UserAgent(), "req_query", c.Request.URL.RawQuery}
|
||||
if config != nil && config.Zap != nil && config.Zap.AccessReqHeaders {
|
||||
attributes = append(attributes, "req_headers", redactHeaders(c.Request.Header))
|
||||
}
|
||||
if config != nil && config.Zap != nil && config.Zap.AccessReqBody {
|
||||
attributes = append(attributes, "req_body", requestText)
|
||||
}
|
||||
if config != nil && config.Zap != nil && config.Zap.AccessRespData {
|
||||
attributes = append(attributes, "resp_data", responseText)
|
||||
}
|
||||
if privateErrors != "" {
|
||||
attributes = append(attributes, "error_msg", privateErrors)
|
||||
}
|
||||
logger.InfoContext(c.Request.Context(), "请求完成", attributes...)
|
||||
}
|
||||
}
|
||||
|
||||
func redactHeaders(headers map[string][]string) map[string][]string {
|
||||
out := make(map[string][]string, len(headers))
|
||||
func redactHeaders(headers map[string][]string) map[string]string {
|
||||
out := make(map[string]string, len(headers))
|
||||
for key, values := range headers {
|
||||
lower := strings.ToLower(key)
|
||||
if strings.Contains(lower, "token") || lower == "authorization" || lower == "cookie" {
|
||||
out[key] = []string{"******"}
|
||||
if lower == "authorization" || lower == "cookie" || lower == "set-cookie" || lower == "x-token" {
|
||||
out[key] = "***"
|
||||
} else {
|
||||
out[key] = values
|
||||
out[key] = strings.Join(values, ",")
|
||||
}
|
||||
}
|
||||
return out
|
||||
|
||||
@@ -31,7 +31,10 @@ func OperationAudit(runtime *conf.Runtime, service *service.AuditRecorder) gin.H
|
||||
}
|
||||
if c.Request.Method == http.MethodGet {
|
||||
requestBody = operationQueryBody(c.Request.URL.RawQuery)
|
||||
} else if value, ok := c.Get(ctxReqBodyKey); ok {
|
||||
requestBody = []byte(stringValue(value))
|
||||
} else if c.Request.Body != nil {
|
||||
// Fallback for tests or custom middleware chains that omit AccessLog.
|
||||
if strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data") {
|
||||
requestBody = []byte("[文件]")
|
||||
} else {
|
||||
@@ -43,8 +46,6 @@ func OperationAudit(runtime *conf.Runtime, service *service.AuditRecorder) gin.H
|
||||
// safety limit and only applies the configured operation-log limit when a
|
||||
// download response is recorded. Using maxBytes here would silently
|
||||
// truncate ordinary JSON responses before that decision is possible.
|
||||
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: 1 << 20}
|
||||
c.Writer = writer
|
||||
started := time.Now()
|
||||
c.Next()
|
||||
userID := uint(0)
|
||||
@@ -55,7 +56,12 @@ func OperationAudit(runtime *conf.Runtime, service *service.AuditRecorder) gin.H
|
||||
}
|
||||
requestID, _ := c.Get("request_id")
|
||||
status := c.Writer.Status()
|
||||
responseBody := writer.body.String()
|
||||
responseBody := ""
|
||||
if value, ok := c.Get(ctxRespBufferKey); ok {
|
||||
if body, bok := value.(*bytes.Buffer); bok {
|
||||
responseBody = body.String()
|
||||
}
|
||||
}
|
||||
if isDownloadResponse(c) && len(responseBody) > maxBytes {
|
||||
responseBody = "[超出记录长度]"
|
||||
}
|
||||
|
||||
@@ -8,6 +8,14 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// The global access logger is the single request/response capture point.
|
||||
// OperationAudit consumes these values after the handler returns, avoiding a
|
||||
// second body read/writer wrapper.
|
||||
const (
|
||||
ctxReqBodyKey = "kra_req_body"
|
||||
ctxRespBufferKey = "kra_resp_buffer"
|
||||
)
|
||||
|
||||
type captureWriter struct {
|
||||
gin.ResponseWriter
|
||||
body bytes.Buffer
|
||||
@@ -17,7 +25,7 @@ type captureWriter struct {
|
||||
func (w *captureWriter) Write(data []byte) (int, error) {
|
||||
limit := w.maxBytes
|
||||
if limit <= 0 {
|
||||
limit = 32768
|
||||
limit = 1024
|
||||
}
|
||||
if w.body.Len() < limit {
|
||||
remaining := limit - w.body.Len()
|
||||
@@ -30,44 +38,32 @@ func (w *captureWriter) Write(data []byte) (int, error) {
|
||||
return w.ResponseWriter.Write(data)
|
||||
}
|
||||
|
||||
func redactJSON(raw []byte) string {
|
||||
return redactJSONLimit(raw, 32768)
|
||||
}
|
||||
|
||||
func redactJSONLimit(raw []byte, limit int) string {
|
||||
func redactJSON(raw []byte, contentType string, limit int) string {
|
||||
if len(raw) == 0 {
|
||||
return ""
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 32768
|
||||
limit = 1024
|
||||
}
|
||||
if len(raw) > limit {
|
||||
raw = raw[:limit]
|
||||
text := string(raw)
|
||||
if !strings.Contains(strings.ToLower(contentType), "json") {
|
||||
if len(text) > limit {
|
||||
return "[超出记录长度]"
|
||||
}
|
||||
return text
|
||||
}
|
||||
var value any
|
||||
if json.Unmarshal(raw, &value) != nil {
|
||||
return string(raw)
|
||||
}
|
||||
var clean func(any)
|
||||
clean = func(current any) {
|
||||
switch v := current.(type) {
|
||||
case map[string]any:
|
||||
for key, item := range v {
|
||||
lower := strings.ToLower(key)
|
||||
if strings.Contains(lower, "password") || strings.Contains(lower, "token") || strings.Contains(lower, "secret") {
|
||||
v[key] = "******"
|
||||
} else {
|
||||
clean(item)
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, item := range v {
|
||||
clean(item)
|
||||
}
|
||||
if len(text) > limit {
|
||||
return "[超出记录长度]"
|
||||
}
|
||||
return text
|
||||
}
|
||||
clean(value)
|
||||
maskOperationBody(value)
|
||||
encoded, _ := json.Marshal(value)
|
||||
if len(encoded) > limit {
|
||||
return "[超出记录长度]"
|
||||
}
|
||||
return string(encoded)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"kra/internal/conf"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultCORSHeaders = "Content-Type,AccessToken,X-CSRF-Token, Authorization, Token,X-Token,X-User-Id"
|
||||
defaultCORSMethods = "POST, GET, OPTIONS,DELETE,PUT"
|
||||
defaultCORSExpose = "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type, New-Token, New-Expires-At"
|
||||
)
|
||||
|
||||
// CORS applies the current administration CORS rules on every request so a
|
||||
// configuration reload takes effect without rebuilding the Gin engine.
|
||||
func CORS(runtime *conf.Runtime) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
config := runtime.Admin()
|
||||
if config == nil || config.Cors == nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
mode := strings.TrimSpace(config.Cors.Mode)
|
||||
origin := c.GetHeader("Origin")
|
||||
if mode == "allow-all" {
|
||||
setCORSHeaders(c, origin, defaultCORSHeaders, defaultCORSMethods, defaultCORSExpose, true)
|
||||
} else if rule := matchingCORSRule(config.Cors.Whitelist, origin); rule != nil {
|
||||
setCORSHeaders(c, rule.AllowOrigin, rule.AllowHeaders, rule.AllowMethods, rule.ExposeHeaders, rule.AllowCredentials)
|
||||
} else if mode == "strict-whitelist" && !(c.Request.Method == http.MethodGet && c.Request.URL.Path == "/health") {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
if c.Request.Method == http.MethodOptions {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func matchingCORSRule(rules []*conf.AdminBackend_CORSRule, origin string) *conf.AdminBackend_CORSRule {
|
||||
for _, rule := range rules {
|
||||
if rule != nil && origin == rule.AllowOrigin {
|
||||
return rule
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func setCORSHeaders(c *gin.Context, origin, headers, methods, expose string, credentials bool) {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
c.Header("Access-Control-Allow-Headers", headers)
|
||||
c.Header("Access-Control-Allow-Methods", methods)
|
||||
c.Header("Access-Control-Expose-Headers", expose)
|
||||
if credentials {
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
@@ -16,14 +17,18 @@ import (
|
||||
// system errors and therefore are not inserted into sys_error.
|
||||
func ErrorAudit(audit *service.AuditRecorder) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: 1 << 20}
|
||||
c.Writer = writer
|
||||
c.Next()
|
||||
if strings.Contains(c.Request.URL.Path, "/sysError/") || c.Writer.Status() >= 300 && c.Writer.Status() < 500 {
|
||||
return
|
||||
}
|
||||
var response httpx.Response
|
||||
if json.Unmarshal(writer.body.Bytes(), &response) != nil || response.Code == httpx.CodeSuccess || expectedClientFailure(response.Msg) {
|
||||
var body []byte
|
||||
if value, ok := c.Get(ctxRespBufferKey); ok {
|
||||
if buffer, valid := value.(*bytes.Buffer); valid {
|
||||
body = buffer.Bytes()
|
||||
}
|
||||
}
|
||||
if json.Unmarshal(body, &response) != nil || response.Code == httpx.CodeSuccess || expectedClientFailure(response.Msg) {
|
||||
return
|
||||
}
|
||||
requestID, _ := c.Get("request_id")
|
||||
|
||||
@@ -33,7 +33,7 @@ func Recovery(audit *service.AuditRecorder, logger *slog.Logger) gin.HandlerFunc
|
||||
request, _ := httputil.DumpRequest(c.Request, false)
|
||||
info := fmt.Sprintf("error=%v request=%s stack=%s", panicValue, request, debug.Stack())
|
||||
if logger != nil {
|
||||
logger.ErrorContext(c.Request.Context(), "recovery from panic", "error", panicValue, "request", string(request), "stack", string(debug.Stack()))
|
||||
logger.ErrorContext(c.Request.Context(), "recovery from panic", "mod", "error", "error", panicValue, "request", string(request), "stack", string(debug.Stack()))
|
||||
}
|
||||
requestID, _ := c.Get("request_id")
|
||||
_ = audit.CreateErrorRequest(c.Request.Context(), &dto.ErrorRecordRequest{Form: c.Request.URL.Path, Info: info, Level: "error", RequestID: stringValue(requestID), TraceID: stringValueFromContext(c, "trace_id")})
|
||||
|
||||
@@ -3,15 +3,14 @@ package middleware
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"kra/pkg/logging"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var traceParentPattern = regexp.MustCompile(`^00-([0-9a-f]{32})-([0-9a-f]{16})-[0-9a-f]{2}$`)
|
||||
|
||||
func randomHex(bytes int) string {
|
||||
value := make([]byte, bytes)
|
||||
_, _ = rand.Read(value)
|
||||
@@ -21,13 +20,13 @@ func randomHex(bytes int) string {
|
||||
func RequestMeta() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
requestID := c.GetHeader("X-Request-Id")
|
||||
if requestID == "" || len(requestID) > 128 || strings.ContainsAny(requestID, "\r\n") {
|
||||
if !saneHeaderID(requestID) {
|
||||
requestID = uuid.NewString()
|
||||
}
|
||||
traceID, parentSpanID := "", ""
|
||||
if match := traceParentPattern.FindStringSubmatch(strings.ToLower(c.GetHeader("traceparent"))); len(match) == 3 {
|
||||
traceID, parentSpanID = match[1], match[2]
|
||||
} else if candidate := c.GetHeader("X-Trace-Id"); len(candidate) <= 128 && !strings.ContainsAny(candidate, "\r\n") {
|
||||
if upstreamTraceID, upstreamSpanID, ok := parseTraceparent(c.GetHeader("traceparent")); ok {
|
||||
traceID, parentSpanID = upstreamTraceID, upstreamSpanID
|
||||
} else if candidate := c.GetHeader("X-Trace-Id"); saneHeaderID(candidate) {
|
||||
traceID = candidate
|
||||
}
|
||||
if traceID == "" {
|
||||
@@ -36,13 +35,68 @@ func RequestMeta() gin.HandlerFunc {
|
||||
spanID := randomHex(8)
|
||||
c.Header("X-Request-Id", requestID)
|
||||
c.Header("X-Trace-Id", traceID)
|
||||
if len(traceID) == 32 {
|
||||
if validTraceID(traceID) {
|
||||
c.Header("traceparent", "00-"+traceID+"-"+spanID+"-01")
|
||||
}
|
||||
c.Set("request_id", requestID)
|
||||
c.Set("trace_id", traceID)
|
||||
c.Set("span_id", spanID)
|
||||
c.Set("parent_span_id", parentSpanID)
|
||||
c.Request = c.Request.WithContext(logging.WithContextFields(c.Request.Context(), &logging.ContextFields{
|
||||
RequestID: requestID, TraceID: traceID, SpanID: spanID, ParentSpanID: parentSpanID,
|
||||
DeviceID: c.GetHeader("X-Device-Id"), ClientIP: c.ClientIP(),
|
||||
HTTPMethod: c.Request.Method, HTTPPath: c.Request.URL.Path,
|
||||
}))
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func parseTraceparent(value string) (traceID, parentSpanID string, ok bool) {
|
||||
parts := strings.Split(value, "-")
|
||||
if len(parts) < 4 {
|
||||
return "", "", false
|
||||
}
|
||||
version, traceID, parentSpanID, flags := parts[0], parts[1], parts[2], parts[3]
|
||||
if !lowerHex(version, 2) || version == "ff" || version == "00" && len(parts) != 4 ||
|
||||
!validTraceID(traceID) || !lowerHex(parentSpanID, 16) || allZero(parentSpanID) || !lowerHex(flags, 2) {
|
||||
return "", "", false
|
||||
}
|
||||
return traceID, parentSpanID, true
|
||||
}
|
||||
|
||||
func saneHeaderID(value string) bool {
|
||||
if value == "" || len(value) > 64 {
|
||||
return false
|
||||
}
|
||||
for index := 0; index < len(value); index++ {
|
||||
if value[index] <= 0x20 || value[index] > 0x7e {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validTraceID(value string) bool { return lowerHex(value, 32) && !allZero(value) }
|
||||
|
||||
func lowerHex(value string, length int) bool {
|
||||
if len(value) != length {
|
||||
return false
|
||||
}
|
||||
for index := 0; index < len(value); index++ {
|
||||
if current := value[index]; current < '0' || current > '9' {
|
||||
if current < 'a' || current > 'f' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func allZero(value string) bool {
|
||||
for index := 0; index < len(value); index++ {
|
||||
if value[index] != '0' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user