2026-05-05 21:45:19 +08:00
|
|
|
package log
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"reflect"
|
|
|
|
|
"sort"
|
|
|
|
|
"strings"
|
|
|
|
|
"sync"
|
2026-05-05 22:52:55 +08:00
|
|
|
|
2026-05-05 23:47:07 +08:00
|
|
|
"apigo.cc/go/cast"
|
2026-05-05 22:52:55 +08:00
|
|
|
"apigo.cc/go/file"
|
2026-05-05 21:45:19 +08:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// MetaField describes the serialization and visualization metadata for a single log field.
|
|
|
|
|
type MetaField struct {
|
|
|
|
|
Index int `json:"index"`
|
|
|
|
|
Name string `json:"name"`
|
|
|
|
|
Color string `json:"color,omitempty"`
|
|
|
|
|
Format string `json:"format,omitempty"`
|
|
|
|
|
WithoutKey bool `json:"withoutKey,omitempty"`
|
|
|
|
|
Hide bool `json:"hide,omitempty"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
metaRegistry = make(map[string][]MetaField)
|
|
|
|
|
metaLock sync.RWMutex
|
|
|
|
|
metaFilePath = ".log.meta.json"
|
|
|
|
|
)
|
|
|
|
|
|
2026-05-05 22:52:55 +08:00
|
|
|
// LoadMeta loads metadata from the specified file into the global registry.
|
|
|
|
|
func LoadMeta(path string) error {
|
|
|
|
|
metaLock.Lock()
|
|
|
|
|
defer metaLock.Unlock()
|
|
|
|
|
return file.UnmarshalFile(path, &metaRegistry)
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 21:45:19 +08:00
|
|
|
// RegisterType registers a log model's metadata into the global registry.
|
|
|
|
|
// logType is the string identifier (e.g. "info", "error").
|
|
|
|
|
func RegisterType(logType string, model any) {
|
2026-05-09 14:44:41 +08:00
|
|
|
t := reflect.TypeOf(model)
|
|
|
|
|
if t.Kind() == reflect.Ptr {
|
|
|
|
|
t = t.Elem()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 强制检查 Reset 方法是否被显式实现(防止继承 BaseLog 后忘记重置业务字段)
|
|
|
|
|
if t.Kind() == reflect.Struct {
|
|
|
|
|
ptrType := reflect.PointerTo(t)
|
|
|
|
|
method, ok := ptrType.MethodByName("Reset")
|
|
|
|
|
if !ok {
|
|
|
|
|
panic("log model must implement Reset() method: " + t.Name())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查该方法是否属于当前类型(而不是继承自 BaseLog 且没有被重写)
|
|
|
|
|
baseResetMethod, _ := reflect.PointerTo(reflect.TypeOf(BaseLog{})).MethodByName("Reset")
|
|
|
|
|
if method.Func.Pointer() == baseResetMethod.Func.Pointer() {
|
|
|
|
|
panic("log model must override Reset() method to clear its own fields: " + t.Name())
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 21:45:19 +08:00
|
|
|
fields := extractMetaFields(model)
|
|
|
|
|
|
|
|
|
|
metaLock.Lock()
|
|
|
|
|
metaRegistry[logType] = fields
|
|
|
|
|
metaLock.Unlock()
|
|
|
|
|
|
|
|
|
|
syncMetaFile()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetMeta returns the metadata fields for a given logType.
|
|
|
|
|
func GetMeta(logType string) []MetaField {
|
|
|
|
|
metaLock.RLock()
|
|
|
|
|
defer metaLock.RUnlock()
|
|
|
|
|
return metaRegistry[logType]
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 14:44:41 +08:00
|
|
|
// fieldInfo is used internally for storing fields with their absolute position.
|
2026-05-05 21:45:19 +08:00
|
|
|
type fieldInfo struct {
|
|
|
|
|
field reflect.StructField
|
|
|
|
|
pos int
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractMetaFields(model any) []MetaField {
|
|
|
|
|
t := reflect.TypeOf(model)
|
|
|
|
|
if t.Kind() == reflect.Ptr {
|
|
|
|
|
t = t.Elem()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if t.Kind() != reflect.Struct {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 14:44:41 +08:00
|
|
|
var flatFields []fieldInfo
|
2026-05-05 21:45:19 +08:00
|
|
|
flattenStructFields(t, &flatFields, nil)
|
|
|
|
|
|
2026-05-09 14:44:41 +08:00
|
|
|
// Determine final indices
|
|
|
|
|
maxLiteralPos := -1
|
|
|
|
|
var highPosFields []fieldInfo
|
2026-05-05 21:45:19 +08:00
|
|
|
for _, f := range flatFields {
|
2026-05-09 14:44:41 +08:00
|
|
|
if f.pos < 1000 {
|
|
|
|
|
if f.pos > maxLiteralPos {
|
|
|
|
|
maxLiteralPos = f.pos
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
highPosFields = append(highPosFields, f)
|
2026-05-05 21:45:19 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 14:44:41 +08:00
|
|
|
// Sort high pos fields by their pos
|
|
|
|
|
sort.Slice(highPosFields, func(i, j int) bool {
|
|
|
|
|
return highPosFields[i].pos < highPosFields[j].pos
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Assign real indices to high pos fields
|
|
|
|
|
finalPosMap := make(map[string]int)
|
|
|
|
|
for _, f := range flatFields {
|
|
|
|
|
if f.pos < 1000 {
|
|
|
|
|
finalPosMap[f.field.Name] = f.pos
|
|
|
|
|
}
|
2026-05-05 21:45:19 +08:00
|
|
|
}
|
2026-05-09 14:44:41 +08:00
|
|
|
nextPos := maxLiteralPos + 1
|
|
|
|
|
for _, f := range highPosFields {
|
|
|
|
|
finalPosMap[f.field.Name] = nextPos
|
|
|
|
|
nextPos++
|
2026-05-05 21:45:19 +08:00
|
|
|
}
|
|
|
|
|
|
2026-05-09 14:44:41 +08:00
|
|
|
maxPos := nextPos - 1
|
|
|
|
|
metaFields := make([]MetaField, maxPos+1)
|
|
|
|
|
// Initialize with empty MetaFields having Index set
|
|
|
|
|
for i := range metaFields {
|
|
|
|
|
metaFields[i] = MetaField{Index: i}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, f := range flatFields {
|
|
|
|
|
tag := f.field.Tag.Get("log")
|
2026-05-05 21:45:19 +08:00
|
|
|
if tag == "-" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 14:44:41 +08:00
|
|
|
realPos := finalPosMap[f.field.Name]
|
2026-05-05 21:45:19 +08:00
|
|
|
meta := MetaField{
|
2026-05-09 14:44:41 +08:00
|
|
|
Index: realPos,
|
|
|
|
|
Name: f.field.Name,
|
2026-05-05 21:45:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if tag != "" {
|
|
|
|
|
parts := strings.Split(tag, ",")
|
|
|
|
|
for _, part := range parts {
|
|
|
|
|
kv := strings.SplitN(part, ":", 2)
|
|
|
|
|
if len(kv) == 2 {
|
|
|
|
|
key := strings.TrimSpace(kv[0])
|
|
|
|
|
val := strings.TrimSpace(kv[1])
|
|
|
|
|
switch key {
|
|
|
|
|
case "color":
|
|
|
|
|
meta.Color = val
|
|
|
|
|
case "format":
|
|
|
|
|
meta.Format = val
|
|
|
|
|
case "withoutkey":
|
|
|
|
|
meta.WithoutKey = (val == "true")
|
|
|
|
|
case "hide":
|
|
|
|
|
meta.Hide = (val == "true")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// Apply some default visual rules if not specified
|
|
|
|
|
// LogType shouldn't show the key in standard console
|
2026-05-09 14:44:41 +08:00
|
|
|
if f.field.Name == "LogType" && meta.Color == "" {
|
2026-05-05 21:45:19 +08:00
|
|
|
meta.WithoutKey = true
|
|
|
|
|
}
|
2026-05-09 14:44:41 +08:00
|
|
|
|
|
|
|
|
metaFields[realPos] = meta
|
2026-05-05 21:45:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return metaFields
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 14:44:41 +08:00
|
|
|
func flattenStructFields(t reflect.Type, result *[]fieldInfo, parentIndex []int) {
|
2026-05-05 21:45:19 +08:00
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
|
|
|
f := t.Field(i)
|
|
|
|
|
if !f.IsExported() && !f.Anonymous {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 14:44:41 +08:00
|
|
|
pos := 10 + i // default position if not specified
|
2026-05-05 21:45:19 +08:00
|
|
|
tag := f.Tag.Get("log")
|
|
|
|
|
if tag != "" {
|
|
|
|
|
parts := strings.Split(tag, ",")
|
|
|
|
|
for _, part := range parts {
|
|
|
|
|
kv := strings.SplitN(part, ":", 2)
|
|
|
|
|
if len(kv) == 2 && strings.TrimSpace(kv[0]) == "pos" {
|
2026-05-09 14:44:41 +08:00
|
|
|
if p := cast.To[int](strings.TrimSpace(kv[1])); p >= 0 {
|
2026-05-05 21:45:19 +08:00
|
|
|
pos = p
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Compute the full index path from the root
|
|
|
|
|
fullIndex := make([]int, len(parentIndex), len(parentIndex)+1)
|
|
|
|
|
copy(fullIndex, parentIndex)
|
|
|
|
|
fullIndex = append(fullIndex, i)
|
|
|
|
|
f.Index = fullIndex
|
|
|
|
|
|
2026-05-09 14:44:41 +08:00
|
|
|
if f.Anonymous && f.Type.Kind() == reflect.Struct {
|
|
|
|
|
flattenStructFields(f.Type, result, f.Index)
|
2026-05-05 21:45:19 +08:00
|
|
|
} else {
|
2026-05-09 14:44:41 +08:00
|
|
|
*result = append(*result, fieldInfo{
|
|
|
|
|
field: f,
|
|
|
|
|
pos: pos,
|
|
|
|
|
})
|
2026-05-05 21:45:19 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func syncMetaFile() {
|
|
|
|
|
metaLock.RLock()
|
2026-05-05 22:52:55 +08:00
|
|
|
defer metaLock.RUnlock()
|
|
|
|
|
_ = file.MarshalFilePretty(metaFilePath, metaRegistry)
|
2026-05-05 21:45:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetMetaFilePath allows changing the path for testing or configuration purposes
|
|
|
|
|
func SetMetaFilePath(path string) {
|
|
|
|
|
metaFilePath = path
|
|
|
|
|
}
|