Files
loggerx/format.go
T

334 lines
8.7 KiB
Go
Raw Normal View History

2024-01-22 18:05:51 +08:00
package loggerx
import (
2025-01-15 15:29:45 +08:00
"bytes"
2024-01-22 18:05:51 +08:00
"context"
"encoding/json"
"fmt"
"path/filepath"
"runtime"
2026-09-14 00:14:30 +08:00
"sort"
2026-09-13 20:41:27 +08:00
"strconv"
2024-01-22 18:05:51 +08:00
"strings"
"time"
)
2026-09-14 00:14:30 +08:00
// resolveBasePath 计算进程工作目录(用于把绝对路径裁成相对路径)
// 只在 NewLogger 里算一次:filepath.Abs 要走系统调用,而每条日志都会用到
func resolveBasePath() string {
p, err := filepath.Abs("./")
if err != nil {
return ""
}
return strings.ReplaceAll(p, "\\", "/")
}
// basePath 进程工作目录
// 值放在共享指针里,Channel()/WriteAsync() 拷贝出的实例才能看到同一个结果
2026-09-13 21:53:37 +08:00
func (l *Logger) basePath() string {
2026-09-14 00:14:30 +08:00
if l.basePathVal == nil {
return ""
}
return *l.basePathVal
}
// levelOf 事件名 -> 级别
func levelOf(event string) Level {
switch event {
case "debug":
return LevelDebug
case "warn":
return LevelWarn
case "error":
return LevelError
default:
return LevelInfo
}
2026-09-13 21:53:37 +08:00
}
2024-01-23 00:12:08 +08:00
func (l *Logger) logger(ctx context.Context, event string, v ...any) {
2026-09-14 00:14:30 +08:00
// 级别过滤:低于设定级别的日志直接丢弃,连 JSON 都不序列化
if levelOf(event) < l.option.minLevel {
return
}
2026-09-13 20:41:27 +08:00
// 调用方可能是 log 包(Logger.Write -> logger),所以这里取第 2 层
pc, file, line, ok := runtime.Caller(2)
2024-01-22 18:05:51 +08:00
2026-09-13 20:41:27 +08:00
var funcName string
if ok && pc != 0 {
if fn := runtime.FuncForPC(pc); fn != nil {
funcName = strings.TrimPrefix(filepath.Ext(fn.Name()), ".")
}
2026-09-13 21:53:37 +08:00
file = strings.TrimPrefix(strings.ReplaceAll(file, "\\", "/"), l.basePath())
2026-09-13 20:41:27 +08:00
}
2024-01-22 18:05:51 +08:00
nowTime := time.Now().In(l.option.timeZone).Format("2006-01-02 15:04:05.000000")
2024-01-22 18:05:51 +08:00
2026-09-13 20:41:27 +08:00
// ctx 允许为 nil,不能直接调 Value
var traceId string
if ctx != nil {
traceId, _ = ctx.Value(l.option.traceField).(string)
}
2024-01-22 18:05:51 +08:00
2026-09-13 20:41:27 +08:00
// error 转成带堆栈的字符串(%+v 会带上 pkg/errors 的调用栈)
2026-09-14 00:14:30 +08:00
// 注意:这里会写进调用方传入的切片,因此先复制一份,不改调用方的内存
args := v
for idx, val := range args {
2026-09-13 20:41:27 +08:00
if _, isErr := val.(error); isErr {
2026-09-14 00:14:30 +08:00
if &args[0] == &v[0] {
args = append([]any(nil), v...)
}
args[idx] = fmt.Sprintf("%+v", val)
2024-07-26 17:52:32 +08:00
}
}
2026-09-13 20:41:27 +08:00
var gid string
if l.option.isGid {
gid = getGID()
}
2024-02-03 01:57:56 +08:00
fd := FormatData{
2026-09-14 00:14:30 +08:00
Level: event,
2024-02-03 01:57:56 +08:00
Time: nowTime,
2026-09-13 20:41:27 +08:00
File: file + ":" + strconv.Itoa(line),
2024-02-03 01:57:56 +08:00
Func: funcName,
2026-09-13 20:41:27 +08:00
Gid: gid,
2026-09-14 00:14:30 +08:00
Content: args,
2024-02-03 01:57:56 +08:00
TraceId: traceId,
2026-09-13 20:41:27 +08:00
Expand: l.option.expandData,
2024-02-03 01:57:56 +08:00
}
2024-04-18 12:02:44 +08:00
2026-09-14 00:14:30 +08:00
line1 := l.marshalLine(fd, nowTime, file, line, gid, traceId)
2024-04-18 12:02:44 +08:00
2026-09-14 00:14:30 +08:00
// 一条日志 = 一行完整合法的 JSON(+ 换行),
// 这样 Fluent Bit / Vector / Loki / jq 等采集端可以直接解析。
// 旧实现写成 "\n[info]{...}":既不是 JSON 行,也没有行尾换行
if _, err := l.write(event, line1); err != nil {
l.reportError(fmt.Errorf("loggerx: 写入日志失败: %w", err))
}
2026-09-13 20:41:27 +08:00
if l.option.errorToInfo && event == "error" {
2026-09-14 00:14:30 +08:00
if _, err := l.write("info", line1); err != nil {
l.reportError(fmt.Errorf("loggerx: 写入 info 日志失败: %w", err))
}
2026-09-13 20:41:27 +08:00
}
}
2026-09-14 00:14:30 +08:00
// marshalLine 按当前配置(格式 + 前缀)渲染一条日志,返回以换行结尾的一行
//
// 前缀/换行都 append 进目标切片,不额外分配一个容器再拷贝:
// 这条路径每条日志都走,多一次分配就会体现在吞吐上
func (l *Logger) marshalLine(fd FormatData, nowTime, file string, line int, gid, traceId string) []byte {
prefix := l.option.prefix
var body []byte
if l.option.format == FormatText {
body = marshalText(fd)
} else {
body = marshalLog(fd, nowTime, file, line, l.option.escapeHTML, gid, traceId)
body = bytes.TrimRight(body, "\n")
}
out := make([]byte, 0, len(prefix)+len(body)+1)
out = append(out, prefix...)
out = append(out, body...)
return append(out, '\n')
}
// marshalText 把一条日志渲染成单行紧凑文本
// 形如:level=info time=2026-01-02 15:04:05.000000 file=/a.go:12 func=main gid=7 content=hi
// 值里含空格/引号/换行时用 %q 包起来,保证「一行一条」且不歧义
//
// 用 append 到 []byte 而不是 strings.Builder + Sprintf
// 文本格式的意义就是便宜,走反射/格式化会把省下的 JSON 成本又花回去
func marshalText(fd FormatData) []byte {
var buf []byte
buf = append(buf, "level="...)
buf = append(buf, fd.Level...)
buf = appendTextField(buf, "time", fd.Time)
buf = appendTextField(buf, "file", fd.File)
if fd.Func != "" {
buf = appendTextField(buf, "func", fd.Func)
}
if fd.Gid != "" {
buf = appendTextField(buf, "gid", fd.Gid)
}
if fd.TraceId != "" {
buf = appendTextField(buf, "traceId", fd.TraceId)
}
buf = append(buf, " content="...)
buf = appendContent(buf, fd.Content)
for _, k := range sortedKeys(fd.Expand) {
buf = appendTextField(buf, k, fd.Expand[k])
}
return buf
}
// appendTextField 追加一个 key=value 字段,必要时加引号
func appendTextField(buf []byte, key, val string) []byte {
buf = append(buf, ' ')
buf = append(buf, key...)
buf = append(buf, '=')
if val == "" {
return append(buf, `""`...)
}
if strings.ContainsAny(val, " \t\"'=\n\r") {
return strconv.AppendQuote(buf, val)
}
return append(buf, val...)
}
// appendContent 渲染 content(多个参数用空格分隔)
// 单个值走快速路径,避免 fmt 的反射开销
func appendContent(buf []byte, v any) []byte {
switch val := v.(type) {
case nil:
return append(buf, `""`...)
case []any:
for i, item := range val {
if i > 0 {
buf = append(buf, ' ')
}
buf = appendValue(buf, item)
}
if len(val) == 0 {
buf = append(buf, `""`...)
}
return buf
case []string:
if len(val) == 0 {
return append(buf, `""`...)
}
return append(buf, strings.Join(val, " ")...)
case string:
return appendValue(buf, val)
default:
return appendValue(buf, val)
}
}
// appendValue 渲染单个值,需要时加引号
func appendValue(buf []byte, v any) []byte {
var s string
switch val := v.(type) {
case string:
s = val
case error:
s = val.Error()
case fmt.Stringer:
s = val.String()
case int:
return strconv.AppendInt(buf, int64(val), 10)
case int64:
return strconv.AppendInt(buf, val, 10)
case bool:
return strconv.AppendBool(buf, val)
default:
s = fmt.Sprintf("%v", v)
}
if s == "" {
return append(buf, `""`...)
}
if strings.ContainsAny(s, " \t\"'=\n\r") {
return strconv.AppendQuote(buf, s)
}
return append(buf, s...)
}
// sortedKeys 让 map 输出顺序稳定,便于 diff 与测试
func sortedKeys(m map[string]string) []string {
if len(m) == 0 {
return nil
}
keys := make([]string, 0, len(m))
for k := range m {
keys = append(keys, k)
}
sort.Strings(keys)
return keys
}
// marshalLog 序列化一条日志为 JSON(返回以换行结尾)
//
2026-09-13 20:41:27 +08:00
// json 序列化失败时(chan/func 等不可序列化类型,或循环引用)不能静默丢弃,
2026-09-14 00:14:30 +08:00
// 否则会留下一行没有内容的坏记录,需要降级成可读文本
2026-09-13 20:41:27 +08:00
func marshalLog(fd FormatData, nowTime, file string, line int, escapeHTML bool, gid, traceId string) []byte {
var (
b []byte
err error
)
if escapeHTML {
b, err = json.Marshal(fd)
2025-01-15 15:29:45 +08:00
} else {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
2026-09-13 20:41:27 +08:00
err = encoder.Encode(fd)
b = bytes.TrimRight(buf.Bytes(), "\n")
2025-01-15 15:29:45 +08:00
}
2026-09-13 20:41:27 +08:00
if err == nil {
2026-09-14 00:14:30 +08:00
return append(b, '\n')
2024-01-23 00:12:08 +08:00
}
2024-01-22 18:05:51 +08:00
2026-09-13 20:41:27 +08:00
// 降级:逐字段尝试,失败的内容用 %+v 兜底
fallback := FormatData{
2026-09-14 00:14:30 +08:00
Level: fd.Level,
2026-09-13 20:41:27 +08:00
Time: nowTime,
File: file + ":" + strconv.Itoa(line),
Func: fd.Func,
Gid: gid,
TraceId: traceId,
Content: stringifyValues(fd.Content),
Expand: fd.Expand,
}
if fb, ferr := json.Marshal(fallback); ferr == nil {
2026-09-14 00:14:30 +08:00
return append(fb, '\n')
2026-09-13 20:41:27 +08:00
}
2026-09-14 00:14:30 +08:00
return []byte(fmt.Sprintf("{\"level\":%q,\"time\":%q,\"content\":%q,\"marshal_error\":%q}\n",
fd.Level, nowTime, fmt.Sprintf("%+v", fd.Content), err.Error()))
2026-09-13 20:41:27 +08:00
}
// stringifyValues 把不可序列化的值转成字符串
func stringifyValues(v any) []string {
switch val := v.(type) {
case nil:
return nil
case []any:
out := make([]string, 0, len(val))
for _, item := range val {
out = append(out, fmt.Sprintf("%+v", item))
}
return out
2026-09-14 00:14:30 +08:00
case []string:
return val
2026-09-13 20:41:27 +08:00
default:
return []string{fmt.Sprintf("%+v", val)}
}
}
// getGID 获取当前 goroutine 的 ID
func getGID() string {
b := make([]byte, 64)
b = b[:runtime.Stack(b, false)]
b = bytes.TrimPrefix(b, []byte("goroutine "))
idx := bytes.IndexByte(b, ' ')
if idx < 0 {
// 解析失败时不要 panic
return "0"
}
return string(b[:idx])
2024-01-22 18:05:51 +08:00
}
2024-02-03 01:57:56 +08:00
2026-09-14 00:14:30 +08:00
// FormatData 一条日志的落盘结构
2024-02-03 01:57:56 +08:00
type FormatData struct {
2026-09-14 00:14:30 +08:00
Level string `json:"level,omitempty"`
2026-09-13 20:41:27 +08:00
Time string `json:"time,omitempty"`
File string `json:"file,omitempty"`
Func string `json:"func,omitempty"`
Gid string `json:"gid,omitempty"`
Content interface{} `json:"content,omitempty"`
TraceId string `json:"traceId,omitempty"`
Expand map[string]string `json:"expand,omitempty"`
2024-02-03 01:57:56 +08:00
}