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) == "" }