210 lines
4.9 KiB
Go
210 lines
4.9 KiB
Go
package log
|
|
|
|
import (
|
|
"reflect"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"apigo.cc/go/file"
|
|
)
|
|
|
|
// 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"
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
|
|
// 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) {
|
|
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]
|
|
}
|
|
|
|
// fieldInfo is used internally for sorting fields before flattening.
|
|
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
|
|
}
|
|
|
|
var flatFields []reflect.StructField
|
|
flattenStructFields(t, &flatFields, nil)
|
|
|
|
var metaFields []MetaField
|
|
var extraField *reflect.StructField
|
|
var callStacksField *reflect.StructField
|
|
|
|
// Process fields, separating Extra and CallStacks
|
|
var regularFields []reflect.StructField
|
|
for _, f := range flatFields {
|
|
if f.Name == "Extra" {
|
|
extraField = &f
|
|
continue
|
|
}
|
|
if f.Name == "CallStacks" {
|
|
callStacksField = &f
|
|
continue
|
|
}
|
|
regularFields = append(regularFields, f)
|
|
}
|
|
|
|
// Reassemble: regular fields -> CallStacks -> Extra
|
|
var finalFields []reflect.StructField
|
|
finalFields = append(finalFields, regularFields...)
|
|
if callStacksField != nil {
|
|
finalFields = append(finalFields, *callStacksField)
|
|
}
|
|
if extraField != nil {
|
|
finalFields = append(finalFields, *extraField)
|
|
}
|
|
|
|
for i, f := range finalFields {
|
|
tag := f.Tag.Get("log")
|
|
if tag == "-" {
|
|
continue
|
|
}
|
|
|
|
meta := MetaField{
|
|
Index: i,
|
|
Name: f.Name,
|
|
}
|
|
|
|
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
|
|
if f.Name == "LogType" && meta.Color == "" {
|
|
meta.WithoutKey = true
|
|
}
|
|
|
|
metaFields = append(metaFields, meta)
|
|
}
|
|
|
|
return metaFields
|
|
}
|
|
|
|
func flattenStructFields(t reflect.Type, result *[]reflect.StructField, parentIndex []int) {
|
|
var infos []fieldInfo
|
|
|
|
for i := 0; i < t.NumField(); i++ {
|
|
f := t.Field(i)
|
|
if !f.IsExported() && !f.Anonymous {
|
|
continue
|
|
}
|
|
|
|
isEmbeddedStruct := f.Anonymous && f.Type.Kind() == reflect.Struct
|
|
pos := 1000 + i // default position if not specified
|
|
if isEmbeddedStruct {
|
|
pos = i - 1000 // default to top priority for embedded structs
|
|
}
|
|
|
|
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" {
|
|
if p, err := strconv.Atoi(strings.TrimSpace(kv[1])); err == nil {
|
|
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
|
|
|
|
infos = append(infos, fieldInfo{
|
|
field: f,
|
|
pos: pos,
|
|
})
|
|
}
|
|
|
|
// Sort fields in the current struct level by pos
|
|
sort.Slice(infos, func(i, j int) bool {
|
|
return infos[i].pos < infos[j].pos
|
|
})
|
|
|
|
for _, info := range infos {
|
|
if info.field.Anonymous && info.field.Type.Kind() == reflect.Struct {
|
|
// Embedded struct, extract its fields first (parent first)
|
|
flattenStructFields(info.field.Type, result, info.field.Index)
|
|
} else {
|
|
*result = append(*result, info.field)
|
|
}
|
|
}
|
|
}
|
|
|
|
func syncMetaFile() {
|
|
metaLock.RLock()
|
|
defer metaLock.RUnlock()
|
|
_ = file.MarshalFilePretty(metaFilePath, metaRegistry)
|
|
}
|
|
|
|
// SetMetaFilePath allows changing the path for testing or configuration purposes
|
|
func SetMetaFilePath(path string) {
|
|
metaFilePath = path
|
|
}
|