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-13 20:41:27 +08:00
|
|
|
"strconv"
|
2024-01-22 18:05:51 +08:00
|
|
|
"strings"
|
|
|
|
|
"time"
|
|
|
|
|
)
|
|
|
|
|
|
2024-01-23 00:12:08 +08:00
|
|
|
func (l *Logger) logger(ctx context.Context, event string, v ...any) {
|
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()), ".")
|
|
|
|
|
}
|
|
|
|
|
if basePath, err := filepath.Abs("./"); err == nil {
|
|
|
|
|
basePath = strings.ReplaceAll(basePath, "\\", "/")
|
|
|
|
|
file = strings.TrimPrefix(strings.ReplaceAll(file, "\\", "/"), basePath)
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-22 18:05:51 +08:00
|
|
|
|
2024-06-20 23:59:13 +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 的调用栈)
|
2025-01-15 15:29:45 +08:00
|
|
|
for idx, val := range v {
|
2026-09-13 20:41:27 +08:00
|
|
|
if _, isErr := val.(error); isErr {
|
2024-07-26 17:52:32 +08:00
|
|
|
v[idx] = fmt.Sprintf("%+v", val)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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{
|
|
|
|
|
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,
|
2024-06-27 21:06:47 +08:00
|
|
|
Content: v,
|
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-13 20:41:27 +08:00
|
|
|
fdb := marshalLog(fd, nowTime, file, line, l.option.escapeHTML, gid, traceId)
|
2024-04-18 12:02:44 +08:00
|
|
|
|
2026-09-13 20:41:27 +08:00
|
|
|
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)
|
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 {
|
|
|
|
|
return b
|
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{
|
|
|
|
|
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])
|
2024-01-22 18:05:51 +08:00
|
|
|
}
|
2024-02-03 01:57:56 +08:00
|
|
|
|
|
|
|
|
type FormatData struct {
|
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"`
|
|
|
|
|
Stack string `json:"stack,omitempty"`
|
|
|
|
|
Expand map[string]string `json:"expand,omitempty"`
|
2024-02-03 01:57:56 +08:00
|
|
|
}
|