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"` }