kra-new/internal/server/middleware/access_log.go

87 lines
2.8 KiB
Go

package middleware
import (
"bytes"
"io"
"log/slog"
"strings"
"time"
"kra/internal/conf"
"github.com/gin-gonic/gin"
)
// 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 {
return func(c *gin.Context) {
var requestBody []byte
multipart := strings.Contains(c.GetHeader("Content-Type"), "multipart/form-data")
if c.Request.Body != nil && !multipart {
requestBody, _ = io.ReadAll(c.Request.Body)
c.Request.Body = io.NopCloser(bytes.NewReader(requestBody))
}
maxBytes := 1 << 20
writer := &captureWriter{ResponseWriter: c.Writer, maxBytes: maxBytes}
c.Writer = writer
started := time.Now()
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)
}
userID, authorityID := uint(0), uint(0)
if claims := Claims(c); claims != nil {
userID, authorityID = claims.ID, claims.AuthorityID
}
route := c.FullPath()
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))
}
logger.InfoContext(c.Request.Context(), "http access", attributes...)
}
}
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{"******"}
} else {
out[key] = values
}
}
return out
}
func stringValueFromContext(c *gin.Context, key string) string {
value, _ := c.Get(key)
return stringValue(value)
}