重构日志与可观测性体系
新增单行文本编码器与结构化 GORM 日志,统一错误记录与请求日志策略,收紧日志文件权限并修复按天切分与压缩,支付回调参数脱敏,生产强制阿里云短信,RequestID 校验防注入,日志文案中文化。
This commit is contained in:
@@ -0,0 +1,128 @@
|
||||
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) == ""
|
||||
}
|
||||
Reference in New Issue
Block a user