package log import ( "bytes" "reflect" "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() } var flatFields []reflect.StructField flattenStructFields(t, &flatFields, nil) var extraField *reflect.StructField var callStacksField *reflect.StructField 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) } var finalFields []reflect.StructField finalFields = append(finalFields, regularFields...) if callStacksField != nil { finalFields = append(finalFields, *callStacksField) } if extraField != nil { finalFields = append(finalFields, *extraField) } var accessors []fieldAccessor for _, f := range finalFields { if f.Tag.Get("log") == "-" { continue } accessors = append(accessors, fieldAccessor{ indexPath: f.Index, name: f.Name, }) } 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(',') } 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 } // Check if this field should be desensitized isSensitive := false if len(sensitiveKeys) > 0 { fixedName := fixField(fieldName) for _, sk := range sensitiveKeys { if sk == fixedName { isSensitive = true break } } } if isSensitive { buf.WriteString(`"******"`) return } 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") } case reflect.Map: if v.IsNil() || v.Len() == 0 { buf.WriteString("{}") return } // Handle map with cast.ToJSON var b []byte if len(sensitiveKeys) > 0 { b, _ = cast.ToJSONDesensitizeBytes(v.Interface(), sensitiveKeys) } else { b, _ = cast.ToJSONBytes(v.Interface()) } if len(b) > 0 { buf.Write(b) } else { buf.WriteString("{}") } case reflect.Slice, reflect.Array: if v.IsNil() || v.Len() == 0 { buf.WriteString("[]") return } b, _ := cast.ToJSONBytes(v.Interface()) if len(b) > 0 { buf.Write(b) } else { buf.WriteString("[]") } default: // Fallback for other complex types b, _ := cast.ToJSONBytes(v.Interface()) if len(b) > 0 { buf.Write(b) } else { buf.WriteString("null") } } } 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('"') }