Files
yml2213 88d74aca7d 重构日志与可观测性体系
新增单行文本编码器与结构化 GORM 日志,统一错误记录与请求日志策略,收紧日志文件权限并修复按天切分与压缩,支付回调参数脱敏,生产强制阿里云短信,RequestID 校验防注入,日志文案中文化。
2026-07-29 16:19:35 +08:00

129 lines
3.0 KiB
Go

package logging
import (
"fmt"
"sort"
"strconv"
"strings"
"time"
"go.uber.org/zap/buffer"
"go.uber.org/zap/zapcore"
)
var textBufferPool = buffer.NewPool()
// textEncoder 把结构化字段输出为便于直接阅读和 grep 的单行文本。
type textEncoder struct {
*zapcore.MapObjectEncoder
location *time.Location
}
func newTextEncoder(location *time.Location) zapcore.Encoder {
return &textEncoder{
MapObjectEncoder: zapcore.NewMapObjectEncoder(),
location: location,
}
}
func (e *textEncoder) Clone() zapcore.Encoder {
cloned := &textEncoder{
MapObjectEncoder: zapcore.NewMapObjectEncoder(),
location: e.location,
}
for key, value := range e.Fields {
cloned.Fields[key] = value
}
return cloned
}
func (e *textEncoder) EncodeEntry(entry zapcore.Entry, fields []zapcore.Field) (*buffer.Buffer, error) {
ctx := e.Clone().(*textEncoder)
for i := range fields {
fields[i].AddTo(ctx)
}
buf := textBufferPool.Get()
buf.AppendString(entry.Time.In(e.location).Format("2006-01-02 15:04:05.000"))
buf.AppendString(" | ")
buf.AppendString(fmt.Sprintf("%-5s", strings.ToUpper(entry.Level.String())))
buf.AppendString(" | ")
buf.AppendString(cleanText(entry.Message))
if entry.Level >= zapcore.WarnLevel && entry.Caller.Defined {
buf.AppendString(" | caller=")
buf.AppendString(entry.Caller.TrimmedPath())
}
keys := orderedKeys(ctx.Fields)
for _, key := range keys {
value := ctx.Fields[key]
if isEmptyValue(value) {
continue
}
buf.AppendString(" | ")
buf.AppendString(key)
buf.AppendByte('=')
buf.AppendString(formatValue(value))
}
if entry.Stack != "" {
buf.AppendString(" | stack=")
buf.AppendString(strconv.Quote(entry.Stack))
}
buf.AppendByte('\n')
return buf, nil
}
func orderedKeys(fields map[string]any) []string {
priority := []string{
"module", "request_id", "method", "route", "path", "status", "code",
"duration_ms", "rows", "user_id", "admin_id", "client_ip", "error",
}
keys := make([]string, 0, len(fields))
seen := make(map[string]bool, len(fields))
for _, key := range priority {
if _, ok := fields[key]; ok {
keys = append(keys, key)
seen[key] = true
}
}
rest := make([]string, 0, len(fields)-len(keys))
for key := range fields {
if !seen[key] {
rest = append(rest, key)
}
}
sort.Strings(rest)
return append(keys, rest...)
}
func formatValue(value any) string {
switch typed := value.(type) {
case string:
if typed != "" && !strings.ContainsAny(typed, " \t\r\n|=\"") {
return typed
}
return strconv.Quote(typed)
case []byte:
return strconv.Quote(string(typed))
case time.Time:
return typed.Format(time.RFC3339)
case time.Duration:
return typed.String()
default:
return cleanText(fmt.Sprint(typed))
}
}
func cleanText(value string) string {
return strings.NewReplacer("\r", `\r`, "\n", `\n`, "\t", `\t`).Replace(value)
}
func isEmptyValue(value any) bool {
if value == nil {
return true
}
text, ok := value.(string)
return ok && strings.TrimSpace(text) == ""
}