package exerr import ( "encoding/hex" "encoding/json" "errors" "fmt" "github.com/rs/zerolog" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "gogs.mikescher.com/BlackForestBytes/goext/langext" "strconv" "strings" "time" ) // This is a buffed up map[string]any // we also save type information of the map-values // which allows us to deserialize them back into te correct types later type MetaMap map[string]MetaValue type metaDataType string const ( MDTString metaDataType = "String" MDTStringPtr metaDataType = "StringPtr" MDTInt metaDataType = "Int" MDTInt8 metaDataType = "Int8" MDTInt16 metaDataType = "Int16" MDTInt32 metaDataType = "Int32" MDTInt64 metaDataType = "Int64" MDTFloat32 metaDataType = "Float32" MDTFloat64 metaDataType = "Float64" MDTBool metaDataType = "Bool" MDTBytes metaDataType = "Bytes" MDTObjectID metaDataType = "ObjectID" MDTTime metaDataType = "Time" MDTDuration metaDataType = "Duration" MDTStringArray metaDataType = "StringArr" MDTIntArray metaDataType = "IntArr" MDTInt32Array metaDataType = "Int32Arr" MDTID metaDataType = "ID" MDTAny metaDataType = "Interface" MDTNil metaDataType = "Nil" MDTEnum metaDataType = "Enum" ) type MetaValue struct { DataType metaDataType `json:"dataType"` Value interface{} `json:"value"` } type metaValueSerialization struct { DataType metaDataType `bson:"dataType"` Value string `bson:"value"` Raw interface{} `bson:"raw"` } func (v MetaValue) SerializeValue() (string, error) { switch v.DataType { case MDTString: return v.Value.(string), nil case MDTID: return v.Value.(IDWrap).Serialize(), nil case MDTAny: return v.Value.(AnyWrap).Serialize(), nil case MDTStringPtr: if langext.IsNil(v.Value) { return "#", nil } r := v.Value.(*string) if r != nil { return "*" + *r, nil } else { return "#", nil } case MDTInt: return strconv.Itoa(v.Value.(int)), nil case MDTInt8: return strconv.FormatInt(int64(v.Value.(int8)), 10), nil case MDTInt16: return strconv.FormatInt(int64(v.Value.(int16)), 10), nil case MDTInt32: return strconv.FormatInt(int64(v.Value.(int32)), 10), nil case MDTInt64: return strconv.FormatInt(v.Value.(int64), 10), nil case MDTFloat32: return strconv.FormatFloat(float64(v.Value.(float32)), 'X', -1, 32), nil case MDTFloat64: return strconv.FormatFloat(v.Value.(float64), 'X', -1, 64), nil case MDTBool: if v.Value.(bool) { return "true", nil } else { return "false", nil } case MDTBytes: return hex.EncodeToString(v.Value.([]byte)), nil case MDTObjectID: return v.Value.(primitive.ObjectID).Hex(), nil case MDTTime: return strconv.FormatInt(v.Value.(time.Time).Unix(), 10) + "|" + strconv.FormatInt(int64(v.Value.(time.Time).Nanosecond()), 10), nil case MDTDuration: return v.Value.(time.Duration).String(), nil case MDTStringArray: if langext.IsNil(v.Value) { return "#", nil } r, err := json.Marshal(v.Value.([]string)) if err != nil { return "", err } return string(r), nil case MDTIntArray: if langext.IsNil(v.Value) { return "#", nil } r, err := json.Marshal(v.Value.([]int)) if err != nil { return "", err } return string(r), nil case MDTInt32Array: if langext.IsNil(v.Value) { return "#", nil } r, err := json.Marshal(v.Value.([]int32)) if err != nil { return "", err } return string(r), nil case MDTNil: return "", nil case MDTEnum: return v.Value.(EnumWrap).Serialize(), nil } return "", errors.New("Unknown type: " + string(v.DataType)) } func (v MetaValue) ShortString(lim int) string { switch v.DataType { case MDTString: r := strings.ReplaceAll(v.Value.(string), "\r", "") r = strings.ReplaceAll(r, "\n", "\\n") r = strings.ReplaceAll(r, "\t", "\\t") return langext.StrLimit(r, lim, "...") case MDTID: return v.Value.(IDWrap).String() case MDTAny: return v.Value.(AnyWrap).String() case MDTStringPtr: if langext.IsNil(v.Value) { return "<>" } r := langext.CoalesceString(v.Value.(*string), "<>") r = strings.ReplaceAll(r, "\r", "") r = strings.ReplaceAll(r, "\n", "\\n") r = strings.ReplaceAll(r, "\t", "\\t") return langext.StrLimit(r, lim, "...") case MDTInt: return strconv.Itoa(v.Value.(int)) case MDTInt8: return strconv.FormatInt(int64(v.Value.(int8)), 10) case MDTInt16: return strconv.FormatInt(int64(v.Value.(int16)), 10) case MDTInt32: return strconv.FormatInt(int64(v.Value.(int32)), 10) case MDTInt64: return strconv.FormatInt(v.Value.(int64), 10) case MDTFloat32: return strconv.FormatFloat(float64(v.Value.(float32)), 'g', 4, 32) case MDTFloat64: return strconv.FormatFloat(v.Value.(float64), 'g', 4, 64) case MDTBool: return fmt.Sprintf("%v", v.Value.(bool)) case MDTBytes: return langext.StrLimit(hex.EncodeToString(v.Value.([]byte)), lim, "...") case MDTObjectID: return v.Value.(primitive.ObjectID).Hex() case MDTTime: return v.Value.(time.Time).Format(time.RFC3339) case MDTDuration: return v.Value.(time.Duration).String() case MDTStringArray: if langext.IsNil(v.Value) { return "<>" } r, err := json.Marshal(v.Value.([]string)) if err != nil { return "(err)" } return langext.StrLimit(string(r), lim, "...") case MDTIntArray: if langext.IsNil(v.Value) { return "<>" } r, err := json.Marshal(v.Value.([]int)) if err != nil { return "(err)" } return langext.StrLimit(string(r), lim, "...") case MDTInt32Array: if langext.IsNil(v.Value) { return "<>" } r, err := json.Marshal(v.Value.([]int32)) if err != nil { return "(err)" } return langext.StrLimit(string(r), lim, "...") case MDTNil: return "<>" case MDTEnum: return v.Value.(EnumWrap).String() } return "(err)" } func (v MetaValue) Apply(key string, evt *zerolog.Event, limitLen *int) *zerolog.Event { switch v.DataType { case MDTString: if limitLen == nil { return evt.Str(key, v.Value.(string)) } else { evt.Str(key, langext.StrLimit(v.Value.(string), *limitLen, "...")) } case MDTID: return evt.Str(key, v.Value.(IDWrap).Value) case MDTAny: if v.Value.(AnyWrap).IsError { return evt.Str(key, "(err)") } else { if limitLen == nil { return evt.Str(key, v.Value.(AnyWrap).Json) } else { evt.Str(key, langext.StrLimit(v.Value.(AnyWrap).Json, *limitLen, "...")) } } case MDTStringPtr: if langext.IsNil(v.Value) { return evt.Str(key, "<>") } if limitLen == nil { return evt.Str(key, langext.CoalesceString(v.Value.(*string), "<>")) } else { evt.Str(key, langext.StrLimit(langext.CoalesceString(v.Value.(*string), "<>"), *limitLen, "...")) } case MDTInt: return evt.Int(key, v.Value.(int)) case MDTInt8: return evt.Int8(key, v.Value.(int8)) case MDTInt16: return evt.Int16(key, v.Value.(int16)) case MDTInt32: return evt.Int32(key, v.Value.(int32)) case MDTInt64: return evt.Int64(key, v.Value.(int64)) case MDTFloat32: return evt.Float32(key, v.Value.(float32)) case MDTFloat64: return evt.Float64(key, v.Value.(float64)) case MDTBool: return evt.Bool(key, v.Value.(bool)) case MDTBytes: return evt.Bytes(key, v.Value.([]byte)) case MDTObjectID: return evt.Str(key, v.Value.(primitive.ObjectID).Hex()) case MDTTime: return evt.Time(key, v.Value.(time.Time)) case MDTDuration: return evt.Dur(key, v.Value.(time.Duration)) case MDTStringArray: if langext.IsNil(v.Value) { return evt.Strs(key, nil) } return evt.Strs(key, v.Value.([]string)) case MDTIntArray: if langext.IsNil(v.Value) { return evt.Ints(key, nil) } return evt.Ints(key, v.Value.([]int)) case MDTInt32Array: if langext.IsNil(v.Value) { return evt.Ints32(key, nil) } return evt.Ints32(key, v.Value.([]int32)) case MDTNil: return evt.Str(key, "<>") case MDTEnum: if v.Value.(EnumWrap).IsNil { return evt.Any(key, nil) } else if v.Value.(EnumWrap).ValueRaw != nil { return evt.Any(key, v.Value.(EnumWrap).ValueRaw) } else { return evt.Str(key, v.Value.(EnumWrap).ValueString) } } return evt.Str(key, "(err)") } func (v MetaValue) MarshalJSON() ([]byte, error) { str, err := v.SerializeValue() if err != nil { return nil, err } return json.Marshal(string(v.DataType) + ":" + str) } func (v *MetaValue) UnmarshalJSON(data []byte) error { var str = "" err := json.Unmarshal(data, &str) if err != nil { return err } split := strings.SplitN(str, ":", 2) if len(split) != 2 { return errors.New("failed to decode MetaValue: '" + str + "'") } return v.Deserialize(split[1], metaDataType(split[0])) } func (v MetaValue) MarshalBSON() ([]byte, error) { serval, err := v.SerializeValue() if err != nil { return nil, Wrap(err, "failed to bson-marshal MetaValue (serialize)").Build() } // this is an kinda ugly hack - but serialization to mongodb and back can loose the correct type information.... bin, err := bson.Marshal(metaValueSerialization{ DataType: v.DataType, Value: serval, Raw: v.Value, }) if err != nil { return nil, Wrap(err, "failed to bson-marshal MetaValue (marshal)").Build() } return bin, nil } func (v *MetaValue) UnmarshalBSON(bytes []byte) error { var serval metaValueSerialization err := bson.Unmarshal(bytes, &serval) if err != nil { return Wrap(err, "failed to bson-unmarshal MetaValue (unmarshal)").Build() } err = v.Deserialize(serval.Value, serval.DataType) if err != nil { return Wrap(err, "failed to deserialize MetaValue from bson").Str("raw", serval.Value).Build() } return nil } func (v *MetaValue) Deserialize(value string, datatype metaDataType) error { switch datatype { case MDTString: v.Value = value v.DataType = datatype return nil case MDTID: v.Value = deserializeIDWrap(value) v.DataType = datatype return nil case MDTAny: v.Value = deserializeAnyWrap(value) v.DataType = datatype return nil case MDTStringPtr: if len(value) <= 0 || (value[0] != '*' && value[0] != '#') { return errors.New("Invalid StringPtr: " + value) } else if value == "#" { v.Value = nil v.DataType = datatype return nil } else { v.Value = langext.Ptr(value[1:]) v.DataType = datatype return nil } case MDTInt: pv, err := strconv.ParseInt(value, 10, 0) if err != nil { return err } v.Value = int(pv) v.DataType = datatype return nil case MDTInt8: pv, err := strconv.ParseInt(value, 10, 8) if err != nil { return err } v.Value = int8(pv) v.DataType = datatype return nil case MDTInt16: pv, err := strconv.ParseInt(value, 10, 16) if err != nil { return err } v.Value = int16(pv) v.DataType = datatype return nil case MDTInt32: pv, err := strconv.ParseInt(value, 10, 32) if err != nil { return err } v.Value = int32(pv) v.DataType = datatype return nil case MDTInt64: pv, err := strconv.ParseInt(value, 10, 64) if err != nil { return err } v.Value = pv v.DataType = datatype return nil case MDTFloat32: pv, err := strconv.ParseFloat(value, 64) if err != nil { return err } v.Value = float32(pv) v.DataType = datatype return nil case MDTFloat64: pv, err := strconv.ParseFloat(value, 64) if err != nil { return err } v.Value = pv v.DataType = datatype return nil case MDTBool: if value == "true" { v.Value = true v.DataType = datatype return nil } if value == "false" { v.Value = false v.DataType = datatype return nil } return errors.New("invalid bool value: " + value) case MDTBytes: r, err := hex.DecodeString(value) if err != nil { return err } v.Value = r v.DataType = datatype return nil case MDTObjectID: r, err := primitive.ObjectIDFromHex(value) if err != nil { return err } v.Value = r v.DataType = datatype return nil case MDTTime: ps := strings.Split(value, "|") if len(ps) != 2 { return errors.New("invalid time.time: " + value) } p1, err := strconv.ParseInt(ps[0], 10, 64) if err != nil { return err } p2, err := strconv.ParseInt(ps[1], 10, 32) if err != nil { return err } v.Value = time.Unix(p1, p2) v.DataType = datatype return nil case MDTDuration: r, err := time.ParseDuration(value) if err != nil { return err } v.Value = r v.DataType = datatype return nil case MDTStringArray: if value == "#" { v.Value = nil v.DataType = datatype return nil } pj := make([]string, 0) err := json.Unmarshal([]byte(value), &pj) if err != nil { return err } v.Value = pj v.DataType = datatype return nil case MDTIntArray: if value == "#" { v.Value = nil v.DataType = datatype return nil } pj := make([]int, 0) err := json.Unmarshal([]byte(value), &pj) if err != nil { return err } v.Value = pj v.DataType = datatype return nil case MDTInt32Array: if value == "#" { v.Value = nil v.DataType = datatype return nil } pj := make([]int32, 0) err := json.Unmarshal([]byte(value), &pj) if err != nil { return err } v.Value = pj v.DataType = datatype return nil case MDTNil: v.Value = nil v.DataType = datatype return nil case MDTEnum: v.Value = deserializeEnumWrap(value) v.DataType = datatype return nil } return errors.New("Unknown type: " + string(datatype)) } func (v MetaValue) ValueString() string { switch v.DataType { case MDTString: return v.Value.(string) case MDTID: return v.Value.(IDWrap).String() case MDTAny: return v.Value.(AnyWrap).String() case MDTStringPtr: if langext.IsNil(v.Value) { return "<>" } return langext.CoalesceString(v.Value.(*string), "<>") case MDTInt: return strconv.Itoa(v.Value.(int)) case MDTInt8: return strconv.FormatInt(int64(v.Value.(int8)), 10) case MDTInt16: return strconv.FormatInt(int64(v.Value.(int16)), 10) case MDTInt32: return strconv.FormatInt(int64(v.Value.(int32)), 10) case MDTInt64: return strconv.FormatInt(v.Value.(int64), 10) case MDTFloat32: return strconv.FormatFloat(float64(v.Value.(float32)), 'g', 4, 32) case MDTFloat64: return strconv.FormatFloat(v.Value.(float64), 'g', 4, 64) case MDTBool: return fmt.Sprintf("%v", v.Value.(bool)) case MDTBytes: return hex.EncodeToString(v.Value.([]byte)) case MDTObjectID: return v.Value.(primitive.ObjectID).Hex() case MDTTime: return v.Value.(time.Time).Format(time.RFC3339Nano) case MDTDuration: return v.Value.(time.Duration).String() case MDTStringArray: if langext.IsNil(v.Value) { return "<>" } r, err := json.MarshalIndent(v.Value.([]string), "", " ") if err != nil { return "(err)" } return string(r) case MDTIntArray: if langext.IsNil(v.Value) { return "<>" } r, err := json.MarshalIndent(v.Value.([]int), "", " ") if err != nil { return "(err)" } return string(r) case MDTInt32Array: if langext.IsNil(v.Value) { return "<>" } r, err := json.MarshalIndent(v.Value.([]int32), "", " ") if err != nil { return "(err)" } return string(r) case MDTNil: return "<>" case MDTEnum: return v.Value.(EnumWrap).String() } return "(err)" } // rawValueForJson returns most-of-the-time the `Value` field // but for some datatyes we do special processing // all, so we can pluck the output value in json.Marshal without any suprises func (v MetaValue) rawValueForJson() any { if v.DataType == MDTAny { if v.Value.(AnyWrap).IsNil { return nil } if v.Value.(AnyWrap).IsError { return bson.M{"@error": true} } jsonobj := primitive.M{} jsonarr := primitive.A{} if err := json.Unmarshal([]byte(v.Value.(AnyWrap).Json), &jsonobj); err == nil { return jsonobj } else if err := json.Unmarshal([]byte(v.Value.(AnyWrap).Json), &jsonarr); err == nil { return jsonarr } else { return bson.M{"type": v.Value.(AnyWrap).Type, "data": v.Value.(AnyWrap).Json} } } if v.DataType == MDTID { if v.Value.(IDWrap).IsNil { return nil } return v.Value.(IDWrap).Value } if v.DataType == MDTBytes { return hex.EncodeToString(v.Value.([]byte)) } if v.DataType == MDTDuration { return v.Value.(time.Duration).String() } if v.DataType == MDTTime { return v.Value.(time.Time).Format(time.RFC3339Nano) } if v.DataType == MDTObjectID { return v.Value.(primitive.ObjectID).Hex() } if v.DataType == MDTNil { return nil } if v.DataType == MDTEnum { if v.Value.(EnumWrap).IsNil { return nil } if v.Value.(EnumWrap).ValueRaw != nil { return v.Value.(EnumWrap).ValueRaw } return v.Value.(EnumWrap).ValueString } return v.Value } func (mm MetaMap) FormatOneLine(singleMaxLen int) string { r := "" i := 0 for key, val := range mm { if i > 0 { r += ", " } r += "\"" + key + "\"" r += ": " r += "\"" + val.ShortString(singleMaxLen) + "\"" i++ } return r } func (mm MetaMap) FormatMultiLine(indentFront string, indentKeys string, maxLenValue int) string { r := "" r += indentFront + "{" + "\n" for key, val := range mm { if key == "gin.body" { continue } r += indentFront r += indentKeys r += "\"" + key + "\"" r += ": " r += "\"" + val.ShortString(maxLenValue) + "\"" r += ",\n" } r += indentFront + "}" return r } func (mm MetaMap) Any() bool { return len(mm) > 0 } func (mm MetaMap) Apply(evt *zerolog.Event, limitLen *int) *zerolog.Event { for key, val := range mm { evt = val.Apply(key, evt, limitLen) } return evt } func (mm MetaMap) add(key string, mdtype metaDataType, val interface{}) { if _, ok := mm[key]; !ok { mm[key] = MetaValue{DataType: mdtype, Value: val} return } for i := 2; ; i++ { realkey := key + "-" + strconv.Itoa(i) if _, ok := mm[realkey]; !ok { mm[realkey] = MetaValue{DataType: mdtype, Value: val} return } } }