2026-05-05 21:45:19 +08:00
|
|
|
package log
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"bytes"
|
|
|
|
|
"reflect"
|
2026-05-09 14:44:41 +08:00
|
|
|
"sort"
|
2026-05-05 21:45:19 +08:00
|
|
|
"strconv"
|
|
|
|
|
|
|
|
|
|
"apigo.cc/go/cast"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type fieldAccessor struct {
|
|
|
|
|
indexPath []int
|
|
|
|
|
name string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var (
|
|
|
|
|
accessorsCache = make(map[string][]fieldAccessor)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// getAccessors caches the reflection index paths for the flattened fields.
|
|
|
|
|
func getAccessors(logType string, model any) []fieldAccessor {
|
|
|
|
|
metaLock.RLock()
|
|
|
|
|
if acc, ok := accessorsCache[logType]; ok {
|
|
|
|
|
metaLock.RUnlock()
|
|
|
|
|
return acc
|
|
|
|
|
}
|
|
|
|
|
metaLock.RUnlock()
|
|
|
|
|
|
|
|
|
|
metaLock.Lock()
|
|
|
|
|
defer metaLock.Unlock()
|
|
|
|
|
|
|
|
|
|
// Double check
|
|
|
|
|
if acc, ok := accessorsCache[logType]; ok {
|
|
|
|
|
return acc
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
t := reflect.TypeOf(model)
|
|
|
|
|
if t.Kind() == reflect.Ptr {
|
|
|
|
|
t = t.Elem()
|
|
|
|
|
}
|
|
|
|
|
|
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 (must match meta.go)
|
|
|
|
|
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
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
accessors := make([]fieldAccessor, maxPos+1)
|
|
|
|
|
for _, f := range flatFields {
|
|
|
|
|
if f.field.Tag.Get("log") == "-" {
|
2026-05-05 21:45:19 +08:00
|
|
|
continue
|
|
|
|
|
}
|
2026-05-09 14:44:41 +08:00
|
|
|
realPos := finalPosMap[f.field.Name]
|
|
|
|
|
accessors[realPos] = fieldAccessor{
|
|
|
|
|
indexPath: f.field.Index,
|
|
|
|
|
name: f.field.Name,
|
|
|
|
|
}
|
2026-05-05 21:45:19 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
accessorsCache[logType] = accessors
|
|
|
|
|
return accessors
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func ToArrayBytes(entry LogEntry, sensitiveKeys []string) []byte {
|
|
|
|
|
var buf bytes.Buffer
|
|
|
|
|
buf.WriteByte('[')
|
|
|
|
|
|
|
|
|
|
base := entry.GetBaseLog()
|
|
|
|
|
if base == nil {
|
|
|
|
|
buf.WriteByte(']')
|
|
|
|
|
return buf.Bytes()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logType := base.LogType
|
|
|
|
|
if logType == "" {
|
|
|
|
|
// Fallback for undefined types
|
|
|
|
|
logType = "undefined"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
accessors := getAccessors(logType, entry)
|
|
|
|
|
v := reflect.ValueOf(entry)
|
|
|
|
|
if v.Kind() == reflect.Ptr {
|
|
|
|
|
v = v.Elem()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for i, acc := range accessors {
|
|
|
|
|
if i > 0 {
|
|
|
|
|
buf.WriteByte(',')
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-09 14:44:41 +08:00
|
|
|
if acc.indexPath == nil {
|
|
|
|
|
buf.WriteByte('0')
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 21:45:19 +08:00
|
|
|
fv := v.FieldByIndex(acc.indexPath)
|
|
|
|
|
writeValue(&buf, fv, acc.name, sensitiveKeys)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
buf.WriteByte(']')
|
|
|
|
|
return buf.Bytes()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeValue(buf *bytes.Buffer, v reflect.Value, fieldName string, sensitiveKeys []string) {
|
|
|
|
|
if !v.IsValid() {
|
|
|
|
|
buf.WriteString("null")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 23:25:17 +08:00
|
|
|
// Check if this root field should be desensitized
|
2026-05-05 23:09:46 +08:00
|
|
|
if len(sensitiveKeys) > 0 {
|
|
|
|
|
fixedName := fixField(fieldName)
|
|
|
|
|
for _, sk := range sensitiveKeys {
|
|
|
|
|
if sk == fixedName {
|
2026-05-05 23:32:43 +08:00
|
|
|
buf.WriteString(`"***"`)
|
2026-05-05 23:25:17 +08:00
|
|
|
return
|
2026-05-05 23:09:46 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-05 21:45:19 +08:00
|
|
|
switch v.Kind() {
|
|
|
|
|
case reflect.String:
|
|
|
|
|
writeString(buf, v.String())
|
|
|
|
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
|
|
|
|
buf.WriteString(strconv.FormatInt(v.Int(), 10))
|
|
|
|
|
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
|
|
|
|
buf.WriteString(strconv.FormatUint(v.Uint(), 10))
|
|
|
|
|
case reflect.Float32, reflect.Float64:
|
|
|
|
|
buf.WriteString(strconv.FormatFloat(v.Float(), 'g', -1, 64))
|
|
|
|
|
case reflect.Bool:
|
|
|
|
|
if v.Bool() {
|
|
|
|
|
buf.WriteString("true")
|
|
|
|
|
} else {
|
|
|
|
|
buf.WriteString("false")
|
|
|
|
|
}
|
|
|
|
|
default:
|
2026-05-05 23:25:17 +08:00
|
|
|
// Use cast for complex types to ensure deep desensitization
|
|
|
|
|
b, _ := cast.ToJSONDesensitizeBytes(v.Interface(), sensitiveKeys)
|
|
|
|
|
if len(b) == 0 {
|
2026-05-05 21:45:19 +08:00
|
|
|
buf.WriteString("null")
|
2026-05-05 23:25:17 +08:00
|
|
|
} else {
|
|
|
|
|
buf.Write(b)
|
2026-05-05 21:45:19 +08:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeString(buf *bytes.Buffer, s string) {
|
|
|
|
|
buf.WriteByte('"')
|
|
|
|
|
for i := 0; i < len(s); i++ {
|
|
|
|
|
c := s[i]
|
|
|
|
|
switch c {
|
|
|
|
|
case '\\':
|
|
|
|
|
buf.WriteString(`\\`)
|
|
|
|
|
case '"':
|
|
|
|
|
buf.WriteString(`\"`)
|
|
|
|
|
case '\n':
|
|
|
|
|
buf.WriteString(`\n`)
|
|
|
|
|
case '\r':
|
|
|
|
|
buf.WriteString(`\r`)
|
|
|
|
|
case '\t':
|
|
|
|
|
buf.WriteString(`\t`)
|
|
|
|
|
default:
|
|
|
|
|
buf.WriteByte(c)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
buf.WriteByte('"')
|
|
|
|
|
}
|