更新
This commit is contained in:
@@ -7,25 +7,51 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// basePath 进程工作目录(用于把绝对路径裁成相对路径)
|
||||
// 只算一次并缓存:filepath.Abs 每次都要走系统调用,而它每条日志都会被用到
|
||||
// resolveBasePath 计算进程工作目录(用于把绝对路径裁成相对路径)
|
||||
// 只在 NewLogger 里算一次:filepath.Abs 要走系统调用,而每条日志都会用到
|
||||
func resolveBasePath() string {
|
||||
p, err := filepath.Abs("./")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return strings.ReplaceAll(p, "\\", "/")
|
||||
}
|
||||
|
||||
// basePath 进程工作目录
|
||||
// 值放在共享指针里,Channel()/WriteAsync() 拷贝出的实例才能看到同一个结果
|
||||
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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func (l *Logger) logger(ctx context.Context, event string, v ...any) {
|
||||
// 级别过滤:低于设定级别的日志直接丢弃,连 JSON 都不序列化
|
||||
if levelOf(event) < l.option.minLevel {
|
||||
return
|
||||
}
|
||||
|
||||
// 调用方可能是 log 包(Logger.Write -> logger),所以这里取第 2 层
|
||||
pc, file, line, ok := runtime.Caller(2)
|
||||
|
||||
@@ -46,9 +72,14 @@ func (l *Logger) logger(ctx context.Context, event string, v ...any) {
|
||||
}
|
||||
|
||||
// error 转成带堆栈的字符串(%+v 会带上 pkg/errors 的调用栈)
|
||||
for idx, val := range v {
|
||||
// 注意:这里会写进调用方传入的切片,因此先复制一份,不改调用方的内存
|
||||
args := v
|
||||
for idx, val := range args {
|
||||
if _, isErr := val.(error); isErr {
|
||||
v[idx] = fmt.Sprintf("%+v", val)
|
||||
if &args[0] == &v[0] {
|
||||
args = append([]any(nil), v...)
|
||||
}
|
||||
args[idx] = fmt.Sprintf("%+v", val)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,29 +89,169 @@ func (l *Logger) logger(ctx context.Context, event string, v ...any) {
|
||||
}
|
||||
|
||||
fd := FormatData{
|
||||
Level: event,
|
||||
Time: nowTime,
|
||||
File: file + ":" + strconv.Itoa(line),
|
||||
Func: funcName,
|
||||
Gid: gid,
|
||||
Content: v,
|
||||
Content: args,
|
||||
TraceId: traceId,
|
||||
Expand: l.option.expandData,
|
||||
}
|
||||
|
||||
fdb := marshalLog(fd, nowTime, file, line, l.option.escapeHTML, gid, traceId)
|
||||
line1 := l.marshalLine(fd, nowTime, file, line, gid, traceId)
|
||||
|
||||
fdb = append([]byte("\n["+event+"]"), fdb...)
|
||||
|
||||
_, _ = l.write(event, fdb)
|
||||
// 一条日志 = 一行完整合法的 JSON(+ 换行),
|
||||
// 这样 Fluent Bit / Vector / Loki / jq 等采集端可以直接解析。
|
||||
// 旧实现写成 "\n[info]{...}":既不是 JSON 行,也没有行尾换行
|
||||
if _, err := l.write(event, line1); err != nil {
|
||||
l.reportError(fmt.Errorf("loggerx: 写入日志失败: %w", err))
|
||||
}
|
||||
|
||||
if l.option.errorToInfo && event == "error" {
|
||||
_, _ = l.write("info", fdb)
|
||||
if _, err := l.write("info", line1); err != nil {
|
||||
l.reportError(fmt.Errorf("loggerx: 写入 info 日志失败: %w", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// marshalLog 序列化日志内容
|
||||
// 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(返回以换行结尾)
|
||||
//
|
||||
// json 序列化失败时(chan/func 等不可序列化类型,或循环引用)不能静默丢弃,
|
||||
// 否则只会留下一行没有内容的 "["+event+"]",需要降级成可读的文本
|
||||
// 否则会留下一行没有内容的坏记录,需要降级成可读文本
|
||||
func marshalLog(fd FormatData, nowTime, file string, line int, escapeHTML bool, gid, traceId string) []byte {
|
||||
var (
|
||||
b []byte
|
||||
@@ -96,11 +267,12 @@ func marshalLog(fd FormatData, nowTime, file string, line int, escapeHTML bool,
|
||||
b = bytes.TrimRight(buf.Bytes(), "\n")
|
||||
}
|
||||
if err == nil {
|
||||
return b
|
||||
return append(b, '\n')
|
||||
}
|
||||
|
||||
// 降级:逐字段尝试,失败的内容用 %+v 兜底
|
||||
fallback := FormatData{
|
||||
Level: fd.Level,
|
||||
Time: nowTime,
|
||||
File: file + ":" + strconv.Itoa(line),
|
||||
Func: fd.Func,
|
||||
@@ -110,11 +282,11 @@ func marshalLog(fd FormatData, nowTime, file string, line int, escapeHTML bool,
|
||||
Expand: fd.Expand,
|
||||
}
|
||||
if fb, ferr := json.Marshal(fallback); ferr == nil {
|
||||
return fb
|
||||
return append(fb, '\n')
|
||||
}
|
||||
|
||||
return []byte(fmt.Sprintf(`{"time":%q,"content":%q,"marshal_error":%q}`,
|
||||
nowTime, fmt.Sprintf("%+v", fd.Content), err.Error()))
|
||||
return []byte(fmt.Sprintf("{\"level\":%q,\"time\":%q,\"content\":%q,\"marshal_error\":%q}\n",
|
||||
fd.Level, nowTime, fmt.Sprintf("%+v", fd.Content), err.Error()))
|
||||
}
|
||||
|
||||
// stringifyValues 把不可序列化的值转成字符串
|
||||
@@ -128,6 +300,8 @@ func stringifyValues(v any) []string {
|
||||
out = append(out, fmt.Sprintf("%+v", item))
|
||||
}
|
||||
return out
|
||||
case []string:
|
||||
return val
|
||||
default:
|
||||
return []string{fmt.Sprintf("%+v", val)}
|
||||
}
|
||||
@@ -146,13 +320,14 @@ func getGID() string {
|
||||
return string(b[:idx])
|
||||
}
|
||||
|
||||
// FormatData 一条日志的落盘结构
|
||||
type FormatData struct {
|
||||
Level string `json:"level,omitempty"`
|
||||
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"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user