Files
loggerx/format.go
T
2026-09-13 21:53:37 +08:00

159 lines
3.9 KiB
Go

package loggerx
import (
"bytes"
"context"
"encoding/json"
"fmt"
"path/filepath"
"runtime"
"strconv"
"strings"
"time"
)
// basePath 进程工作目录(用于把绝对路径裁成相对路径)
// 只算一次并缓存:filepath.Abs 每次都要走系统调用,而它每条日志都会被用到
func (l *Logger) basePath() string {
l.basePathOnce.Do(func() {
p, err := filepath.Abs("./")
if err != nil {
p = ""
}
l.basePathVal = strings.ReplaceAll(p, "\\", "/")
})
return l.basePathVal
}
func (l *Logger) logger(ctx context.Context, event string, v ...any) {
// 调用方可能是 log 包(Logger.Write -> logger),所以这里取第 2 层
pc, file, line, ok := runtime.Caller(2)
var funcName string
if ok && pc != 0 {
if fn := runtime.FuncForPC(pc); fn != nil {
funcName = strings.TrimPrefix(filepath.Ext(fn.Name()), ".")
}
file = strings.TrimPrefix(strings.ReplaceAll(file, "\\", "/"), l.basePath())
}
nowTime := time.Now().In(l.option.timeZone).Format("2006-01-02 15:04:05.000000")
// ctx 允许为 nil,不能直接调 Value
var traceId string
if ctx != nil {
traceId, _ = ctx.Value(l.option.traceField).(string)
}
// error 转成带堆栈的字符串(%+v 会带上 pkg/errors 的调用栈)
for idx, val := range v {
if _, isErr := val.(error); isErr {
v[idx] = fmt.Sprintf("%+v", val)
}
}
var gid string
if l.option.isGid {
gid = getGID()
}
fd := FormatData{
Time: nowTime,
File: file + ":" + strconv.Itoa(line),
Func: funcName,
Gid: gid,
Content: v,
TraceId: traceId,
Expand: l.option.expandData,
}
fdb := marshalLog(fd, nowTime, file, line, l.option.escapeHTML, gid, traceId)
fdb = append([]byte("\n["+event+"]"), fdb...)
_, _ = l.write(event, fdb)
if l.option.errorToInfo && event == "error" {
_, _ = l.write("info", fdb)
}
}
// marshalLog 序列化日志内容
// json 序列化失败时(chan/func 等不可序列化类型,或循环引用)不能静默丢弃,
// 否则只会留下一行没有内容的 "["+event+"]",需要降级成可读的文本
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)
} else {
var buf bytes.Buffer
encoder := json.NewEncoder(&buf)
encoder.SetEscapeHTML(false)
err = encoder.Encode(fd)
b = bytes.TrimRight(buf.Bytes(), "\n")
}
if err == nil {
return b
}
// 降级:逐字段尝试,失败的内容用 %+v 兜底
fallback := FormatData{
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 {
return fb
}
return []byte(fmt.Sprintf(`{"time":%q,"content":%q,"marshal_error":%q}`,
nowTime, fmt.Sprintf("%+v", fd.Content), err.Error()))
}
// 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
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])
}
type FormatData struct {
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"`
Stack string `json:"stack,omitempty"`
Expand map[string]string `json:"expand,omitempty"`
}