diff --git a/model/modeldecoder/generator/cmd/main.go b/model/modeldecoder/generator/cmd/main.go index 2068feb12e8..d330240636f 100644 --- a/model/modeldecoder/generator/cmd/main.go +++ b/model/modeldecoder/generator/cmd/main.go @@ -30,6 +30,9 @@ import ( const ( basePath = "github.com/elastic/apm-server" modeldecoderPath = "model/modeldecoder" + + pkgV2 = "v2" + pkgV3RUM = "rumv3" ) var ( @@ -38,26 +41,24 @@ var ( ) func main() { - genV2Models() - genRUMV3Models() + genV2() + genRUMV3() } -func genV2Models() { - pkg := "v2" - rootObjs := []string{"metadataRoot"} - out := filepath.Join(filepath.FromSlash(modeldecoderPath), pkg, "model_generated.go") - gen, err := generator.NewGenerator(importPath, pkg, typPath, rootObjs) +func genV2() { + rootObjs := []string{"metadataRoot", "transactionRoot"} + out := filepath.Join(filepath.FromSlash(modeldecoderPath), pkgV2, "model_generated.go") + gen, err := generator.NewGenerator(importPath, pkgV2, typPath, rootObjs) if err != nil { panic(err) } generate(gen, out) } -func genRUMV3Models() { - pkg := "rumv3" - rootObjs := []string{"metadataRoot"} - out := filepath.Join(filepath.FromSlash(modeldecoderPath), pkg, "model_generated.go") - gen, err := generator.NewGenerator(importPath, pkg, typPath, rootObjs) +func genRUMV3() { + rootObjs := []string{"metadataRoot", "transactionRoot"} + out := filepath.Join(filepath.FromSlash(modeldecoderPath), pkgV3RUM, "model_generated.go") + gen, err := generator.NewGenerator(importPath, pkgV3RUM, typPath, rootObjs) if err != nil { panic(err) } diff --git a/model/modeldecoder/generator/generator.go b/model/modeldecoder/generator/generator.go index d9f92e3005e..35927bd93ad 100644 --- a/model/modeldecoder/generator/generator.go +++ b/model/modeldecoder/generator/generator.go @@ -42,14 +42,18 @@ import ( type Generator struct { buf bytes.Buffer pkgName string - rootObjs map[string]structType + rootObjs []structType + // parsed structs from loading types from the provided package structTypes structTypes + // keep track of already processed types in case one type is // referenced multiple times processedTypes map[string]struct{} + // keep track of imports to add + imports map[string]struct{} - nullableString, nullableInt, nullableInterface string + nullableString, nullableInt, nullableFloat64, nullableBool, nullableInterface string } // NewGenerator takes an importPath and the package name for which @@ -70,10 +74,13 @@ func NewGenerator(importPath string, pkg string, typPath string, g := Generator{ pkgName: loaded.Types.Name(), structTypes: structTypes, - rootObjs: make(map[string]structType, len(root)), + rootObjs: make([]structType, 0, len(root)), processedTypes: make(map[string]struct{}), + imports: make(map[string]struct{}), nullableString: fmt.Sprintf("%s.String", typPath), nullableInt: fmt.Sprintf("%s.Int", typPath), + nullableFloat64: fmt.Sprintf("%s.Float64", typPath), + nullableBool: fmt.Sprintf("%s.Bool", typPath), nullableInterface: fmt.Sprintf("%s.Interface", typPath), } for _, r := range root { @@ -82,41 +89,56 @@ func NewGenerator(importPath string, pkg string, typPath string, if !ok { return nil, fmt.Errorf("object with root key %s not found", rootObjPath) } - g.rootObjs[rootObj.name] = rootObj + g.rootObjs = append(g.rootObjs, rootObj) } return &g, nil } // Generate writes generated methods to the buffer func (g *Generator) Generate() (bytes.Buffer, error) { - fmt.Fprintf(&g.buf, ` + // run generator code, writing code to the buffer + // and collect required imports + for _, rootObj := range g.rootObjs { + if err := g.generate(rootObj, ""); err != nil { + return g.buf, err + } + } + + // write package, edit note and imports to temporary buffer + var buf bytes.Buffer + fmt.Fprintf(&buf, ` // Code generated by "modeldecoder/generator". DO NOT EDIT. package %s import ( - "encoding/json" "fmt" - "unicode/utf8" -) `[1:], g.pkgName) - - for _, rootObj := range g.rootObjs { - if err := g.generate(rootObj, ""); err != nil { - return g.buf, err - } + for i := range g.imports { + fmt.Fprintf(&buf, ` +"%s"`, i) } - return g.buf, nil + fmt.Fprint(&buf, ` +)`) + + // combine buffers in correct order + fmt.Fprint(&buf, g.buf.String()) + return buf, nil } const ( ruleRequired = "required" ruleMax = "max" + ruleMin = "min" + ruleEnum = "enum" ruleMaxVals = "maxVals" rulePattern = "pattern" rulePatternKeys = "patternKeys" ruleTypes = "types" ruleTypesVals = "typesVals" + + importJSON = "encoding/json" + importUTF8 = "unicode/utf8" ) type structTypes map[string]structType @@ -152,9 +174,18 @@ func (g *Generator) generate(st structType, key string) error { if key != "" { key += "." } - for _, f := range st.fields { - if child, ok := g.structTypes[f.typ.String()]; ok { - if err := g.generate(child, fmt.Sprintf("%s%s", key, jsonName(f))); err != nil { + for _, field := range st.fields { + var childTyp string + switch fieldTyp := field.typ.Underlying().(type) { + case *types.Map: + childTyp = fieldTyp.Elem().String() + case *types.Slice: + childTyp = fieldTyp.Elem().String() + default: + childTyp = field.typ.String() + } + if child, ok := g.structTypes[childTyp]; ok { + if err := g.generate(child, fmt.Sprintf("%s%s", key, jsonName(field))); err != nil { return err } } @@ -163,8 +194,11 @@ func (g *Generator) generate(st structType, key string) error { } func (g *Generator) generateIsSet(structTyp structType, key string) error { + if len(structTyp.fields) == 0 { + return fmt.Errorf("unhandled struct %s (does not have any exported fields)", structTyp.name) + } fmt.Fprintf(&g.buf, ` -func (m *%s) IsSet() bool { +func (val *%s) IsSet() bool { return`, structTyp.name) if key != "" { key += "." @@ -178,9 +212,9 @@ func (m *%s) IsSet() bool { switch t := f.typ.Underlying().(type) { case *types.Slice, *types.Map: - fmt.Fprintf(&g.buf, `%s len(m.%s) > 0`, prefix, f.name) + fmt.Fprintf(&g.buf, `%s len(val.%s) > 0`, prefix, f.name) case *types.Struct: - fmt.Fprintf(&g.buf, `%s m.%s.IsSet()`, prefix, f.name) + fmt.Fprintf(&g.buf, `%s val.%s.IsSet()`, prefix, f.name) default: return fmt.Errorf("unhandled type %T for IsSet() for '%s%s'", t, key, jsonName(f)) } @@ -193,7 +227,7 @@ func (m *%s) IsSet() bool { func (g *Generator) generateReset(structTyp structType, key string) error { fmt.Fprintf(&g.buf, ` -func (m *%s) Reset() { +func (val *%s) Reset() { `, structTyp.name) if key != "" { key += "." @@ -206,20 +240,20 @@ func (m *%s) Reset() { // this potentially leads to keeping more memory allocated than required; // at the moment metadata.process.argv is the only slice fmt.Fprintf(&g.buf, ` -m.%s = m.%s[:0] +val.%s = val.%s[:0] `[1:], f.name, f.name) case *types.Map: // the map is cleared, not returning the underlying memory to // the garbage collector; when map size differs this potentially // leads to keeping more memory allocated than required fmt.Fprintf(&g.buf, ` -for k := range m.%s { - delete(m.%s, k) +for k := range val.%s { + delete(val.%s, k) } `[1:], f.name, f.name) case *types.Struct: fmt.Fprintf(&g.buf, ` -m.%s.Reset() +val.%s.Reset() `[1:], f.name) default: return fmt.Errorf("unhandled type %T for Reset() for '%s%s'", t, key, jsonName(f)) @@ -233,11 +267,18 @@ m.%s.Reset() func (g *Generator) generateValidation(structTyp structType, key string) error { fmt.Fprintf(&g.buf, ` -func (m *%s) validate() error { +func (val *%s) validate() error { `, structTyp.name) - if _, ok := g.rootObjs[structTyp.name]; !ok { + var isRoot bool + for _, rootObjs := range g.rootObjs { + if structTyp.name == rootObjs.name { + isRoot = true + break + } + } + if !isRoot { fmt.Fprint(&g.buf, ` -if !m.IsSet() { +if !val.IsSet() { return nil } `[1:]) @@ -253,7 +294,7 @@ if !m.IsSet() { // if field is a model struct, call its validation function if _, ok := g.structTypes[f.typ.String()]; ok { fmt.Fprintf(&g.buf, ` -if err := m.%s.validate(); err != nil{ +if err := val.%s.validate(); err != nil{ return err } `[1:], f.name) @@ -278,22 +319,31 @@ if err := m.%s.validate(); err != nil{ for _, rule := range sortedRules { switch rule { case ruleRequired: - fmt.Fprintf(&g.buf, ` -if len(m.%s) == 0{ - return fmt.Errorf("'%s' required") -} -`[1:], f.name, flattenedName) + ruleNullableRequired(&g.buf, f.name, flattenedName) default: return fmt.Errorf("unhandled tag rule '%s' for '%s'", rule, flattenedName) } } case *types.Map: + var elem structType + switch t.Elem().Underlying().(type) { + case *types.Basic, *types.Interface: // do nothing special + case *types.Struct: + if customStruct, ok := g.structTypes[t.Elem().String()]; ok { + elem = customStruct + } else { + return fmt.Errorf("unhandled struct type %s iterating map for '%s'", t, flattenedName) + } + default: + return fmt.Errorf("unhandled type %s iterating map for '%s'", t, flattenedName) + } + var required bool if _, ok := parts[ruleRequired]; ok { required = true delete(parts, ruleRequired) fmt.Fprintf(&g.buf, ` -if len(m.%s) == 0{ +if len(val.%s) == 0{ return fmt.Errorf("'%s' required") } `[1:], f.name, flattenedName) @@ -301,19 +351,34 @@ if len(m.%s) == 0{ if len(parts) == 0 { continue } + types, typesRestricted := parts[ruleTypesVals] // iterate over map once and run checks - fmt.Fprintf(&g.buf, ` -for k,v := range m.%s{ + if typesRestricted || elem.name != "" { + fmt.Fprintf(&g.buf, ` +for k,v := range val.%s{ +`[1:], f.name) + } else { + fmt.Fprintf(&g.buf, ` +for k := range val.%s{ `[1:], f.name) + } + if regex, ok := parts[rulePatternKeys]; ok { delete(parts, rulePatternKeys) fmt.Fprintf(&g.buf, ` -if !%s.MatchString(k){ +if k != "" && !%s.MatchString(k){ return fmt.Errorf("validation rule '%s(%s)' violated for '%s'") } `[1:], regex, rulePatternKeys, regex, flattenedName) + if elem.name != "" { + fmt.Fprintf(&g.buf, ` +if err := v.validate(); err != nil{ + return err +} +`[1:]) + } } - if types, ok := parts[ruleTypesVals]; ok { + if typesRestricted { delete(parts, ruleTypesVals) fmt.Fprintf(&g.buf, ` switch t := v.(type){ @@ -326,6 +391,7 @@ case nil: for _, typ := range strings.Split(types, ";") { if typ == "number" { typ = "json.Number" + g.imports[importJSON] = struct{}{} } fmt.Fprintf(&g.buf, ` case %s: @@ -333,6 +399,7 @@ case %s: if typ == "string" { if maxVal, ok := parts[ruleMaxVals]; ok { delete(parts, ruleMaxVals) + g.imports[importUTF8] = struct{}{} fmt.Fprintf(&g.buf, ` if utf8.RuneCountInString(t) > %s{ return fmt.Errorf("validation rule '%s(%s)' violated for '%s'") @@ -356,29 +423,47 @@ default: } case *types.Struct: switch f.typ.String() { - //TODO(simitt): can these type checks be more generic? case g.nullableString: for _, rule := range sortedRules { val := parts[rule] switch rule { - case ruleRequired: + case ruleEnum: fmt.Fprintf(&g.buf, ` -if !m.%s.IsSet() { - return fmt.Errorf("'%s' required") +if val.%s.Val != ""{ + var matchEnum bool + for _, s := range %s { + if val.%s.Val == s{ + matchEnum = true + break + } + } + if !matchEnum{ + return fmt.Errorf("validation rule '%s(%s)' violated for '%s'") + } } -`[1:], f.name, flattenedName) +`[1:], f.name, val, f.name, rule, val, flattenedName) case ruleMax: + g.imports[importUTF8] = struct{}{} fmt.Fprintf(&g.buf, ` -if utf8.RuneCountInString(m.%s.Val) > %s{ +if utf8.RuneCountInString(val.%s.Val) > %s{ + return fmt.Errorf("validation rule '%s(%s)' violated for '%s'") +} +`[1:], f.name, val, rule, val, flattenedName) + case ruleMin: + g.imports[importUTF8] = struct{}{} + fmt.Fprintf(&g.buf, ` +if utf8.RuneCountInString(val.%s.Val) < %s{ return fmt.Errorf("validation rule '%s(%s)' violated for '%s'") } `[1:], f.name, val, rule, val, flattenedName) case rulePattern: fmt.Fprintf(&g.buf, ` -if !%s.MatchString(m.%s.Val){ +if val.%s.Val != "" && !%s.MatchString(val.%s.Val){ return fmt.Errorf("validation rule '%s(%s)' violated for '%s'") } -`[1:], val, f.name, rule, val, flattenedName) +`[1:], f.name, val, f.name, rule, val, flattenedName) + case ruleRequired: + ruleNullableRequired(&g.buf, f.name, flattenedName) default: return fmt.Errorf("unhandled tag rule '%s' for '%s'", rule, flattenedName) } @@ -388,14 +473,26 @@ if !%s.MatchString(m.%s.Val){ val := parts[rule] switch rule { case ruleRequired: + ruleNullableRequired(&g.buf, f.name, flattenedName) + case ruleMax: fmt.Fprintf(&g.buf, ` -if !m.%s.IsSet() { - return fmt.Errorf("'%s' required") +if val.%s.Val > %s{ + return fmt.Errorf("validation rule '%s(%s)' violated for '%s'") } -`[1:], f.name, flattenedName) - case ruleMax: +`[1:], f.name, val, rule, val, flattenedName) + default: + return fmt.Errorf("unhandled tag rule '%s' for '%s'", rule, flattenedName) + } + } + case g.nullableFloat64: + for _, rule := range sortedRules { + val := parts[rule] + switch rule { + case ruleRequired: + ruleNullableRequired(&g.buf, f.name, flattenedName) + case ruleMin: fmt.Fprintf(&g.buf, ` -if m.%s.Val > %s{ +if val.%s.Val < %s{ return fmt.Errorf("validation rule '%s(%s)' violated for '%s'") } `[1:], f.name, val, rule, val, flattenedName) @@ -412,39 +509,54 @@ if m.%s.Val > %s{ val := parts[rule] switch rule { case ruleRequired: - fmt.Fprintf(&g.buf, ` -if !m.%s.IsSet() { - return fmt.Errorf("'%s' required") -} -`[1:], f.name, flattenedName) + ruleNullableRequired(&g.buf, f.name, flattenedName) case ruleMax: //handled in switch statement for string types case ruleTypes: - fmt.Fprintf(&g.buf, ` -switch t := m.%s.Val.(type){ -`[1:], f.name) + if _, ok := parts[ruleMax]; ok { + fmt.Fprintf(&g.buf, ` +switch t := val.%s.Val.(type){ + `[1:], f.name) + } else { + fmt.Fprintf(&g.buf, ` +switch val.%s.Val.(type){ + `[1:], f.name) + } for _, typ := range strings.Split(val, ";") { - if typ == "int" { + switch typ { + case "int": + g.imports[importJSON] = struct{}{} fmt.Fprintf(&g.buf, ` +case int: case json.Number: -`[1:]) - fmt.Fprintf(&g.buf, ` -if _, err := t.Int64(); err != nil{ - return fmt.Errorf("validation rule '%s(%s)' violated for '%s'") -} + if _, err := t.Int64(); err != nil{ + return fmt.Errorf("validation rule '%s(%s)' violated for '%s'") + } `[1:], rule, val, flattenedName) - } - fmt.Fprintf(&g.buf, ` + case "string": + fmt.Fprintf(&g.buf, ` case %s: -`[1:], typ) - if typ == "string" { + `[1:], typ) if max, ok := parts[ruleMax]; ok { + g.imports[importUTF8] = struct{}{} fmt.Fprintf(&g.buf, ` if utf8.RuneCountInString(t) > %s{ - return fmt.Errorf("validation rule '%s(%s)' violated for '%s'") +return fmt.Errorf("validation rule '%s(%s)' violated for '%s'") } `[1:], max, ruleMax, max, flattenedName) } + case "interface": + fmt.Fprint(&g.buf, ` +case interface{}: +`[1:]) + case "map[string]interface": + fmt.Fprint(&g.buf, ` +case map[string]interface{}: +`[1:]) + default: + fmt.Fprintf(&g.buf, ` +case %s: +`[1:], typ) } } if !required { @@ -465,11 +577,7 @@ default: for _, rule := range sortedRules { switch rule { case ruleRequired: - fmt.Fprintf(&g.buf, ` -if !m.%s.IsSet(){ - return fmt.Errorf("'%s' required") -} -`[1:], f.name, flattenedName) + ruleNullableRequired(&g.buf, f.name, flattenedName) default: return fmt.Errorf("unhandled tag rule '%s' for '%s'", rule, flattenedName) } @@ -486,6 +594,14 @@ if !m.%s.IsSet(){ return nil } +func ruleNullableRequired(b *bytes.Buffer, name string, key string) { + fmt.Fprintf(b, ` +if !val.%s.IsSet() { + return fmt.Errorf("'%s' required") +} +`[1:], name, key) +} + func jsonName(f structField) string { parts := parseTag(f.tag, "json") if len(parts) == 0 { @@ -560,30 +676,26 @@ func parseTag(structTag reflect.StructTag, tagName string) []string { func validationTag(structTag reflect.StructTag) (map[string]string, error) { parts := parseTag(structTag, "validate") m := make(map[string]string, len(parts)) + errPrefix := "parse validation tag:" for _, rule := range parts { parts := strings.Split(rule, "=") switch len(parts) { case 1: // valueless rule e.g. required if rule != parts[0] { - return nil, fmt.Errorf("malformed tag '%s'", rule) + return nil, fmt.Errorf("%s malformed tag '%s'", errPrefix, rule) } switch rule { case ruleRequired: m[rule] = "" default: - return nil, fmt.Errorf("unhandled tag rule '%s'", rule) + return nil, fmt.Errorf("%s unhandled tag rule '%s'", errPrefix, rule) } case 2: // rule=value m[parts[0]] = parts[1] - switch parts[0] { - case ruleMax, ruleMaxVals, rulePattern, rulePatternKeys, ruleTypes, ruleTypesVals: - default: - return nil, fmt.Errorf("unhandled tag rule '%s'", parts[0]) - } default: - return nil, fmt.Errorf("malformed tag '%s'", rule) + return nil, fmt.Errorf("%s malformed tag '%s'", errPrefix, rule) } } return m, nil diff --git a/model/modeldecoder/modeldecodertest/populator.go b/model/modeldecoder/modeldecodertest/populator.go index b49fa864174..d93c1ae8866 100644 --- a/model/modeldecoder/modeldecodertest/populator.go +++ b/model/modeldecoder/modeldecodertest/populator.go @@ -19,49 +19,111 @@ package modeldecodertest import ( "fmt" + "net" + "net/http" "reflect" "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" - "github.com/elastic/apm-server/model/modeldecoder/nullable" "github.com/elastic/beats/v7/libbeat/common" + + "github.com/elastic/apm-server/model" + "github.com/elastic/apm-server/model/modeldecoder/nullable" ) // InitStructValues iterates through the struct fields represented by // the given reflect.Value and initializes all fields with // some arbitrary value. func InitStructValues(i interface{}) { - SetStructValues(i, "initialized", 1) + SetStructValues(i, "unknown", 1, true, time.Now()) } // SetStructValues iterates through the struct fields represented by // the given reflect.Value and initializes all fields with // the given values for strings and integers. -func SetStructValues(in interface{}, vStr string, vInt int) { +func SetStructValues(in interface{}, vStr string, vInt int, vBool bool, vTime time.Time) { IterateStruct(in, func(f reflect.Value, key string) { var newVal interface{} - switch v := f.Interface().(type) { - case map[string]interface{}: - newVal = map[string]interface{}{vStr: vStr} - case common.MapStr: - newVal = common.MapStr{vStr: vStr} - case []string: - newVal = []string{vStr} - case []int: - newVal = []int{vInt, vInt} - case nullable.String: - v.Set(vStr) - newVal = v - case nullable.Int: - v.Set(vInt) - newVal = v - case nullable.Interface: - v.Set(vStr) - newVal = v - default: - if f.Type().Kind() == reflect.Struct { + switch fKind := f.Kind(); fKind { + case reflect.Slice: + switch v := f.Interface().(type) { + case []string: + newVal = []string{vStr} + case []int: + newVal = []int{vInt, vInt} + default: + if f.Type().Elem().Kind() != reflect.Struct { + panic(fmt.Sprintf("unhandled type %s for key %s", v, key)) + } + if f.IsNil() { + f.Set(reflect.MakeSlice(f.Type(), 1, 1)) + } + f.Index(0).Set(reflect.Zero(f.Type().Elem())) + return + } + case reflect.Map: + switch v := f.Interface().(type) { + case map[string]interface{}: + newVal = map[string]interface{}{vStr: vStr} + case common.MapStr: + newVal = common.MapStr{vStr: vStr} + case map[string]float64: + newVal = map[string]float64{vStr: float64(vInt) + 0.5} + default: + if f.Type().Elem().Kind() != reflect.Struct { + panic(fmt.Sprintf("unhandled type %s for key %s", v, key)) + } + if f.IsNil() { + f.Set(reflect.MakeMap(f.Type())) + } + mKey := reflect.Zero(f.Type().Key()) + mVal := reflect.Zero(f.Type().Elem()) + f.SetMapIndex(mKey, mVal) + return + } + case reflect.Struct: + switch v := f.Interface().(type) { + case nullable.String: + v.Set(vStr) + newVal = v + case nullable.Int: + v.Set(vInt) + newVal = v + case nullable.Interface: + if strings.Contains(key, "port") { + v.Set(vInt) + } else { + v.Set(vStr) + } + newVal = v + case nullable.Bool: + v.Set(vBool) + newVal = v + case nullable.Float64: + v.Set(float64(vInt) + 0.5) + newVal = v + case nullable.TimeMicrosUnix: + v.Set(vTime) + newVal = v + case nullable.HTTPHeader: + v.Set(http.Header{vStr: []string{vStr, vStr}}) + newVal = v + default: + if f.IsZero() { + f.Set(reflect.Zero(f.Type())) + } return } - panic(fmt.Sprintf("unhandled type %T for key %s", f.Type().Kind(), key)) + case reflect.Ptr: + if f.IsNil() { + f.Set(reflect.Zero(f.Type())) + } + return + default: + panic(fmt.Sprintf("unhandled type %s for key %s", fKind, key)) } f.Set(reflect.ValueOf(newVal)) }) @@ -87,6 +149,76 @@ func SetZeroStructValue(i interface{}, callback func(string)) { }) } +// AssertStructValues recursively walks through the given struct and asserts +// that values are equal to expected values +func AssertStructValues(t *testing.T, i interface{}, isException func(string) bool, + vStr string, vInt int, vBool bool, vIP net.IP, vTime time.Time) { + IterateStruct(i, func(f reflect.Value, key string) { + if isException(key) { + return + } + fVal := f.Interface() + var newVal interface{} + switch fVal.(type) { + case map[string]interface{}: + newVal = map[string]interface{}{vStr: vStr} + case map[string]float64: + newVal = map[string]float64{vStr: float64(vInt) + 0.5} + case common.MapStr: + newVal = common.MapStr{vStr: vStr} + case *model.Labels: + newVal = &model.Labels{vStr: vStr} + case *model.Custom: + newVal = &model.Custom{vStr: vStr} + case []string: + newVal = []string{vStr} + case []int: + newVal = []int{vInt, vInt} + case string: + newVal = vStr + case *string: + newVal = &vStr + case int: + newVal = vInt + case *int: + newVal = &vInt + case float64: + newVal = float64(vInt) + 0.5 + case *float64: + val := float64(vInt) + 0.5 + newVal = &val + case net.IP: + newVal = vIP + case bool: + newVal = vBool + case *bool: + newVal = &vBool + case http.Header: + newVal = http.Header{vStr: []string{vStr, vStr}} + case time.Time: + newVal = vTime + default: + // the populator recursively iterates over struct and structPtr + // calling this function for all fields; + // it is enough to only assert they are not zero here + if f.Type().Kind() == reflect.Struct { + assert.NotZero(t, fVal, key) + return + } + if f.Type().Kind() == reflect.Ptr && f.Type().Elem().Kind() == reflect.Struct { + assert.NotZero(t, fVal, key) + return + } + if f.Type().Kind() == reflect.Map || f.Type().Kind() == reflect.Array { + assert.NotZero(t, fVal, key) + return + } + panic(fmt.Sprintf("unhandled type %s for key %s", f.Type().Kind(), key)) + } + assert.Equal(t, newVal, fVal, key) + }) +} + // IterateStruct iterates through the struct fields represented by // the given reflect.Value and calls the given function on every field. func IterateStruct(i interface{}, fn func(reflect.Value, string)) { @@ -100,7 +232,7 @@ func IterateStruct(i interface{}, fn func(reflect.Value, string)) { func iterateStruct(v reflect.Value, key string, fn func(f reflect.Value, fKey string)) { t := v.Type() if t.Kind() != reflect.Struct { - panic(fmt.Sprintf("iterateStruct: invalid typ %T", t.Kind())) + panic(fmt.Sprintf("iterateStruct: invalid type %s", t.Kind())) } if key != "" { key += "." @@ -111,6 +243,7 @@ func iterateStruct(v reflect.Value, key string, fn func(f reflect.Value, fKey st if !f.CanSet() { continue } + stf := t.Field(i) fTyp := stf.Type name := jsonName(stf) @@ -119,14 +252,43 @@ func iterateStruct(v reflect.Value, key string, fn func(f reflect.Value, fKey st } fKey = fmt.Sprintf("%s%s", key, name) - if fTyp.Kind() == reflect.Struct { + // call the given function with every field + fn(f, fKey) + // check field type for recursive iteration + switch f.Kind() { + case reflect.Ptr: + if !f.IsZero() && fTyp.Elem().Kind() == reflect.Struct { + iterateStruct(f.Elem(), fKey, fn) + } + case reflect.Struct: switch f.Interface().(type) { - case nullable.String, nullable.Int, nullable.Interface: + case nullable.String, nullable.Int, nullable.Bool, nullable.Float64, + nullable.Interface, nullable.HTTPHeader, nullable.TimeMicrosUnix: default: iterateStruct(f, fKey, fn) } + case reflect.Map: + if f.Type().Elem().Kind() != reflect.Struct { + continue + } + iter := f.MapRange() + for iter.Next() { + mKey := iter.Key() + mVal := iter.Value() + ptr := reflect.New(mVal.Type()) + ptr.Elem().Set(mVal) + iterateStruct(ptr.Elem(), fmt.Sprintf("%s.[%s]", fKey, mKey), fn) + f.SetMapIndex(mKey, ptr.Elem()) + i++ + } + case reflect.Slice, reflect.Array: + for j := 0; j < f.Len(); j++ { + sliceField := f.Index(j) + if sliceField.Kind() == reflect.Struct { + iterateStruct(sliceField, fmt.Sprintf("%s.[%v]", fKey, j), fn) + } + } } - fn(f, fKey) } } diff --git a/model/modeldecoder/modeldecodertest/strbuilder.go b/model/modeldecoder/modeldecodertest/strbuilder.go new file mode 100644 index 00000000000..434c881433d --- /dev/null +++ b/model/modeldecoder/modeldecodertest/strbuilder.go @@ -0,0 +1,30 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package modeldecodertest + +import "strings" + +// BuildString creates a string conisisting of nRunes runes +func BuildString(nRunes int) string { + return BuildStringWith(nRunes, '⌘') +} + +// BuildStringWith creates a string conisisting of nRunes of the given rune +func BuildStringWith(nRunes int, r rune) string { + return strings.Repeat(string(r), nRunes) +} diff --git a/model/modeldecoder/modeldecodertest/testdata.go b/model/modeldecoder/modeldecodertest/testdata.go new file mode 100644 index 00000000000..f8503de9327 --- /dev/null +++ b/model/modeldecoder/modeldecodertest/testdata.go @@ -0,0 +1,59 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package modeldecodertest + +import ( + "bytes" + "encoding/json" + "io" + "testing" + + "github.com/stretchr/testify/require" + + "github.com/elastic/apm-server/decoder" +) + +// DecodeData decodes input from the io.Reader into the given output +// it skips the metadata line if eventType is not set to metadata +func DecodeData(t *testing.T, r io.Reader, eventType string, out interface{}) { + dec := decoder.NewJSONIteratorDecoder(r) + // skip first line (metadata) for all events but metadata + if eventType != "metadata" && eventType != "m" { + var data interface{} + require.NoError(t, dec.Decode(&data)) + } + // decode data + require.NoError(t, dec.Decode(&out)) +} + +// DecodeDataWithReplacement decodes input from the io.Reader and replaces data for the +// given key with the provided newData before decoding into the output +func DecodeDataWithReplacement(t *testing.T, r io.Reader, eventType string, key string, newData string, out interface{}) { + var data map[string]interface{} + DecodeData(t, r, eventType, &data) + // replace data for given key with newData + eventData := data[eventType].(map[string]interface{}) + var keyData interface{} + require.NoError(t, json.Unmarshal([]byte(newData), &keyData)) + eventData[key] = keyData + + // unmarshal data into struct + b, err := json.Marshal(eventData) + require.NoError(t, err) + require.NoError(t, decoder.NewJSONIteratorDecoder(bytes.NewReader(b)).Decode(out)) +} diff --git a/model/modeldecoder/nullable/nullable.go b/model/modeldecoder/nullable/nullable.go index b4c5daad3aa..8d53a3a6986 100644 --- a/model/modeldecoder/nullable/nullable.go +++ b/model/modeldecoder/nullable/nullable.go @@ -18,6 +18,9 @@ package nullable import ( + "fmt" + "net/http" + "time" "unsafe" jsoniter "github.com/json-iterator/go" @@ -42,6 +45,24 @@ func init() { (*((*Int)(ptr))).isSet = true } }) + jsoniter.RegisterTypeDecoderFunc("nullable.Float64", func(ptr unsafe.Pointer, iter *jsoniter.Iterator) { + switch iter.WhatIsNext() { + case jsoniter.NilValue: + iter.ReadNil() + default: + (*((*Float64)(ptr))).Val = iter.ReadFloat64() + (*((*Float64)(ptr))).isSet = true + } + }) + jsoniter.RegisterTypeDecoderFunc("nullable.Bool", func(ptr unsafe.Pointer, iter *jsoniter.Iterator) { + switch iter.WhatIsNext() { + case jsoniter.NilValue: + iter.ReadNil() + default: + (*((*Bool)(ptr))).Val = iter.ReadBool() + (*((*Bool)(ptr))).isSet = true + } + }) jsoniter.RegisterTypeDecoderFunc("nullable.Interface", func(ptr unsafe.Pointer, iter *jsoniter.Iterator) { switch iter.WhatIsNext() { case jsoniter.NilValue: @@ -51,6 +72,50 @@ func init() { (*((*Interface)(ptr))).isSet = true } }) + jsoniter.RegisterTypeDecoderFunc("nullable.TimeMicrosUnix", func(ptr unsafe.Pointer, iter *jsoniter.Iterator) { + switch iter.WhatIsNext() { + case jsoniter.NilValue: + iter.ReadNil() + default: + us := iter.ReadInt() + s := us / 1000000 + ns := (us - (s * 1000000)) * 1000 + (*((*TimeMicrosUnix)(ptr))).Val = time.Unix(int64(s), int64(ns)).UTC() + (*((*TimeMicrosUnix)(ptr))).isSet = true + } + }) + jsoniter.RegisterTypeDecoderFunc("nullable.HTTPHeader", func(ptr unsafe.Pointer, iter *jsoniter.Iterator) { + switch iter.WhatIsNext() { + case jsoniter.NilValue: + iter.ReadNil() + default: + m, ok := iter.Read().(map[string]interface{}) + if !ok { + iter.Error = fmt.Errorf("invalid input for HTTPHeader: %v", m) + } + h := http.Header{} + for key, val := range m { + switch v := val.(type) { + case nil: + case string: + h.Add(key, v) + case []interface{}: + for _, entry := range v { + switch entry := entry.(type) { + case string: + h.Add(key, entry) + default: + iter.Error = fmt.Errorf("invalid input for HTTPHeader: %v", v) + } + } + default: + iter.Error = fmt.Errorf("invalid input for HTTPHeader: %v", v) + } + } + (*((*HTTPHeader)(ptr))).Val = h + (*((*HTTPHeader)(ptr))).isSet = true + } + }) } // String stores a string value and the @@ -103,6 +168,56 @@ func (v *Int) Reset() { v.isSet = false } +// Float64 stores a float64 value and the +// information if the value has been set +type Float64 struct { + Val float64 + isSet bool +} + +// Set sets the value +func (v *Float64) Set(val float64) { + v.Val = val + v.isSet = true +} + +// IsSet is true when decode was called +func (v *Float64) IsSet() bool { + return v.isSet +} + +// Reset sets the Int to it's initial state +// where it is not set and has no value +func (v *Float64) Reset() { + v.Val = 0.0 + v.isSet = false +} + +// Bool stores a bool value and the +// information if the value has been set +type Bool struct { + Val bool + isSet bool +} + +// Set sets the value +func (v *Bool) Set(val bool) { + v.Val = val + v.isSet = true +} + +// IsSet is true when decode was called +func (v *Bool) IsSet() bool { + return v.isSet +} + +// Reset sets the Int to it's initial state +// where it is not set and has no value +func (v *Bool) Reset() { + v.Val = false + v.isSet = false +} + // Interface stores an interface{} value and the // information if the value has been set // @@ -129,3 +244,51 @@ func (v *Interface) Reset() { v.Val = nil v.isSet = false } + +type TimeMicrosUnix struct { + Val time.Time + isSet bool +} + +// Set sets the value +func (v *TimeMicrosUnix) Set(val time.Time) { + v.Val = val + v.isSet = true +} + +// IsSet is true when decode was called +func (v *TimeMicrosUnix) IsSet() bool { + return v.isSet +} + +// Reset sets the Interface to it's initial state +// where it is not set and has no value +func (v *TimeMicrosUnix) Reset() { + v.Val = time.Time{} + v.isSet = false +} + +type HTTPHeader struct { + Val http.Header + isSet bool +} + +// Set sets the value +func (v *HTTPHeader) Set(val http.Header) { + v.Val = val + v.isSet = true +} + +// IsSet is true when decode was called +func (v *HTTPHeader) IsSet() bool { + return v.isSet +} + +// Reset sets the Interface to it's initial state +// where it is not set and has no value +func (v *HTTPHeader) Reset() { + for k := range v.Val { + delete(v.Val, k) + } + v.isSet = false +} diff --git a/model/modeldecoder/nullable/nullable_test.go b/model/modeldecoder/nullable/nullable_test.go index 3c27a8a938a..320b3f8689a 100644 --- a/model/modeldecoder/nullable/nullable_test.go +++ b/model/modeldecoder/nullable/nullable_test.go @@ -18,8 +18,10 @@ package nullable import ( + "net/http" "strings" "testing" + "time" jsoniter "github.com/json-iterator/go" "github.com/stretchr/testify/assert" @@ -27,9 +29,13 @@ import ( ) type testType struct { - S String `json:"s"` - I Int `json:"i"` - V Interface `json:"v"` + S String `json:"s"` + I Int `json:"i"` + F Float64 `json:"f"` + B Bool `json:"b"` + V Interface `json:"v"` + Tms TimeMicrosUnix `json:"tms"` + H HTTPHeader `json:"h"` } var json = jsoniter.ConfigCompatibleWithStandardLibrary @@ -108,6 +114,82 @@ func TestInt(t *testing.T) { } } +func TestFloat64(t *testing.T) { + for _, tc := range []struct { + name string + input string + + val float64 + isSet, fail bool + }{ + {name: "values", input: `{"f":44.89}`, val: 44.89, isSet: true}, + {name: "integer", input: `{"f":44}`, val: 44.00, isSet: true}, + {name: "zero", input: `{"f":0}`, isSet: true}, + {name: "null", input: `{"f":null}`, isSet: false}, + {name: "missing", input: `{}`}, + {name: "invalid", input: `{"f":"1.0.1"}`, fail: true}, + } { + t.Run(tc.name, func(t *testing.T) { + dec := json.NewDecoder(strings.NewReader(tc.input)) + var testStruct testType + err := dec.Decode(&testStruct) + if tc.fail { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.isSet, testStruct.F.IsSet()) + assert.Equal(t, tc.val, testStruct.F.Val) + } + + testStruct.F.Reset() + assert.False(t, testStruct.F.IsSet()) + assert.Empty(t, testStruct.F.Val) + + testStruct.F.Set(55.67) + assert.True(t, testStruct.F.IsSet()) + assert.Equal(t, 55.67, testStruct.F.Val) + }) + } +} + +func TestBool(t *testing.T) { + for _, tc := range []struct { + name string + input string + + val bool + isSet, fail bool + }{ + {name: "true", input: `{"b":true}`, val: true, isSet: true}, + {name: "false", input: `{"b":false}`, val: false, isSet: true}, + {name: "null", input: `{"b":null}`, isSet: false}, + {name: "missing", input: `{}`}, + {name: "convert", input: `{"b":1}`, fail: true}, + {name: "invalid", input: `{"b":"1.0.1"}`, fail: true}, + } { + t.Run(tc.name, func(t *testing.T) { + dec := json.NewDecoder(strings.NewReader(tc.input)) + var testStruct testType + err := dec.Decode(&testStruct) + if tc.fail { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.isSet, testStruct.B.IsSet()) + assert.Equal(t, tc.val, testStruct.B.Val) + } + + testStruct.B.Reset() + assert.False(t, testStruct.B.IsSet()) + assert.Empty(t, testStruct.B.Val) + + testStruct.B.Set(true) + assert.True(t, testStruct.B.IsSet()) + assert.Equal(t, true, testStruct.B.Val) + }) + } +} + func TestInterface(t *testing.T) { for _, tc := range []struct { name string @@ -141,3 +223,87 @@ func TestInterface(t *testing.T) { }) } } + +func TestTimeMicrosUnix(t *testing.T) { + for _, tc := range []struct { + name string + input string + + val string + isSet, fail bool + }{ + {name: "valid", input: `{"tms":1599996822281000}`, isSet: true, + val: "2020-09-13 11:33:42.281 +0000 UTC"}, + {name: "null", input: `{"tms":null}`, val: time.Time{}.String()}, + {name: "invalid-type", input: `{"tms":""}`, fail: true, isSet: true}, + {name: "invalid-type", input: `{"tms":123.56}`, fail: true, isSet: true}, + {name: "missing", input: `{}`, val: time.Time{}.String()}, + } { + t.Run(tc.name, func(t *testing.T) { + dec := json.NewDecoder(strings.NewReader(tc.input)) + var testStruct testType + err := dec.Decode(&testStruct) + if tc.fail { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.isSet, testStruct.Tms.IsSet()) + assert.Equal(t, tc.val, testStruct.Tms.Val.String()) + } + + testStruct.Tms.Reset() + assert.False(t, testStruct.Tms.IsSet()) + assert.Zero(t, testStruct.Tms.Val) + + testStruct.Tms.Set(time.Now()) + assert.True(t, testStruct.Tms.IsSet()) + assert.NotZero(t, testStruct.Tms.Val) + }) + } +} +func TestHTTPHeader(t *testing.T) { + for _, tc := range []struct { + name string + input string + + val http.Header + isSet, fail bool + }{ + {name: "valid", isSet: true, input: ` +{"h":{"content-type":"application/x-ndjson","Authorization":"Bearer 123-token","authorization":"ApiKey 123-api-key","Accept":["text/html", "application/xhtml+xml"]}}`, + val: http.Header{ + "Content-Type": []string{"application/x-ndjson"}, + "Authorization": []string{"ApiKey 123-api-key", "Bearer 123-token"}, + "Accept": []string{"text/html", "application/xhtml+xml"}, + }}, + {name: "valid2", input: `{"h":{"k":["a","b"]}}`, isSet: true, val: http.Header{"K": []string{"a", "b"}}}, + {name: "null", input: `{"h":null}`}, + {name: "invalid-type", input: `{"h":""}`, fail: true, isSet: true}, + {name: "invalid-array", input: `{"h":{"k":["a",23]}}`, isSet: true, fail: true}, + {name: "missing", input: `{}`}, + } { + t.Run(tc.name, func(t *testing.T) { + dec := json.NewDecoder(strings.NewReader(tc.input)) + var testStruct testType + err := dec.Decode(&testStruct) + if tc.fail { + require.Error(t, err) + } else { + require.NoError(t, err) + assert.Equal(t, tc.isSet, testStruct.H.IsSet()) + assert.Equal(t, len(tc.val), len(testStruct.H.Val)) + for k, v := range tc.val { + assert.ElementsMatch(t, v, testStruct.H.Val.Values(k)) + } + } + + testStruct.H.Reset() + assert.False(t, testStruct.H.IsSet()) + assert.Empty(t, testStruct.H.Val) + + testStruct.H.Set(http.Header{"Accept": []string{"*/*"}}) + assert.True(t, testStruct.H.IsSet()) + assert.NotEmpty(t, testStruct.H.Val) + }) + } +} diff --git a/model/modeldecoder/rumv3/decoder.go b/model/modeldecoder/rumv3/decoder.go index 440d5c66eb4..41a11f42a47 100644 --- a/model/modeldecoder/rumv3/decoder.go +++ b/model/modeldecoder/rumv3/decoder.go @@ -19,19 +19,32 @@ package rumv3 import ( "fmt" + "net/http" + "net/textproto" + "strings" "sync" + "time" "github.com/elastic/beats/v7/libbeat/common" "github.com/elastic/apm-server/decoder" "github.com/elastic/apm-server/model" + "github.com/elastic/apm-server/model/modeldecoder" ) -var metadataRootPool = sync.Pool{ - New: func() interface{} { - return &metadataRoot{} - }, -} +var ( + metadataRootPool = sync.Pool{ + New: func() interface{} { + return &metadataRoot{} + }, + } + + transactionRootPool = sync.Pool{ + New: func() interface{} { + return &transactionRoot{} + }, + } +) func fetchMetadataRoot() *metadataRoot { return metadataRootPool.Get().(*metadataRoot) @@ -42,6 +55,33 @@ func releaseMetadataRoot(m *metadataRoot) { metadataRootPool.Put(m) } +func fetchTransactionRoot() *transactionRoot { + return transactionRootPool.Get().(*transactionRoot) +} + +func releaseTransactionRoot(m *transactionRoot) { + m.Reset() + transactionRootPool.Put(m) +} + +// DecodeNestedTransaction uses the given decoder to create the input model, +// then runs the defined validations on the input model +// and finally maps the values fom the input model to the given *model.Transaction instance +// +// DecodeNestedTransaction should be used when the decoder contains the `transaction` key +func DecodeNestedTransaction(d decoder.Decoder, input *modeldecoder.Input, out *model.Transaction) error { + root := fetchTransactionRoot() + defer releaseTransactionRoot(root) + if err := d.Decode(&root); err != nil { + return fmt.Errorf("decode error %w", err) + } + if err := root.validate(); err != nil { + return fmt.Errorf("validation error %w", err) + } + mapToTransactionModel(&root.Transaction, &input.Metadata, input.RequestTime, input.Config.Experimental, out) + return nil +} + // DecodeNestedMetadata uses the given decoder to create the input models, // then runs the defined validations on the input models // and finally maps the values fom the input model to the given *model.Metadata instance @@ -111,3 +151,220 @@ func mapToMetadataModel(m *metadata, out *model.Metadata) { out.User.Name = m.User.Name.Val } } + +func mapToTransactionModel(t *transaction, metadata *model.Metadata, reqTime time.Time, experimental bool, out *model.Transaction) { + if t == nil { + return + } + + // prefill with metadata information, then overwrite with event specific metadata + out.Metadata = *metadata + + // only set metadata Labels + out.Metadata.Labels = metadata.Labels.Clone() + + // overwrite Service values if set + if t.Context.Service.Agent.Name.IsSet() { + out.Metadata.Service.Agent.Name = t.Context.Service.Agent.Name.Val + } + if t.Context.Service.Agent.Version.IsSet() { + out.Metadata.Service.Agent.Version = t.Context.Service.Agent.Version.Val + } + if t.Context.Service.Environment.IsSet() { + out.Metadata.Service.Environment = t.Context.Service.Environment.Val + } + if t.Context.Service.Framework.Name.IsSet() { + out.Metadata.Service.Framework.Name = t.Context.Service.Framework.Name.Val + } + if t.Context.Service.Framework.Version.IsSet() { + out.Metadata.Service.Framework.Version = t.Context.Service.Framework.Version.Val + } + if t.Context.Service.Language.Name.IsSet() { + out.Metadata.Service.Language.Name = t.Context.Service.Language.Name.Val + } + if t.Context.Service.Language.Version.IsSet() { + out.Metadata.Service.Language.Version = t.Context.Service.Language.Version.Val + } + if t.Context.Service.Name.IsSet() { + out.Metadata.Service.Name = t.Context.Service.Name.Val + } + if t.Context.Service.Runtime.Name.IsSet() { + out.Metadata.Service.Runtime.Name = t.Context.Service.Runtime.Name.Val + } + if t.Context.Service.Runtime.Version.IsSet() { + out.Metadata.Service.Runtime.Version = t.Context.Service.Runtime.Version.Val + } + if t.Context.Service.Version.IsSet() { + out.Metadata.Service.Version = t.Context.Service.Version.Val + } + + // overwrite User specific values if set + // either populate all User fields or none to avoid mixing + // different user data + if t.Context.User.ID.IsSet() || t.Context.User.Email.IsSet() || t.Context.User.Name.IsSet() { + out.Metadata.User = model.User{} + if t.Context.User.ID.IsSet() { + out.Metadata.User.ID = fmt.Sprint(t.Context.User.ID.Val) + } + if t.Context.User.Email.IsSet() { + out.Metadata.User.Email = t.Context.User.Email.Val + } + if t.Context.User.Name.IsSet() { + out.Metadata.User.Name = t.Context.User.Name.Val + } + } + + if t.Context.Request.Headers.IsSet() { + if h := t.Context.Request.Headers.Val.Values(textproto.CanonicalMIMEHeaderKey("User-Agent")); len(h) > 0 { + out.Metadata.UserAgent.Original = strings.Join(h, ", ") + } + } + + // fill with event specific information + + // metadata labels and context labels are not merged at decoder level + // but in the output model + if len(t.Context.Tags) > 0 { + labels := model.Labels(t.Context.Tags.Clone()) + out.Labels = &labels + } + if t.Duration.IsSet() { + out.Duration = t.Duration.Val + } + if t.ID.IsSet() { + out.ID = t.ID.Val + } + if t.Marks.IsSet() { + out.Marks = make(model.TransactionMarks, len(t.Marks.Events)) + for event, val := range t.Marks.Events { + if len(val.Measurements) > 0 { + out.Marks[event] = model.TransactionMark(val.Measurements) + } + } + } + if t.Name.IsSet() { + out.Name = t.Name.Val + } + if t.Outcome.IsSet() { + out.Outcome = t.Outcome.Val + } else { + if t.Context.Response.StatusCode.IsSet() { + statusCode := t.Context.Response.StatusCode.Val + if statusCode >= http.StatusInternalServerError { + out.Outcome = "failure" + } else { + out.Outcome = "success" + } + } else { + out.Outcome = "unknown" + } + } + if t.ParentID.IsSet() { + out.ParentID = t.ParentID.Val + } + if t.Result.IsSet() { + out.Result = t.Result.Val + } + + sampled := true + if t.Sampled.IsSet() { + sampled = t.Sampled.Val + } + out.Sampled = &sampled + + // TODO(simitt): set accordingly, once this is fixed: + // https://github.com/elastic/apm-server/issues/4188 + // if t.SampleRate.IsSet() {} + + if t.SpanCount.Dropped.IsSet() { + dropped := t.SpanCount.Dropped.Val + out.SpanCount.Dropped = &dropped + } + if t.SpanCount.Started.IsSet() { + started := t.SpanCount.Started.Val + out.SpanCount.Started = &started + } + out.Timestamp = reqTime + if t.TraceID.IsSet() { + out.TraceID = t.TraceID.Val + } + if t.Type.IsSet() { + out.Type = t.Type.Val + } + if t.UserExperience.IsSet() { + out.UserExperience = &model.UserExperience{} + if t.UserExperience.CumulativeLayoutShift.IsSet() { + out.UserExperience.CumulativeLayoutShift = t.UserExperience.CumulativeLayoutShift.Val + } + if t.UserExperience.FirstInputDelay.IsSet() { + out.UserExperience.FirstInputDelay = t.UserExperience.FirstInputDelay.Val + + } + if t.UserExperience.TotalBlockingTime.IsSet() { + out.UserExperience.TotalBlockingTime = t.UserExperience.TotalBlockingTime.Val + } + } + if t.Context.IsSet() { + if t.Context.Page.IsSet() { + out.Page = &model.Page{} + if t.Context.Page.URL.IsSet() { + out.Page.URL = model.ParseURL(t.Context.Page.URL.Val, "") + } + if t.Context.Page.Referer.IsSet() { + referer := t.Context.Page.Referer.Val + out.Page.Referer = &referer + } + } + + if t.Context.Request.IsSet() { + var request model.Req + if t.Context.Request.Method.IsSet() { + request.Method = t.Context.Request.Method.Val + } + if t.Context.Request.Env.IsSet() { + request.Env = t.Context.Request.Env.Val + } + if t.Context.Request.Headers.IsSet() { + request.Headers = t.Context.Request.Headers.Val.Clone() + } + out.HTTP = &model.Http{Request: &request} + if t.Context.Request.HTTPVersion.IsSet() { + val := t.Context.Request.HTTPVersion.Val + out.HTTP.Version = &val + } + } + if t.Context.Response.IsSet() { + if out.HTTP == nil { + out.HTTP = &model.Http{} + } + var response model.Resp + if t.Context.Response.Headers.IsSet() { + response.Headers = t.Context.Response.Headers.Val.Clone() + } + if t.Context.Response.StatusCode.IsSet() { + val := t.Context.Response.StatusCode.Val + response.StatusCode = &val + } + if t.Context.Response.TransferSize.IsSet() { + val := t.Context.Response.TransferSize.Val + response.TransferSize = &val + } + if t.Context.Response.EncodedBodySize.IsSet() { + val := t.Context.Response.EncodedBodySize.Val + response.EncodedBodySize = &val + } + if t.Context.Response.DecodedBodySize.IsSet() { + val := t.Context.Response.DecodedBodySize.Val + response.DecodedBodySize = &val + } + out.HTTP.Response = &response + } + if len(t.Context.Custom) > 0 { + custom := model.Custom(t.Context.Custom.Clone()) + out.Custom = &custom + } + } + if experimental { + out.Experimental = t.Experimental.Val + } +} diff --git a/model/modeldecoder/rumv3/decoder_test.go b/model/modeldecoder/rumv3/decoder_test.go deleted file mode 100644 index a9384334396..00000000000 --- a/model/modeldecoder/rumv3/decoder_test.go +++ /dev/null @@ -1,104 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you under -// the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package rumv3 - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/elastic/beats/v7/libbeat/common" - - "github.com/elastic/apm-server/decoder" - "github.com/elastic/apm-server/model" - "github.com/elastic/apm-server/model/modeldecoder/modeldecodertest" -) - -func TestResetModelOnRelease(t *testing.T) { - inp := `{"m":{"se":{"n":"service-a"}}}` - m := fetchMetadataRoot() - require.NoError(t, decoder.NewJSONIteratorDecoder(strings.NewReader(inp)).Decode(m)) - require.True(t, m.IsSet()) - releaseMetadataRoot(m) - assert.False(t, m.IsSet()) -} - -func TestDecodeNestedMetadata(t *testing.T) { - t.Run("decode", func(t *testing.T) { - var out model.Metadata - testMinValidMetadata := `{"m":{"se":{"n":"name","a":{"n":"go","ve":"1.0.0"}}}}` - dec := decoder.NewJSONIteratorDecoder(strings.NewReader(testMinValidMetadata)) - require.NoError(t, DecodeNestedMetadata(dec, &out)) - assert.Equal(t, model.Metadata{Service: model.Service{ - Name: "name", - Agent: model.Agent{Name: "go", Version: "1.0.0"}}}, out) - - err := DecodeNestedMetadata(decoder.NewJSONIteratorDecoder(strings.NewReader(`malformed`)), &out) - require.Error(t, err) - assert.Contains(t, err.Error(), "decode") - }) - - t.Run("validate", func(t *testing.T) { - inp := `{}` - var out model.Metadata - err := DecodeNestedMetadata(decoder.NewJSONIteratorDecoder(strings.NewReader(inp)), &out) - require.Error(t, err) - assert.Contains(t, err.Error(), "validation") - }) - -} - -func TestMappingToModel(t *testing.T) { - expected := func(s string) model.Metadata { - return model.Metadata{ - Service: model.Service{Name: s, Version: s, Environment: s, - Agent: model.Agent{Name: s, Version: s}, - Language: model.Language{Name: s, Version: s}, - Runtime: model.Runtime{Name: s, Version: s}, - Framework: model.Framework{Name: s, Version: s}}, - User: model.User{Name: s, Email: s, ID: s}, - Labels: common.MapStr{s: s}, - } - } - - // setup: - // create initialized modeldecoder and empty model metadata - // map modeldecoder to model metadata and manually set - // enhanced data that are never set by the modeldecoder - var m metadata - modeldecodertest.SetStructValues(&m, "init", 5000) - var modelM model.Metadata - mapToMetadataModel(&m, &modelM) - // iterate through model and assert values are set - assert.Equal(t, expected("init"), modelM) - - // overwrite model metadata with specified Values - // then iterate through model and assert values are overwritten - modeldecodertest.SetStructValues(&m, "overwritten", 12) - mapToMetadataModel(&m, &modelM) - assert.Equal(t, expected("overwritten"), modelM) - - // map an empty modeldecoder metadata to the model - // and assert values are unchanged - modeldecodertest.SetZeroStructValues(&m) - mapToMetadataModel(&m, &modelM) - assert.Equal(t, expected("overwritten"), modelM) - -} diff --git a/model/modeldecoder/rumv3/model_test.go b/model/modeldecoder/rumv3/metadata_test.go similarity index 57% rename from model/modeldecoder/rumv3/model_test.go rename to model/modeldecoder/rumv3/metadata_test.go index b04866494ce..9efa450676a 100644 --- a/model/modeldecoder/rumv3/model_test.go +++ b/model/modeldecoder/rumv3/metadata_test.go @@ -18,39 +18,39 @@ package rumv3 import ( - "bytes" - "encoding/json" + "fmt" "io" "os" + "path/filepath" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/elastic/apm-server/decoder" + "github.com/elastic/apm-server/model" "github.com/elastic/apm-server/model/modeldecoder/modeldecodertest" + "github.com/elastic/beats/v7/libbeat/common" ) -func testdata(t *testing.T) io.Reader { - r, err := os.Open("../../../testdata/intake-v3/metadata.ndjson") - require.NoError(t, err) - return r +type testcase struct { + name string + errorKey string + data string } -func TestIsSet(t *testing.T) { - data := `{"se":{"n":"user-service"}}` - var m metadata - require.NoError(t, decoder.NewJSONIteratorDecoder(strings.NewReader(data)).Decode(&m)) - assert.True(t, m.IsSet()) - assert.True(t, m.Service.IsSet()) - assert.True(t, m.Service.Name.IsSet()) - assert.False(t, m.Service.Language.IsSet()) +func testdataReader(t *testing.T, typ string) io.Reader { + p := filepath.Join("..", "..", "..", "testdata", "intake-v3", fmt.Sprintf("%s.ndjson", typ)) + r, err := os.Open(p) + require.NoError(t, err) + return r } -func TestSetReset(t *testing.T) { +func TestSetResetIsSet(t *testing.T) { var m metadataRoot - require.NoError(t, decoder.NewJSONIteratorDecoder(testdata(t)).Decode(&m)) + require.NoError(t, decoder.NewJSONIteratorDecoder(testdataReader(t, "metadata")).Decode(&m)) require.True(t, m.IsSet()) require.NotEmpty(t, m.Metadata.Labels) require.True(t, m.Metadata.Service.IsSet()) @@ -59,42 +59,90 @@ func TestSetReset(t *testing.T) { m.Reset() assert.False(t, m.IsSet()) assert.Equal(t, metadataService{}, m.Metadata.Service) - assert.Equal(t, metadataUser{}, m.Metadata.User) + assert.Equal(t, user{}, m.Metadata.User) assert.Empty(t, m.Metadata.Labels) } -func TestValidationRules(t *testing.T) { - type testcase struct { - name string - errorKey string - data string - } +func TestMetadataResetModelOnRelease(t *testing.T) { + inp := `{"m":{"se":{"n":"service-a"}}}` + m := fetchMetadataRoot() + require.NoError(t, decoder.NewJSONIteratorDecoder(strings.NewReader(inp)).Decode(m)) + require.True(t, m.IsSet()) + releaseMetadataRoot(m) + assert.False(t, m.IsSet()) +} + +func TestDecodeNestedMetadata(t *testing.T) { + t.Run("decode", func(t *testing.T) { + var out model.Metadata + testMinValidMetadata := `{"m":{"se":{"n":"name","a":{"n":"go","ve":"1.0.0"}}}}` + dec := decoder.NewJSONIteratorDecoder(strings.NewReader(testMinValidMetadata)) + require.NoError(t, DecodeNestedMetadata(dec, &out)) + assert.Equal(t, model.Metadata{Service: model.Service{ + Name: "name", + Agent: model.Agent{Name: "go", Version: "1.0.0"}}}, out) + + err := DecodeNestedMetadata(decoder.NewJSONIteratorDecoder(strings.NewReader(`malformed`)), &out) + require.Error(t, err) + assert.Contains(t, err.Error(), "decode") + }) + + t.Run("validate", func(t *testing.T) { + inp := `{}` + var out model.Metadata + err := DecodeNestedMetadata(decoder.NewJSONIteratorDecoder(strings.NewReader(inp)), &out) + require.Error(t, err) + assert.Contains(t, err.Error(), "validation") + }) - strBuilder := func(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = '⌘' +} + +func TestDecodeMetadataMappingToModel(t *testing.T) { + expected := func(s string) model.Metadata { + return model.Metadata{ + Service: model.Service{Name: s, Version: s, Environment: s, + Agent: model.Agent{Name: s, Version: s}, + Language: model.Language{Name: s, Version: s}, + Runtime: model.Runtime{Name: s, Version: s}, + Framework: model.Framework{Name: s, Version: s}}, + User: model.User{Name: s, Email: s, ID: s}, + Labels: common.MapStr{s: s}, } - return string(b) } + // setup: + // create initialized modeldecoder and empty model metadata + // map modeldecoder to model metadata and manually set + // enhanced data that are never set by the modeldecoder + var m metadata + modeldecodertest.SetStructValues(&m, "init", 5000, false, time.Now()) + var modelM model.Metadata + mapToMetadataModel(&m, &modelM) + // iterate through model and assert values are set + assert.Equal(t, expected("init"), modelM) + + // overwrite model metadata with specified Values + // then iterate through model and assert values are overwritten + modeldecodertest.SetStructValues(&m, "overwritten", 12, false, time.Now()) + mapToMetadataModel(&m, &modelM) + assert.Equal(t, expected("overwritten"), modelM) + + // map an empty modeldecoder metadata to the model + // and assert values are unchanged + modeldecodertest.SetZeroStructValues(&m) + mapToMetadataModel(&m, &modelM) + assert.Equal(t, expected("overwritten"), modelM) + +} + +func TestValidationRules(t *testing.T) { testMetadata := func(t *testing.T, key string, tc testcase) { - // load data - // set testcase data for given key - var data map[string]interface{} - require.NoError(t, decoder.NewJSONIteratorDecoder(testdata(t)).Decode(&data)) - meta := data["m"].(map[string]interface{}) - var keyData map[string]interface{} - require.NoError(t, json.Unmarshal([]byte(tc.data), &keyData)) - meta[key] = keyData - - // unmarshal data into metdata struct var m metadata - b, err := json.Marshal(meta) - require.NoError(t, err) - require.NoError(t, decoder.NewJSONIteratorDecoder(bytes.NewReader(b)).Decode(&m)) + r := testdataReader(t, "metadata") + modeldecodertest.DecodeDataWithReplacement(t, r, "m", key, tc.data, &m) + // run validation and checks - err = m.validate() + err := m.validate() if tc.errorKey == "" { assert.NoError(t, err) } else { @@ -109,8 +157,8 @@ func TestValidationRules(t *testing.T) { {name: "id-int", data: `{"id":44}`}, {name: "id-float", errorKey: "types", data: `{"id":45.6}`}, {name: "id-bool", errorKey: "types", data: `{"id":true}`}, - {name: "id-string-max-len", data: `{"id":"` + strBuilder(1024) + `"}`}, - {name: "id-string-max-len", errorKey: "max", data: `{"id":"` + strBuilder(1025) + `"}`}, + {name: "id-string-max-len", data: `{"id":"` + modeldecodertest.BuildString(1024) + `"}`}, + {name: "id-string-max-len", errorKey: "max", data: `{"id":"` + modeldecodertest.BuildString(1025) + `"}`}, } { t.Run(tc.name, func(t *testing.T) { testMetadata(t, "u", tc) @@ -136,8 +184,8 @@ func TestValidationRules(t *testing.T) { t.Run("max-len", func(t *testing.T) { for _, tc := range []testcase{ - {name: "service-environment-max-len", data: `"en":"` + strBuilder(1024) + `"`}, - {name: "service-environment-max-len", errorKey: "max", data: `"en":"` + strBuilder(1025) + `"`}, + {name: "service-environment-max-len", data: `"en":"` + modeldecodertest.BuildString(1024) + `"`}, + {name: "service-environment-max-len", errorKey: "max", data: `"en":"` + modeldecodertest.BuildString(1025) + `"`}, } { t.Run(tc.name, func(t *testing.T) { tc.data = `{"a":{"n":"go","ve":"1.0"},"n":"my-service",` + tc.data + `}` @@ -153,8 +201,8 @@ func TestValidationRules(t *testing.T) { {name: "key-dot", errorKey: "patternKeys", data: `{"k.1":"v1"}`}, {name: "key-asterisk", errorKey: "patternKeys", data: `{"k*1":"v1"}`}, {name: "key-quotemark", errorKey: "patternKeys", data: `{"k\"1":"v1"}`}, - {name: "max-len", data: `{"k1":"` + strBuilder(1024) + `"}`}, - {name: "max-len-exceeded", errorKey: "maxVals", data: `{"k1":"` + strBuilder(1025) + `"}`}, + {name: "max-len", data: `{"k1":"` + modeldecodertest.BuildString(1024) + `"}`}, + {name: "max-len-exceeded", errorKey: "maxVals", data: `{"k1":"` + modeldecodertest.BuildString(1025) + `"}`}, } { t.Run(tc.name, func(t *testing.T) { testMetadata(t, "l", tc) diff --git a/model/modeldecoder/rumv3/model.go b/model/modeldecoder/rumv3/model.go index 346a10204f3..00b3db9472e 100644 --- a/model/modeldecoder/rumv3/model.go +++ b/model/modeldecoder/rumv3/model.go @@ -18,6 +18,7 @@ package rumv3 import ( + "encoding/json" "regexp" "github.com/elastic/beats/v7/libbeat/common" @@ -26,36 +27,106 @@ import ( ) var ( - alphaNumericExtRegex = regexp.MustCompile("^[a-zA-Z0-9 _-]+$") - labelsRegex = regexp.MustCompile("^[^.*\"]*$") //do not allow '.' '*' '"' + regexpAlphaNumericExt = regexp.MustCompile("^[a-zA-Z0-9 _-]+$") + regexpNoDotAsteriskQuote = regexp.MustCompile("^[^.*\"]*$") //do not allow '.' '*' '"' + + enumOutcome = []string{"success", "failure", "unknown"} ) +// entry points + type metadataRoot struct { Metadata metadata `json:"m" validate:"required"` } +type transactionRoot struct { + Transaction transaction `json:"x" validate:"required"` +} + +// other structs + +type context struct { + Custom common.MapStr `json:"cu" validate:"patternKeys=regexpNoDotAsteriskQuote"` + Page contextPage `json:"p"` + Request contextRequest `json:"q"` + Response contextResponse `json:"r"` + Service contextService `json:"se"` + Tags common.MapStr `json:"g" validate:"patternKeys=regexpNoDotAsteriskQuote,typesVals=string;bool;number,maxVals=1024"` + User user `json:"u"` +} + +type contextPage struct { + URL nullable.String `json:"url"` + Referer nullable.String `json:"rf"` +} + +type contextRequest struct { + Env nullable.Interface `json:"en"` + Headers nullable.HTTPHeader `json:"he"` + HTTPVersion nullable.String `json:"hve" validate:"max=1024"` + Method nullable.String `json:"mt" validate:"required,max=1024"` +} + +type contextResponse struct { + DecodedBodySize nullable.Float64 `json:"dbs"` + EncodedBodySize nullable.Float64 `json:"ebs"` + Headers nullable.HTTPHeader `json:"he"` + StatusCode nullable.Int `json:"sc"` + TransferSize nullable.Float64 `json:"ts"` +} + +type contextService struct { + Agent contextServiceAgent `json:"a"` + Environment nullable.String `json:"en" validate:"max=1024"` + Framework contextServiceFramework `json:"fw"` + Language contextServiceLanguage `json:"la"` + Name nullable.String `json:"n" validate:"max=1024,pattern=regexpAlphaNumericExt"` + Runtime contextServiceRuntime `json:"ru"` + Version nullable.String `json:"ve" validate:"max=1024"` +} + +type contextServiceAgent struct { + Name nullable.String `json:"n" validate:"max=1024"` + Version nullable.String `json:"ve" validate:"max=1024"` +} + +type contextServiceFramework struct { + Name nullable.String `json:"n" validate:"max=1024"` + Version nullable.String `json:"ve" validate:"max=1024"` +} + +type contextServiceLanguage struct { + Name nullable.String `json:"n" validate:"max=1024"` + Version nullable.String `json:"ve" validate:"max=1024"` +} + +type contextServiceRuntime struct { + Name nullable.String `json:"n" validate:"max=1024"` + Version nullable.String `json:"ve" validate:"max=1024"` +} + type metadata struct { - Labels common.MapStr `json:"l" validate:"patternKeys=labelsRegex,typesVals=string;bool;number,maxVals=1024"` + Labels common.MapStr `json:"l" validate:"patternKeys=regexpNoDotAsteriskQuote,typesVals=string;bool;number,maxVals=1024"` Service metadataService `json:"se" validate:"required"` - User metadataUser `json:"u"` + User user `json:"u"` } type metadataService struct { Agent metadataServiceAgent `json:"a" validate:"required"` Environment nullable.String `json:"en" validate:"max=1024"` - Framework MetadataServiceFramework `json:"fw"` + Framework metadataServiceFramework `json:"fw"` Language metadataServiceLanguage `json:"la"` - Name nullable.String `json:"n" validate:"required,max=1024,pattern=alphaNumericExtRegex"` + Name nullable.String `json:"n" validate:"required,min=1,max=1024,pattern=regexpAlphaNumericExt"` Runtime metadataServiceRuntime `json:"ru"` Version nullable.String `json:"ve" validate:"max=1024"` } type metadataServiceAgent struct { - Name nullable.String `json:"n" validate:"required,max=1024"` + Name nullable.String `json:"n" validate:"required,min=1,max=1024"` Version nullable.String `json:"ve" validate:"required,max=1024"` } -type MetadataServiceFramework struct { +type metadataServiceFramework struct { Name nullable.String `json:"n" validate:"max=1024"` Version nullable.String `json:"ve" validate:"max=1024"` } @@ -70,7 +141,63 @@ type metadataServiceRuntime struct { Version nullable.String `json:"ve" validate:"required,max=1024"` } -type metadataUser struct { +type transaction struct { + Context context `json:"c"` + Duration nullable.Float64 `json:"d" validate:"required,min=0"` + ID nullable.String `json:"id" validate:"required,max=1024"` + Marks transactionMarks `json:"k"` + Name nullable.String `json:"n" validate:"max=1024"` + Outcome nullable.String `json:"o" validate:"enum=enumOutcome"` + ParentID nullable.String `json:"pid" validate:"max=1024"` + Result nullable.String `json:"rt" validate:"max=1024"` + Sampled nullable.Bool `json:"sm"` + SampleRate nullable.Float64 `json:"sr"` + SpanCount transactionSpanCount `json:"yc" validate:"required"` + TraceID nullable.String `json:"tid" validate:"required,max=1024"` + Type nullable.String `json:"t" validate:"required,max=1024"` + UserExperience transactionUserExperience `json:"exp"` + Experimental nullable.Interface `json:"exper"` +} + +type transactionMarks struct { + Events map[string]transactionMarkEvents `json:"-" validate:"patternKeys=regexpNoDotAsteriskQuote"` +} + +//TODO(simitt): generate +func (m *transactionMarks) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &m.Events) +} + +type transactionMarkEvents struct { + Measurements map[string]float64 `json:"-" validate:"patternKeys=regexpNoDotAsteriskQuote"` +} + +//TODO(simitt): generate +func (m *transactionMarkEvents) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &m.Measurements) +} + +type transactionSpanCount struct { + Dropped nullable.Int `json:"dd"` + Started nullable.Int `json:"sd" validate:"required"` +} + +// userExperience holds real user (browser) experience metrics. +type transactionUserExperience struct { + // CumulativeLayoutShift holds the Cumulative Layout Shift (CLS) metric value, + // or a negative value if CLS is unknown. See https://web.dev/cls/ + CumulativeLayoutShift nullable.Float64 `json:"cls" validate:"min=0"` + + // FirstInputDelay holds the First Input Delay (FID) metric value, + // or a negative value if FID is unknown. See https://web.dev/fid/ + FirstInputDelay nullable.Float64 `json:"fid" validate:"min=0"` + + // TotalBlockingTime holds the Total Blocking Time (TBT) metric value, + // or a negative value if TBT is unknown. See https://web.dev/tbt/ + TotalBlockingTime nullable.Float64 `json:"tbt" validate:"min=0"` +} + +type user struct { ID nullable.Interface `json:"id" validate:"max=1024,types=string;int"` Email nullable.String `json:"em" validate:"max=1024"` Name nullable.String `json:"un" validate:"max=1024"` diff --git a/model/modeldecoder/rumv3/model_generated.go b/model/modeldecoder/rumv3/model_generated.go index b5cf96e5625..dba3d78f043 100644 --- a/model/modeldecoder/rumv3/model_generated.go +++ b/model/modeldecoder/rumv3/model_generated.go @@ -20,48 +20,49 @@ package rumv3 import ( - "encoding/json" "fmt" + + "encoding/json" "unicode/utf8" ) -func (m *metadataRoot) IsSet() bool { - return m.Metadata.IsSet() +func (val *metadataRoot) IsSet() bool { + return val.Metadata.IsSet() } -func (m *metadataRoot) Reset() { - m.Metadata.Reset() +func (val *metadataRoot) Reset() { + val.Metadata.Reset() } -func (m *metadataRoot) validate() error { - if err := m.Metadata.validate(); err != nil { +func (val *metadataRoot) validate() error { + if err := val.Metadata.validate(); err != nil { return err } - if !m.Metadata.IsSet() { + if !val.Metadata.IsSet() { return fmt.Errorf("'m' required") } return nil } -func (m *metadata) IsSet() bool { - return len(m.Labels) > 0 || m.Service.IsSet() || m.User.IsSet() +func (val *metadata) IsSet() bool { + return len(val.Labels) > 0 || val.Service.IsSet() || val.User.IsSet() } -func (m *metadata) Reset() { - for k := range m.Labels { - delete(m.Labels, k) +func (val *metadata) Reset() { + for k := range val.Labels { + delete(val.Labels, k) } - m.Service.Reset() - m.User.Reset() + val.Service.Reset() + val.User.Reset() } -func (m *metadata) validate() error { - if !m.IsSet() { +func (val *metadata) validate() error { + if !val.IsSet() { return nil } - for k, v := range m.Labels { - if !labelsRegex.MatchString(k) { - return fmt.Errorf("validation rule 'patternKeys(labelsRegex)' violated for 'm.l'") + for k, v := range val.Labels { + if k != "" && !regexpNoDotAsteriskQuote.MatchString(k) { + return fmt.Errorf("validation rule 'patternKeys(regexpNoDotAsteriskQuote)' violated for 'm.l'") } switch t := v.(type) { case nil: @@ -75,205 +76,666 @@ func (m *metadata) validate() error { return fmt.Errorf("validation rule 'typesVals(string;bool;number)' violated for 'm.l' for key %s", k) } } - if err := m.Service.validate(); err != nil { + if err := val.Service.validate(); err != nil { return err } - if !m.Service.IsSet() { + if !val.Service.IsSet() { return fmt.Errorf("'m.se' required") } - if err := m.User.validate(); err != nil { + if err := val.User.validate(); err != nil { return err } return nil } -func (m *metadataService) IsSet() bool { - return m.Agent.IsSet() || m.Environment.IsSet() || m.Framework.IsSet() || m.Language.IsSet() || m.Name.IsSet() || m.Runtime.IsSet() || m.Version.IsSet() +func (val *metadataService) IsSet() bool { + return val.Agent.IsSet() || val.Environment.IsSet() || val.Framework.IsSet() || val.Language.IsSet() || val.Name.IsSet() || val.Runtime.IsSet() || val.Version.IsSet() } -func (m *metadataService) Reset() { - m.Agent.Reset() - m.Environment.Reset() - m.Framework.Reset() - m.Language.Reset() - m.Name.Reset() - m.Runtime.Reset() - m.Version.Reset() +func (val *metadataService) Reset() { + val.Agent.Reset() + val.Environment.Reset() + val.Framework.Reset() + val.Language.Reset() + val.Name.Reset() + val.Runtime.Reset() + val.Version.Reset() } -func (m *metadataService) validate() error { - if !m.IsSet() { +func (val *metadataService) validate() error { + if !val.IsSet() { return nil } - if err := m.Agent.validate(); err != nil { + if err := val.Agent.validate(); err != nil { return err } - if !m.Agent.IsSet() { + if !val.Agent.IsSet() { return fmt.Errorf("'m.se.a' required") } - if utf8.RuneCountInString(m.Environment.Val) > 1024 { + if utf8.RuneCountInString(val.Environment.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.se.en'") } - if err := m.Framework.validate(); err != nil { + if err := val.Framework.validate(); err != nil { return err } - if err := m.Language.validate(); err != nil { + if err := val.Language.validate(); err != nil { return err } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.se.n'") } - if !alphaNumericExtRegex.MatchString(m.Name.Val) { - return fmt.Errorf("validation rule 'pattern(alphaNumericExtRegex)' violated for 'm.se.n'") + if utf8.RuneCountInString(val.Name.Val) < 1 { + return fmt.Errorf("validation rule 'min(1)' violated for 'm.se.n'") + } + if val.Name.Val != "" && !regexpAlphaNumericExt.MatchString(val.Name.Val) { + return fmt.Errorf("validation rule 'pattern(regexpAlphaNumericExt)' violated for 'm.se.n'") } - if !m.Name.IsSet() { + if !val.Name.IsSet() { return fmt.Errorf("'m.se.n' required") } - if err := m.Runtime.validate(); err != nil { + if err := val.Runtime.validate(); err != nil { return err } - if utf8.RuneCountInString(m.Version.Val) > 1024 { + if utf8.RuneCountInString(val.Version.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.se.ve'") } return nil } -func (m *metadataServiceAgent) IsSet() bool { - return m.Name.IsSet() || m.Version.IsSet() +func (val *metadataServiceAgent) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() } -func (m *metadataServiceAgent) Reset() { - m.Name.Reset() - m.Version.Reset() +func (val *metadataServiceAgent) Reset() { + val.Name.Reset() + val.Version.Reset() } -func (m *metadataServiceAgent) validate() error { - if !m.IsSet() { +func (val *metadataServiceAgent) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.se.a.n'") } - if !m.Name.IsSet() { + if utf8.RuneCountInString(val.Name.Val) < 1 { + return fmt.Errorf("validation rule 'min(1)' violated for 'm.se.a.n'") + } + if !val.Name.IsSet() { return fmt.Errorf("'m.se.a.n' required") } - if utf8.RuneCountInString(m.Version.Val) > 1024 { + if utf8.RuneCountInString(val.Version.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.se.a.ve'") } - if !m.Version.IsSet() { + if !val.Version.IsSet() { return fmt.Errorf("'m.se.a.ve' required") } return nil } -func (m *MetadataServiceFramework) IsSet() bool { - return m.Name.IsSet() || m.Version.IsSet() +func (val *metadataServiceFramework) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() } -func (m *MetadataServiceFramework) Reset() { - m.Name.Reset() - m.Version.Reset() +func (val *metadataServiceFramework) Reset() { + val.Name.Reset() + val.Version.Reset() } -func (m *MetadataServiceFramework) validate() error { - if !m.IsSet() { +func (val *metadataServiceFramework) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.se.fw.n'") } - if utf8.RuneCountInString(m.Version.Val) > 1024 { + if utf8.RuneCountInString(val.Version.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.se.fw.ve'") } return nil } -func (m *metadataServiceLanguage) IsSet() bool { - return m.Name.IsSet() || m.Version.IsSet() +func (val *metadataServiceLanguage) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() } -func (m *metadataServiceLanguage) Reset() { - m.Name.Reset() - m.Version.Reset() +func (val *metadataServiceLanguage) Reset() { + val.Name.Reset() + val.Version.Reset() } -func (m *metadataServiceLanguage) validate() error { - if !m.IsSet() { +func (val *metadataServiceLanguage) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.se.la.n'") } - if !m.Name.IsSet() { + if !val.Name.IsSet() { return fmt.Errorf("'m.se.la.n' required") } - if utf8.RuneCountInString(m.Version.Val) > 1024 { + if utf8.RuneCountInString(val.Version.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.se.la.ve'") } return nil } -func (m *metadataServiceRuntime) IsSet() bool { - return m.Name.IsSet() || m.Version.IsSet() +func (val *metadataServiceRuntime) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() } -func (m *metadataServiceRuntime) Reset() { - m.Name.Reset() - m.Version.Reset() +func (val *metadataServiceRuntime) Reset() { + val.Name.Reset() + val.Version.Reset() } -func (m *metadataServiceRuntime) validate() error { - if !m.IsSet() { +func (val *metadataServiceRuntime) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.se.ru.n'") } - if !m.Name.IsSet() { + if !val.Name.IsSet() { return fmt.Errorf("'m.se.ru.n' required") } - if utf8.RuneCountInString(m.Version.Val) > 1024 { + if utf8.RuneCountInString(val.Version.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.se.ru.ve'") } - if !m.Version.IsSet() { + if !val.Version.IsSet() { return fmt.Errorf("'m.se.ru.ve' required") } return nil } -func (m *metadataUser) IsSet() bool { - return m.ID.IsSet() || m.Email.IsSet() || m.Name.IsSet() +func (val *user) IsSet() bool { + return val.ID.IsSet() || val.Email.IsSet() || val.Name.IsSet() } -func (m *metadataUser) Reset() { - m.ID.Reset() - m.Email.Reset() - m.Name.Reset() +func (val *user) Reset() { + val.ID.Reset() + val.Email.Reset() + val.Name.Reset() } -func (m *metadataUser) validate() error { - if !m.IsSet() { +func (val *user) validate() error { + if !val.IsSet() { return nil } - switch t := m.ID.Val.(type) { + switch t := val.ID.Val.(type) { case string: if utf8.RuneCountInString(t) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.u.id'") } + case int: case json.Number: if _, err := t.Int64(); err != nil { return fmt.Errorf("validation rule 'types(string;int)' violated for 'm.u.id'") } - case int: case nil: default: return fmt.Errorf("validation rule 'types(string;int)' violated for 'm.u.id'") } - if utf8.RuneCountInString(m.Email.Val) > 1024 { + if utf8.RuneCountInString(val.Email.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.u.em'") } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'm.u.un'") } return nil } + +func (val *transactionRoot) IsSet() bool { + return val.Transaction.IsSet() +} + +func (val *transactionRoot) Reset() { + val.Transaction.Reset() +} + +func (val *transactionRoot) validate() error { + if err := val.Transaction.validate(); err != nil { + return err + } + if !val.Transaction.IsSet() { + return fmt.Errorf("'x' required") + } + return nil +} + +func (val *transaction) IsSet() bool { + return val.Context.IsSet() || val.Duration.IsSet() || val.ID.IsSet() || val.Marks.IsSet() || val.Name.IsSet() || val.Outcome.IsSet() || val.ParentID.IsSet() || val.Result.IsSet() || val.Sampled.IsSet() || val.SampleRate.IsSet() || val.SpanCount.IsSet() || val.TraceID.IsSet() || val.Type.IsSet() || val.UserExperience.IsSet() || val.Experimental.IsSet() +} + +func (val *transaction) Reset() { + val.Context.Reset() + val.Duration.Reset() + val.ID.Reset() + val.Marks.Reset() + val.Name.Reset() + val.Outcome.Reset() + val.ParentID.Reset() + val.Result.Reset() + val.Sampled.Reset() + val.SampleRate.Reset() + val.SpanCount.Reset() + val.TraceID.Reset() + val.Type.Reset() + val.UserExperience.Reset() + val.Experimental.Reset() +} + +func (val *transaction) validate() error { + if !val.IsSet() { + return nil + } + if err := val.Context.validate(); err != nil { + return err + } + if val.Duration.Val < 0 { + return fmt.Errorf("validation rule 'min(0)' violated for 'x.d'") + } + if !val.Duration.IsSet() { + return fmt.Errorf("'x.d' required") + } + if utf8.RuneCountInString(val.ID.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.id'") + } + if !val.ID.IsSet() { + return fmt.Errorf("'x.id' required") + } + if err := val.Marks.validate(); err != nil { + return err + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.n'") + } + if val.Outcome.Val != "" { + var matchEnum bool + for _, s := range enumOutcome { + if val.Outcome.Val == s { + matchEnum = true + break + } + } + if !matchEnum { + return fmt.Errorf("validation rule 'enum(enumOutcome)' violated for 'x.o'") + } + } + if utf8.RuneCountInString(val.ParentID.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.pid'") + } + if utf8.RuneCountInString(val.Result.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.rt'") + } + if err := val.SpanCount.validate(); err != nil { + return err + } + if !val.SpanCount.IsSet() { + return fmt.Errorf("'x.yc' required") + } + if utf8.RuneCountInString(val.TraceID.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.tid'") + } + if !val.TraceID.IsSet() { + return fmt.Errorf("'x.tid' required") + } + if utf8.RuneCountInString(val.Type.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.t'") + } + if !val.Type.IsSet() { + return fmt.Errorf("'x.t' required") + } + if err := val.UserExperience.validate(); err != nil { + return err + } + return nil +} + +func (val *context) IsSet() bool { + return len(val.Custom) > 0 || val.Page.IsSet() || val.Request.IsSet() || val.Response.IsSet() || val.Service.IsSet() || len(val.Tags) > 0 || val.User.IsSet() +} + +func (val *context) Reset() { + for k := range val.Custom { + delete(val.Custom, k) + } + val.Page.Reset() + val.Request.Reset() + val.Response.Reset() + val.Service.Reset() + for k := range val.Tags { + delete(val.Tags, k) + } + val.User.Reset() +} + +func (val *context) validate() error { + if !val.IsSet() { + return nil + } + for k := range val.Custom { + if k != "" && !regexpNoDotAsteriskQuote.MatchString(k) { + return fmt.Errorf("validation rule 'patternKeys(regexpNoDotAsteriskQuote)' violated for 'x.c.cu'") + } + } + if err := val.Page.validate(); err != nil { + return err + } + if err := val.Request.validate(); err != nil { + return err + } + if err := val.Response.validate(); err != nil { + return err + } + if err := val.Service.validate(); err != nil { + return err + } + for k, v := range val.Tags { + if k != "" && !regexpNoDotAsteriskQuote.MatchString(k) { + return fmt.Errorf("validation rule 'patternKeys(regexpNoDotAsteriskQuote)' violated for 'x.c.g'") + } + switch t := v.(type) { + case nil: + case string: + if utf8.RuneCountInString(t) > 1024 { + return fmt.Errorf("validation rule 'maxVals(1024)' violated for 'x.c.g'") + } + case bool: + case json.Number: + default: + return fmt.Errorf("validation rule 'typesVals(string;bool;number)' violated for 'x.c.g' for key %s", k) + } + } + if err := val.User.validate(); err != nil { + return err + } + return nil +} + +func (val *contextPage) IsSet() bool { + return val.URL.IsSet() || val.Referer.IsSet() +} + +func (val *contextPage) Reset() { + val.URL.Reset() + val.Referer.Reset() +} + +func (val *contextPage) validate() error { + if !val.IsSet() { + return nil + } + return nil +} + +func (val *contextRequest) IsSet() bool { + return val.Env.IsSet() || val.Headers.IsSet() || val.HTTPVersion.IsSet() || val.Method.IsSet() +} + +func (val *contextRequest) Reset() { + val.Env.Reset() + val.Headers.Reset() + val.HTTPVersion.Reset() + val.Method.Reset() +} + +func (val *contextRequest) validate() error { + if !val.IsSet() { + return nil + } + if utf8.RuneCountInString(val.HTTPVersion.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.q.hve'") + } + if utf8.RuneCountInString(val.Method.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.q.mt'") + } + if !val.Method.IsSet() { + return fmt.Errorf("'x.c.q.mt' required") + } + return nil +} + +func (val *contextResponse) IsSet() bool { + return val.DecodedBodySize.IsSet() || val.EncodedBodySize.IsSet() || val.Headers.IsSet() || val.StatusCode.IsSet() || val.TransferSize.IsSet() +} + +func (val *contextResponse) Reset() { + val.DecodedBodySize.Reset() + val.EncodedBodySize.Reset() + val.Headers.Reset() + val.StatusCode.Reset() + val.TransferSize.Reset() +} + +func (val *contextResponse) validate() error { + if !val.IsSet() { + return nil + } + return nil +} + +func (val *contextService) IsSet() bool { + return val.Agent.IsSet() || val.Environment.IsSet() || val.Framework.IsSet() || val.Language.IsSet() || val.Name.IsSet() || val.Runtime.IsSet() || val.Version.IsSet() +} + +func (val *contextService) Reset() { + val.Agent.Reset() + val.Environment.Reset() + val.Framework.Reset() + val.Language.Reset() + val.Name.Reset() + val.Runtime.Reset() + val.Version.Reset() +} + +func (val *contextService) validate() error { + if !val.IsSet() { + return nil + } + if err := val.Agent.validate(); err != nil { + return err + } + if utf8.RuneCountInString(val.Environment.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.se.en'") + } + if err := val.Framework.validate(); err != nil { + return err + } + if err := val.Language.validate(); err != nil { + return err + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.se.n'") + } + if val.Name.Val != "" && !regexpAlphaNumericExt.MatchString(val.Name.Val) { + return fmt.Errorf("validation rule 'pattern(regexpAlphaNumericExt)' violated for 'x.c.se.n'") + } + if err := val.Runtime.validate(); err != nil { + return err + } + if utf8.RuneCountInString(val.Version.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.se.ve'") + } + return nil +} + +func (val *contextServiceAgent) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() +} + +func (val *contextServiceAgent) Reset() { + val.Name.Reset() + val.Version.Reset() +} + +func (val *contextServiceAgent) validate() error { + if !val.IsSet() { + return nil + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.se.a.n'") + } + if utf8.RuneCountInString(val.Version.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.se.a.ve'") + } + return nil +} + +func (val *contextServiceFramework) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() +} + +func (val *contextServiceFramework) Reset() { + val.Name.Reset() + val.Version.Reset() +} + +func (val *contextServiceFramework) validate() error { + if !val.IsSet() { + return nil + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.se.fw.n'") + } + if utf8.RuneCountInString(val.Version.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.se.fw.ve'") + } + return nil +} + +func (val *contextServiceLanguage) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() +} + +func (val *contextServiceLanguage) Reset() { + val.Name.Reset() + val.Version.Reset() +} + +func (val *contextServiceLanguage) validate() error { + if !val.IsSet() { + return nil + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.se.la.n'") + } + if utf8.RuneCountInString(val.Version.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.se.la.ve'") + } + return nil +} + +func (val *contextServiceRuntime) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() +} + +func (val *contextServiceRuntime) Reset() { + val.Name.Reset() + val.Version.Reset() +} + +func (val *contextServiceRuntime) validate() error { + if !val.IsSet() { + return nil + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.se.ru.n'") + } + if utf8.RuneCountInString(val.Version.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'x.c.se.ru.ve'") + } + return nil +} + +func (val *transactionMarks) IsSet() bool { + return len(val.Events) > 0 +} + +func (val *transactionMarks) Reset() { + for k := range val.Events { + delete(val.Events, k) + } +} + +func (val *transactionMarks) validate() error { + if !val.IsSet() { + return nil + } + for k, v := range val.Events { + if k != "" && !regexpNoDotAsteriskQuote.MatchString(k) { + return fmt.Errorf("validation rule 'patternKeys(regexpNoDotAsteriskQuote)' violated for 'x.k.events'") + } + if err := v.validate(); err != nil { + return err + } + } + return nil +} + +func (val *transactionMarkEvents) IsSet() bool { + return len(val.Measurements) > 0 +} + +func (val *transactionMarkEvents) Reset() { + for k := range val.Measurements { + delete(val.Measurements, k) + } +} + +func (val *transactionMarkEvents) validate() error { + if !val.IsSet() { + return nil + } + for k := range val.Measurements { + if k != "" && !regexpNoDotAsteriskQuote.MatchString(k) { + return fmt.Errorf("validation rule 'patternKeys(regexpNoDotAsteriskQuote)' violated for 'x.k.events.measurements'") + } + } + return nil +} + +func (val *transactionSpanCount) IsSet() bool { + return val.Dropped.IsSet() || val.Started.IsSet() +} + +func (val *transactionSpanCount) Reset() { + val.Dropped.Reset() + val.Started.Reset() +} + +func (val *transactionSpanCount) validate() error { + if !val.IsSet() { + return nil + } + if !val.Started.IsSet() { + return fmt.Errorf("'x.yc.sd' required") + } + return nil +} + +func (val *transactionUserExperience) IsSet() bool { + return val.CumulativeLayoutShift.IsSet() || val.FirstInputDelay.IsSet() || val.TotalBlockingTime.IsSet() +} + +func (val *transactionUserExperience) Reset() { + val.CumulativeLayoutShift.Reset() + val.FirstInputDelay.Reset() + val.TotalBlockingTime.Reset() +} + +func (val *transactionUserExperience) validate() error { + if !val.IsSet() { + return nil + } + if val.CumulativeLayoutShift.Val < 0 { + return fmt.Errorf("validation rule 'min(0)' violated for 'x.exp.cls'") + } + if val.FirstInputDelay.Val < 0 { + return fmt.Errorf("validation rule 'min(0)' violated for 'x.exp.fid'") + } + if val.TotalBlockingTime.Val < 0 { + return fmt.Errorf("validation rule 'min(0)' violated for 'x.exp.tbt'") + } + return nil +} diff --git a/model/modeldecoder/rumv3/transaction_test.go b/model/modeldecoder/rumv3/transaction_test.go new file mode 100644 index 00000000000..0ea98924540 --- /dev/null +++ b/model/modeldecoder/rumv3/transaction_test.go @@ -0,0 +1,338 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package rumv3 + +import ( + "net" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/apm-server/decoder" + "github.com/elastic/apm-server/model" + "github.com/elastic/apm-server/model/modeldecoder" + "github.com/elastic/apm-server/model/modeldecoder/modeldecodertest" + "github.com/elastic/beats/v7/libbeat/common" +) + +func TestTransactionSetResetIsSet(t *testing.T) { + var tRoot transactionRoot + modeldecodertest.DecodeData(t, testdataReader(t, "rum_events"), "x", &tRoot) + require.True(t, tRoot.IsSet()) + // call Reset and ensure initial state, except for array capacity + tRoot.Reset() + assert.False(t, tRoot.IsSet()) +} + +func TestResetTransactionOnRelease(t *testing.T) { + inp := `{"x":{"n":"tr-a"}}` + tr := fetchTransactionRoot() + require.NoError(t, decoder.NewJSONIteratorDecoder(strings.NewReader(inp)).Decode(tr)) + require.True(t, tr.IsSet()) + releaseTransactionRoot(tr) + assert.False(t, tr.IsSet()) +} +func TestDecodeNestedTransaction(t *testing.T) { + t.Run("decode", func(t *testing.T) { + now := time.Now() + input := modeldecoder.Input{Metadata: model.Metadata{}, RequestTime: now, Config: modeldecoder.Config{Experimental: true}} + str := `{"x":{"d":100,"id":"100","tid":"1","t":"request","yc":{"sd":2},"exper":"test"}}` + dec := decoder.NewJSONIteratorDecoder(strings.NewReader(str)) + var out model.Transaction + require.NoError(t, DecodeNestedTransaction(dec, &input, &out)) + assert.Equal(t, "request", out.Type) + assert.Equal(t, "test", out.Experimental) + // fall back to request time + assert.Equal(t, now, out.Timestamp) + + // experimental should only be set if allowed by configuration + input = modeldecoder.Input{Metadata: model.Metadata{}, RequestTime: now, Config: modeldecoder.Config{Experimental: false}} + dec = decoder.NewJSONIteratorDecoder(strings.NewReader(str)) + out = model.Transaction{} + require.NoError(t, DecodeNestedTransaction(dec, &input, &out)) + assert.Nil(t, out.Experimental) + + err := DecodeNestedTransaction(decoder.NewJSONIteratorDecoder(strings.NewReader(`malformed`)), &input, &out) + require.Error(t, err) + assert.Contains(t, err.Error(), "decode") + }) + + t.Run("validate", func(t *testing.T) { + var out model.Transaction + err := DecodeNestedTransaction(decoder.NewJSONIteratorDecoder(strings.NewReader(`{}`)), &modeldecoder.Input{}, &out) + require.Error(t, err) + assert.Contains(t, err.Error(), "validation") + }) +} +func TestDecodeMapToTransactionModel(t *testing.T) { + localhostIP := net.ParseIP("127.0.0.1") + gatewayIP := net.ParseIP("192.168.0.1") + exceptions := func(key string) bool { + // values not set for rumV3: + for _, k := range []string{"Cloud", "System", "Process", "Service.Node", "Node"} { + if strings.HasPrefix(key, k) { + return true + } + } + for _, k := range []string{"Service.Agent.EphemeralID", + "Agent.EphemeralID", "Message", "RepresentativeCount"} { + if k == key { + return true + } + } + return false + } + + initializedMeta := func() *model.Metadata { + var inputMeta metadata + var meta model.Metadata + modeldecodertest.SetStructValues(&inputMeta, "meta", 1, false, time.Now()) + mapToMetadataModel(&inputMeta, &meta) + // initialize values that are not set by input + meta.UserAgent = model.UserAgent{Name: "meta", Original: "meta"} + meta.Client.IP = localhostIP + return &meta + } + + t.Run("set-metadata", func(t *testing.T) { + // do not overwrite metadata with zero transaction values + var inputTr transaction + var tr model.Transaction + mapToTransactionModel(&inputTr, initializedMeta(), time.Now(), true, &tr) + // iterate through metadata model and assert values are set + modeldecodertest.AssertStructValues(t, &tr.Metadata, exceptions, "meta", 1, false, localhostIP, time.Now()) + }) + + t.Run("overwrite-metadata", func(t *testing.T) { + // overwrite defined metadata with transaction metadata values + var inputTr transaction + var tr model.Transaction + modeldecodertest.SetStructValues(&inputTr, "overwritten", 5000, false, time.Now()) + inputTr.Context.Request.Headers.Val.Add("user-agent", "first") + inputTr.Context.Request.Headers.Val.Add("user-agent", "second") + inputTr.Context.Request.Headers.Val.Add("x-real-ip", gatewayIP.String()) + mapToTransactionModel(&inputTr, initializedMeta(), time.Now(), true, &tr) + + // user-agent should be set to context request header values + assert.Equal(t, "first, second", tr.Metadata.UserAgent.Original) + // do not overwrite client.ip if already set in metadata + assert.Equal(t, localhostIP, tr.Metadata.Client.IP, tr.Metadata.Client.IP.String()) + // metadata labels and transaction labels should not be merged + assert.Equal(t, common.MapStr{"meta": "meta"}, tr.Metadata.Labels) + assert.Equal(t, &model.Labels{"overwritten": "overwritten"}, tr.Labels) + // service values should be set + modeldecodertest.AssertStructValues(t, &tr.Metadata.Service, exceptions, "overwritten", 100, true, localhostIP, time.Now()) + // user values should be set + modeldecodertest.AssertStructValues(t, &tr.Metadata.User, exceptions, "overwritten", 100, true, localhostIP, time.Now()) + }) + + t.Run("overwrite-user", func(t *testing.T) { + // user should be populated by metadata or event specific, but not merged + var inputTr transaction + var tr model.Transaction + inputTr.Context.User.Email.Set("test@user.com") + mapToTransactionModel(&inputTr, initializedMeta(), time.Now(), false, &tr) + assert.Equal(t, "test@user.com", tr.Metadata.User.Email) + assert.Zero(t, tr.Metadata.User.ID) + assert.Zero(t, tr.Metadata.User.Name) + }) + + t.Run("other-transaction-values", func(t *testing.T) { + exceptions := func(key string) bool { + // metadata are tested separately + // Page.URL parts are derived from url (separately tested) + // exclude attributes that are not set for RUM + if strings.HasPrefix(key, "Metadata") || strings.HasPrefix(key, "Page.URL") || + key == "HTTP.Request.Body" || key == "HTTP.Request.Cookies" || key == "HTTP.Request.Socket" || + key == "HTTP.Response.HeadersSent" || key == "HTTP.Response.Finished" || + key == "Experimental" || key == "RepresentativeCount" || key == "Message" || + key == "URL" { + return true + } + return false + } + + var inputTr transaction + var tr model.Transaction + eventTime, reqTime := time.Now(), time.Now().Add(time.Second) + modeldecodertest.SetStructValues(&inputTr, "overwritten", 5000, true, eventTime) + mapToTransactionModel(&inputTr, initializedMeta(), reqTime, true, &tr) + modeldecodertest.AssertStructValues(t, &tr, exceptions, "overwritten", 5000, true, localhostIP, reqTime) + }) + + t.Run("page.URL", func(t *testing.T) { + var inputTr transaction + inputTr.Context.Page.URL.Set("https://my.site.test:9201") + var tr model.Transaction + mapToTransactionModel(&inputTr, initializedMeta(), time.Now(), false, &tr) + assert.Equal(t, "https://my.site.test:9201", *tr.Page.URL.Full) + assert.Equal(t, 9201, *tr.Page.URL.Port) + assert.Equal(t, "https", *tr.Page.URL.Scheme) + }) + +} + +func TestTransactionValidationRules(t *testing.T) { + testTransaction := func(t *testing.T, key string, tc testcase) { + var event transaction + r := testdataReader(t, "rum_events") + modeldecodertest.DecodeDataWithReplacement(t, r, "x", key, tc.data, &event) + // run validation and checks + err := event.validate() + if tc.errorKey == "" { + assert.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errorKey) + } + } + + t.Run("context", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "custom", data: `{"cu":{"k1":{"v1":123,"v2":"value"},"k2":34,"k3":[{"a.1":1,"b*\"":2}]}}`}, + {name: "custom-key-dot", errorKey: "patternKeys", data: `{"cu":{"k1.":{"v1":123,"v2":"value"}}}`}, + {name: "custom-key-asterisk", errorKey: "patternKeys", data: `{"cu":{"k1*":{"v1":123,"v2":"value"}}}`}, + {name: "custom-key-quote", errorKey: "patternKeys", data: `{"cu":{"k1\"":{"v1":123,"v2":"value"}}}`}, + {name: "tags", data: `{"g":{"k1":"v1.s*\"","k2":34,"k3":23.56,"k4":true}}`}, + {name: "tags-key-dot", errorKey: "patternKeys", data: `{"g":{"k1.":"v1"}}`}, + {name: "tags-key-asterisk", errorKey: "patternKeys", data: `{"g":{"k1*":"v1"}}`}, + {name: "tags-key-quote", errorKey: "patternKeys", data: `{"g":{"k1\"":"v1"}}`}, + {name: "tags-invalid-type", errorKey: "typesVals", data: `{"g":{"k1":{"v1":"abc"}}}`}, + {name: "tags-invalid-type", errorKey: "typesVals", data: `{"g":{"k1":{"v1":[1,2,3]}}}`}, + {name: "tags-maxVal", data: `{"g":{"k1":"` + modeldecodertest.BuildString(1024) + `"}}`}, + {name: "tags-maxVal-exceeded", errorKey: "maxVals", data: `{"g":{"k1":"` + modeldecodertest.BuildString(1025) + `"}}`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "c", tc) + }) + } + }) + + // this tests an arbitrary field to ensure the max rule works as expected + t.Run("max", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "trace-id-max", data: `"` + modeldecodertest.BuildString(1024) + `"`}, + {name: "trace-id-max-exceeded", errorKey: "max", data: `"` + modeldecodertest.BuildString(1025) + `"`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "tid", tc) + }) + } + }) + + t.Run("service", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "service-name-az", data: `{"se":{"n":"abcdefghijklmnopqrstuvwxyz"}}`}, + {name: "service-name-AZ", data: `{"se":{"n":"ABCDEFGHIJKLMNOPQRSTUVWXYZ"}}`}, + {name: "service-name-09 _-", data: `{"se":{"n":"0123456789 -_"}}`}, + {name: "service-name-invalid", errorKey: "regexpAlphaNumericExt", data: `{"se":{"n":"⌘"}}`}, + {name: "service-name-max", data: `{"e":{"n":"` + modeldecodertest.BuildStringWith(1024, '-') + `"}}`}, + {name: "service-name-max-exceeded", errorKey: "max", data: `{"se":{"n":"` + modeldecodertest.BuildStringWith(1025, '-') + `"}}`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "c", tc) + }) + } + }) + + t.Run("duration", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "duration", data: `0.0`}, + {name: "duration", errorKey: "min", data: `-0.09`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "d", tc) + }) + } + }) + + t.Run("marks", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "marks", data: `{"k1":{"v1":12.3}}`}, + {name: "marks-dot", errorKey: "patternKeys", data: `{"k.1":{"v1":12.3}}`}, + {name: "marks-event-dot", errorKey: "patternKeys", data: `{"k1":{"v.1":12.3}}`}, + {name: "marks-asterisk", errorKey: "patternKeys", data: `{"k*1":{"v1":12.3}}`}, + {name: "marks-event-asterisk", errorKey: "patternKeys", data: `{"k1":{"v*1":12.3}}`}, + {name: "marks-quote", errorKey: "patternKeys", data: `{"k\"1":{"v1":12.3}}`}, + {name: "marks-event-quote", errorKey: "patternKeys", data: `{"k1":{"v\"1":12.3}}`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "k", tc) + }) + } + }) + + t.Run("outcome", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "outcome-success", data: `"success"`}, + {name: "outcome-failure", data: `"failure"`}, + {name: "outcome-unknown", data: `"unknown"`}, + {name: "outcome-invalid", errorKey: "enum", data: `"anything"`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "o", tc) + }) + } + }) + + t.Run("user", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "id-string", data: `{"u":{"id":"user123"}}`}, + {name: "id-int", data: `{"u":{"id":44}}`}, + {name: "id-float", errorKey: "types", data: `{"u":{"id":45.6}}`}, + {name: "id-bool", errorKey: "types", data: `{"u":{"id":true}}`}, + {name: "id-string-max-len", data: `{"u":{"id":"` + modeldecodertest.BuildString(1024) + `"}}`}, + {name: "id-string-max-len-exceeded", errorKey: "max", data: `{"u":{"id":"` + modeldecodertest.BuildString(1025) + `"}}`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "c", tc) + }) + } + }) + + t.Run("required", func(t *testing.T) { + // setup: create full metadata struct with arbitrary values set + var event transaction + modeldecodertest.InitStructValues(&event) + // test vanilla struct is valid + require.NoError(t, event.validate()) + + // iterate through struct, remove every key one by one + // and test that validation behaves as expected + requiredKeys := map[string]interface{}{"d": nil, + "id": nil, + "yc": nil, + "yc.sd": nil, + "tid": nil, + "t": nil, + "c.q.mt": nil, + } + modeldecodertest.SetZeroStructValue(&event, func(key string) { + err := event.validate() + if _, ok := requiredKeys[key]; ok { + require.Error(t, err, key) + assert.Contains(t, err.Error(), key) + } else { + assert.NoError(t, err, key) + } + }) + }) +} diff --git a/model/modeldecoder/v2/decoder.go b/model/modeldecoder/v2/decoder.go index bb2b95ec42a..ed6448587ef 100644 --- a/model/modeldecoder/v2/decoder.go +++ b/model/modeldecoder/v2/decoder.go @@ -19,19 +19,34 @@ package v2 import ( "fmt" + "net/http" + "net/textproto" + "strconv" + "strings" "sync" + "time" "github.com/elastic/beats/v7/libbeat/common" "github.com/elastic/apm-server/decoder" "github.com/elastic/apm-server/model" + "github.com/elastic/apm-server/model/modeldecoder" + "github.com/elastic/apm-server/utility" ) -var metadataRootPool = sync.Pool{ - New: func() interface{} { - return &metadataRoot{} - }, -} +var ( + metadataRootPool = sync.Pool{ + New: func() interface{} { + return &metadataRoot{} + }, + } + + transactionRootPool = sync.Pool{ + New: func() interface{} { + return &transactionRoot{} + }, + } +) func fetchMetadataRoot() *metadataRoot { return metadataRootPool.Get().(*metadataRoot) @@ -42,26 +57,53 @@ func releaseMetadataRoot(m *metadataRoot) { metadataRootPool.Put(m) } -// DecodeMetadata uses the given decoder to create the input models, -// then runs the defined validations on the input models +func fetchTransactionRoot() *transactionRoot { + return transactionRootPool.Get().(*transactionRoot) +} + +func releaseTransactionRoot(m *transactionRoot) { + m.Reset() + transactionRootPool.Put(m) +} + +// DecodeNestedTransaction uses the given decoder to create the input model, +// then runs the defined validations on the input model +// and finally maps the values fom the input model to the given *model.Transaction instance +// +// DecodeNestedTransaction should be used when the decoder contains the `transaction` key +func DecodeNestedTransaction(d decoder.Decoder, input *modeldecoder.Input, out *model.Transaction) error { + root := fetchTransactionRoot() + defer releaseTransactionRoot(root) + if err := d.Decode(&root); err != nil { + return fmt.Errorf("decode error %w", err) + } + if err := root.validate(); err != nil { + return fmt.Errorf("validation error %w", err) + } + mapToTransactionModel(&root.Transaction, &input.Metadata, input.RequestTime, input.Config.Experimental, out) + return nil +} + +// DecodeMetadata uses the given decoder to create the input model, +// then runs the defined validations on the input model // and finally maps the values fom the input model to the given *model.Metadata instance // // DecodeMetadata should be used when the underlying byte stream does not contain the -// `metadata` key, but only the metadata. +// `metadata` key, but only the metadata data. func DecodeMetadata(d decoder.Decoder, out *model.Metadata) error { - return decode(decodeIntoMetadata, d, out) + return decodeMetadata(decodeIntoMetadata, d, out) } -// DecodeNestedMetadata uses the given decoder to create the input models, -// then runs the defined validations on the input models +// DecodeNestedMetadata uses the given decoder to create the input model, +// then runs the defined validations on the input model // and finally maps the values fom the input model to the given *model.Metadata instance // // DecodeNestedMetadata should be used when the underlying byte stream does start with the `metadata` key func DecodeNestedMetadata(d decoder.Decoder, out *model.Metadata) error { - return decode(decodeIntoMetadataRoot, d, out) + return decodeMetadata(decodeIntoMetadataRoot, d, out) } -func decode(decFn func(d decoder.Decoder, m *metadataRoot) error, d decoder.Decoder, out *model.Metadata) error { +func decodeMetadata(decFn func(d decoder.Decoder, m *metadataRoot) error, d decoder.Decoder, out *model.Metadata) error { m := fetchMetadataRoot() defer releaseMetadataRoot(m) if err := decFn(d, m); err != nil { @@ -84,6 +126,9 @@ func decodeIntoMetadataRoot(d decoder.Decoder, m *metadataRoot) error { func mapToMetadataModel(m *metadata, out *model.Metadata) { // Cloud + if m == nil { + return + } if m.Cloud.Account.ID.IsSet() { out.Cloud.AccountID = m.Cloud.Account.ID.Val } @@ -221,3 +266,323 @@ func mapToMetadataModel(m *metadata, out *model.Metadata) { out.User.Name = m.User.Name.Val } } + +func mapToTransactionModel(t *transaction, metadata *model.Metadata, reqTime time.Time, experimental bool, out *model.Transaction) { + if t == nil { + return + } + + // prefill with metadata information, then overwrite with event specific metadata + out.Metadata = *metadata + + // only set metadata Labels + out.Metadata.Labels = metadata.Labels.Clone() + + // overwrite Service values if set + if t.Context.Service.Agent.EphemeralID.IsSet() { + out.Metadata.Service.Agent.EphemeralID = t.Context.Service.Agent.EphemeralID.Val + } + if t.Context.Service.Agent.Name.IsSet() { + out.Metadata.Service.Agent.Name = t.Context.Service.Agent.Name.Val + } + if t.Context.Service.Agent.Version.IsSet() { + out.Metadata.Service.Agent.Version = t.Context.Service.Agent.Version.Val + } + if t.Context.Service.Environment.IsSet() { + out.Metadata.Service.Environment = t.Context.Service.Environment.Val + } + if t.Context.Service.Framework.Name.IsSet() { + out.Metadata.Service.Framework.Name = t.Context.Service.Framework.Name.Val + } + if t.Context.Service.Framework.Version.IsSet() { + out.Metadata.Service.Framework.Version = t.Context.Service.Framework.Version.Val + } + if t.Context.Service.Language.Name.IsSet() { + out.Metadata.Service.Language.Name = t.Context.Service.Language.Name.Val + } + if t.Context.Service.Language.Version.IsSet() { + out.Metadata.Service.Language.Version = t.Context.Service.Language.Version.Val + } + if t.Context.Service.Name.IsSet() { + out.Metadata.Service.Name = t.Context.Service.Name.Val + } + if t.Context.Service.Node.Name.IsSet() { + out.Metadata.Service.Node.Name = t.Context.Service.Node.Name.Val + } + if t.Context.Service.Runtime.Name.IsSet() { + out.Metadata.Service.Runtime.Name = t.Context.Service.Runtime.Name.Val + } + if t.Context.Service.Runtime.Version.IsSet() { + out.Metadata.Service.Runtime.Version = t.Context.Service.Runtime.Version.Val + } + if t.Context.Service.Version.IsSet() { + out.Metadata.Service.Version = t.Context.Service.Version.Val + } + + // overwrite User specific values if set + // either populate all User fields or none to avoid mixing + // different user data + if t.Context.User.ID.IsSet() || t.Context.User.Email.IsSet() || t.Context.User.Name.IsSet() { + out.Metadata.User = model.User{} + if t.Context.User.ID.IsSet() { + out.Metadata.User.ID = fmt.Sprint(t.Context.User.ID.Val) + } + if t.Context.User.Email.IsSet() { + out.Metadata.User.Email = t.Context.User.Email.Val + } + if t.Context.User.Name.IsSet() { + out.Metadata.User.Name = t.Context.User.Name.Val + } + } + + if t.Context.Request.Headers.IsSet() { + if h := t.Context.Request.Headers.Val.Values(textproto.CanonicalMIMEHeaderKey("User-Agent")); len(h) > 0 { + out.Metadata.UserAgent.Original = strings.Join(h, ", ") + } + } + + // only set client information if not already set in metadata + // this is aligned with current logic + if out.Metadata.Client.IP == nil { + // http.Request.Headers and http.Request.Socket information is + // only set for backend events; try to first extract an IP address + // from the headers, if not possible use IP address from socket remote_address + if ip := utility.ExtractIPFromHeader(t.Context.Request.Headers.Val); ip != nil { + out.Metadata.Client.IP = ip + } else if ip := utility.ParseIP(t.Context.Request.Socket.RemoteAddress.Val); ip != nil { + out.Metadata.Client.IP = ip + } + } + + // fill with event specific information + + // metadata labels and context labels are not merged at decoder level + // but in the output model + if len(t.Context.Tags) > 0 { + labels := model.Labels(t.Context.Tags.Clone()) + out.Labels = &labels + } + if t.Duration.IsSet() { + out.Duration = t.Duration.Val + } + if t.ID.IsSet() { + out.ID = t.ID.Val + } + if t.Marks.IsSet() { + out.Marks = make(model.TransactionMarks, len(t.Marks.Events)) + for event, val := range t.Marks.Events { + if len(val.Measurements) > 0 { + out.Marks[event] = model.TransactionMark(val.Measurements) + } + } + } + if t.Name.IsSet() { + out.Name = t.Name.Val + } + if t.Outcome.IsSet() { + out.Outcome = t.Outcome.Val + } else { + if t.Context.Response.StatusCode.IsSet() { + statusCode := t.Context.Response.StatusCode.Val + if statusCode >= http.StatusInternalServerError { + out.Outcome = "failure" + } else { + out.Outcome = "success" + } + } else { + out.Outcome = "unknown" + } + } + if t.ParentID.IsSet() { + out.ParentID = t.ParentID.Val + } + if t.Result.IsSet() { + out.Result = t.Result.Val + } + + sampled := true + if t.Sampled.IsSet() { + sampled = t.Sampled.Val + } + out.Sampled = &sampled + + // TODO(simitt): set accordingly, once this is fixed: + // https://github.com/elastic/apm-server/issues/4188 + // if t.SampleRate.IsSet() {} + + if t.SpanCount.Dropped.IsSet() { + dropped := t.SpanCount.Dropped.Val + out.SpanCount.Dropped = &dropped + } + if t.SpanCount.Started.IsSet() { + started := t.SpanCount.Started.Val + out.SpanCount.Started = &started + } + if t.Timestamp.Val.IsZero() { + out.Timestamp = reqTime + } else { + out.Timestamp = t.Timestamp.Val + } + if t.TraceID.IsSet() { + out.TraceID = t.TraceID.Val + } + if t.Type.IsSet() { + out.Type = t.Type.Val + } + if t.UserExperience.IsSet() { + out.UserExperience = &model.UserExperience{} + if t.UserExperience.CumulativeLayoutShift.IsSet() { + out.UserExperience.CumulativeLayoutShift = t.UserExperience.CumulativeLayoutShift.Val + } + if t.UserExperience.FirstInputDelay.IsSet() { + out.UserExperience.FirstInputDelay = t.UserExperience.FirstInputDelay.Val + + } + if t.UserExperience.TotalBlockingTime.IsSet() { + out.UserExperience.TotalBlockingTime = t.UserExperience.TotalBlockingTime.Val + } + } + if t.Context.IsSet() { + if t.Context.Page.IsSet() { + out.Page = &model.Page{} + if t.Context.Page.URL.IsSet() { + out.Page.URL = model.ParseURL(t.Context.Page.URL.Val, "") + } + if t.Context.Page.Referer.IsSet() { + referer := t.Context.Page.Referer.Val + out.Page.Referer = &referer + } + } + + if t.Context.Request.IsSet() { + var request model.Req + if t.Context.Request.Method.IsSet() { + request.Method = t.Context.Request.Method.Val + } + if t.Context.Request.Env.IsSet() { + request.Env = t.Context.Request.Env.Val + } + if t.Context.Request.Socket.IsSet() { + request.Socket = &model.Socket{} + if t.Context.Request.Socket.Encrypted.IsSet() { + val := t.Context.Request.Socket.Encrypted.Val + request.Socket.Encrypted = &val + } + if t.Context.Request.Socket.RemoteAddress.IsSet() { + val := t.Context.Request.Socket.RemoteAddress.Val + request.Socket.RemoteAddress = &val + } + } + if t.Context.Request.Body.IsSet() { + request.Body = t.Context.Request.Body.Val + } + if t.Context.Request.Cookies.IsSet() { + request.Cookies = t.Context.Request.Cookies.Val + } + if t.Context.Request.Headers.IsSet() { + request.Headers = t.Context.Request.Headers.Val.Clone() + } + out.HTTP = &model.Http{Request: &request} + if t.Context.Request.HTTPVersion.IsSet() { + val := t.Context.Request.HTTPVersion.Val + out.HTTP.Version = &val + } + } + if t.Context.Response.IsSet() { + if out.HTTP == nil { + out.HTTP = &model.Http{} + } + var response model.Resp + if t.Context.Response.Finished.IsSet() { + val := t.Context.Response.Finished.Val + response.Finished = &val + } + if t.Context.Response.Headers.IsSet() { + response.Headers = t.Context.Response.Headers.Val.Clone() + } + if t.Context.Response.HeadersSent.IsSet() { + val := t.Context.Response.HeadersSent.Val + response.HeadersSent = &val + } + if t.Context.Response.StatusCode.IsSet() { + val := t.Context.Response.StatusCode.Val + response.StatusCode = &val + } + if t.Context.Response.TransferSize.IsSet() { + val := t.Context.Response.TransferSize.Val + response.TransferSize = &val + } + if t.Context.Response.EncodedBodySize.IsSet() { + val := t.Context.Response.EncodedBodySize.Val + response.EncodedBodySize = &val + } + if t.Context.Response.DecodedBodySize.IsSet() { + val := t.Context.Response.DecodedBodySize.Val + response.DecodedBodySize = &val + } + out.HTTP.Response = &response + } + if t.Context.Request.URL.IsSet() { + out.URL = &model.URL{} + if t.Context.Request.URL.Raw.IsSet() { + val := t.Context.Request.URL.Raw.Val + out.URL.Original = &val + } + if t.Context.Request.URL.Full.IsSet() { + val := t.Context.Request.URL.Full.Val + out.URL.Full = &val + } + if t.Context.Request.URL.Hostname.IsSet() { + val := t.Context.Request.URL.Hostname.Val + out.URL.Domain = &val + } + if t.Context.Request.URL.Path.IsSet() { + val := t.Context.Request.URL.Path.Val + out.URL.Path = &val + } + if t.Context.Request.URL.Search.IsSet() { + val := t.Context.Request.URL.Search.Val + out.URL.Query = &val + } + if t.Context.Request.URL.Hash.IsSet() { + val := t.Context.Request.URL.Hash.Val + out.URL.Fragment = &val + } + if t.Context.Request.URL.Protocol.IsSet() { + trimmed := strings.TrimSuffix(t.Context.Request.URL.Protocol.Val, ":") + out.URL.Scheme = &trimmed + } + if t.Context.Request.URL.Port.IsSet() { + port, err := strconv.Atoi(fmt.Sprint(t.Context.Request.URL.Port.Val)) + if err == nil { + out.URL.Port = &port + } + } + } + if len(t.Context.Custom) > 0 { + custom := model.Custom(t.Context.Custom.Clone()) + out.Custom = &custom + } + if t.Context.Message.IsSet() { + out.Message = &model.Message{} + if t.Context.Message.Age.IsSet() { + val := t.Context.Message.Age.Milliseconds.Val + out.Message.AgeMillis = &val + } + if t.Context.Message.Body.IsSet() { + val := t.Context.Message.Body.Val + out.Message.Body = &val + } + if t.Context.Message.Headers.IsSet() { + out.Message.Headers = t.Context.Message.Headers.Val.Clone() + } + if t.Context.Message.Queue.IsSet() && t.Context.Message.Queue.Name.IsSet() { + val := t.Context.Message.Queue.Name.Val + out.Message.QueueName = &val + } + } + } + if experimental { + out.Experimental = t.Experimental.Val + } +} diff --git a/model/modeldecoder/v2/decoder_test.go b/model/modeldecoder/v2/decoder_test.go deleted file mode 100644 index 6cefecfc8d7..00000000000 --- a/model/modeldecoder/v2/decoder_test.go +++ /dev/null @@ -1,144 +0,0 @@ -// Licensed to Elasticsearch B.V. under one or more contributor -// license agreements. See the NOTICE file distributed with -// this work for additional information regarding copyright -// ownership. Elasticsearch B.V. licenses this file to you under -// the Apache License, Version 2.0 (the "License"); you may -// not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, -// software distributed under the License is distributed on an -// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -// KIND, either express or implied. See the License for the -// specific language governing permissions and limitations -// under the License. - -package v2 - -import ( - "fmt" - "net" - "reflect" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/elastic/beats/v7/libbeat/common" - - "github.com/elastic/apm-server/decoder" - "github.com/elastic/apm-server/model" - "github.com/elastic/apm-server/model/modeldecoder/modeldecodertest" -) - -func TestResetModelOnRelease(t *testing.T) { - inp := `{"metadata":{"service":{"name":"service-a"}}}` - m := fetchMetadataRoot() - require.NoError(t, decoder.NewJSONIteratorDecoder(strings.NewReader(inp)).Decode(m)) - require.True(t, m.IsSet()) - releaseMetadataRoot(m) - assert.False(t, m.IsSet()) -} - -func TestDecodeMetadata(t *testing.T) { - - for _, tc := range []struct { - name string - input string - decodeFn func(decoder.Decoder, *model.Metadata) error - }{ - {name: "decodeMetadata", decodeFn: DecodeMetadata, - input: `{"service":{"name":"user-service","agent":{"name":"go","version":"1.0.0"}}}`}, - {name: "decodeNestedMetadata", decodeFn: DecodeNestedMetadata, - input: `{"metadata":{"service":{"name":"user-service","agent":{"name":"go","version":"1.0.0"}}}}`}, - } { - t.Run("decode", func(t *testing.T) { - var out model.Metadata - dec := decoder.NewJSONIteratorDecoder(strings.NewReader(tc.input)) - require.NoError(t, tc.decodeFn(dec, &out)) - assert.Equal(t, model.Metadata{Service: model.Service{ - Name: "user-service", - Agent: model.Agent{Name: "go", Version: "1.0.0"}}}, out) - - err := tc.decodeFn(decoder.NewJSONIteratorDecoder(strings.NewReader(`malformed`)), &out) - require.Error(t, err) - assert.Contains(t, err.Error(), "decode") - }) - - t.Run("validate", func(t *testing.T) { - inp := `{}` - var out model.Metadata - err := tc.decodeFn(decoder.NewJSONIteratorDecoder(strings.NewReader(inp)), &out) - require.Error(t, err) - assert.Contains(t, err.Error(), "validation") - }) - } - -} - -func TestMappingToModel(t *testing.T) { - // setup: - // create initialized modeldecoder and empty model metadata - // map modeldecoder to model metadata and manually set - // enhanced data that are never set by the modeldecoder - var m metadata - modeldecodertest.SetStructValues(&m, "init", 5000) - var modelM model.Metadata - modelM.System.IP, modelM.Client.IP = net.ParseIP("127.0.0.1"), net.ParseIP("127.0.0.1") - modelM.UserAgent.Original, modelM.UserAgent.Name = "Firefox/15.0.1", "Firefox/15.0.1" - mapToMetadataModel(&m, &modelM) - - // iterate through model and assert values are set - assertStructValues(t, &modelM, "init", 5000) - - // overwrite model metadata with specified Values - // then iterate through model and assert values are overwritten - modeldecodertest.SetStructValues(&m, "overwritten", 12) - mapToMetadataModel(&m, &modelM) - assertStructValues(t, &modelM, "overwritten", 12) - - // map an empty modeldecoder metadata to the model - // and assert values are unchanged - modeldecodertest.SetZeroStructValues(&m) - mapToMetadataModel(&m, &modelM) - assertStructValues(t, &modelM, "overwritten", 12) -} - -func assertStructValues(t *testing.T, i interface{}, vStr string, vInt int) { - modeldecodertest.IterateStruct(i, func(f reflect.Value, key string) { - fVal := f.Interface() - var newVal interface{} - switch fVal.(type) { - case map[string]interface{}: - newVal = map[string]interface{}{vStr: vStr} - case common.MapStr: - newVal = common.MapStr{vStr: vStr} - case []string: - newVal = []string{vStr} - case []int: - newVal = []int{vInt, vInt} - case string: - newVal = vStr - case int: - newVal = vInt - case *int: - iptr := f.Interface().(*int) - fVal = *iptr - newVal = vInt - case net.IP: - default: - if f.Type().Kind() == reflect.Struct { - return - } - panic(fmt.Sprintf("unhandled type %T for key %s", f.Type().Kind(), key)) - } - if strings.HasPrefix(key, "UserAgent") || key == "Client.IP" || key == "System.IP" { - // these values are not set by modeldecoder - return - } - assert.Equal(t, newVal, fVal, key) - }) -} diff --git a/model/modeldecoder/v2/model_test.go b/model/modeldecoder/v2/metadata_test.go similarity index 57% rename from model/modeldecoder/v2/model_test.go rename to model/modeldecoder/v2/metadata_test.go index 936bd9746fe..3709a636044 100644 --- a/model/modeldecoder/v2/model_test.go +++ b/model/modeldecoder/v2/metadata_test.go @@ -18,40 +18,39 @@ package v2 import ( - "bytes" - "encoding/json" + "fmt" "io" + "net" "os" + "path/filepath" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/elastic/apm-server/decoder" + "github.com/elastic/apm-server/model" "github.com/elastic/apm-server/model/modeldecoder/modeldecodertest" ) -func testdata(t *testing.T) io.Reader { - r, err := os.Open("../../../testdata/intake-v2/metadata.ndjson") - require.NoError(t, err) - return r +type testcase struct { + name string + errorKey string + data string } -func TestIsSet(t *testing.T) { - inp := `{"cloud":{"availability_zone":"eu-west-3","instance":{"id":"1234"}}}` - var m metadata - require.NoError(t, decoder.NewJSONIteratorDecoder(strings.NewReader(inp)).Decode(&m)) - assert.True(t, m.IsSet()) - assert.True(t, m.Cloud.IsSet()) - assert.True(t, m.Cloud.AvailabilityZone.IsSet()) - assert.True(t, m.Cloud.Instance.ID.IsSet()) - assert.False(t, m.Cloud.Instance.Name.IsSet()) +func reader(t *testing.T, typ string) io.Reader { + p := filepath.Join("..", "..", "..", "testdata", "intake-v2", fmt.Sprintf("%s.ndjson", typ)) + r, err := os.Open(p) + require.NoError(t, err) + return r } -func TestSetReset(t *testing.T) { +func TestMetadataSetResetIsSet(t *testing.T) { var m metadataRoot - require.NoError(t, decoder.NewJSONIteratorDecoder(testdata(t)).Decode(&m)) + modeldecodertest.DecodeData(t, reader(t, "metadata"), "metadata", &m) require.True(t, m.IsSet()) require.True(t, m.Metadata.Cloud.IsSet()) require.NotEmpty(t, m.Metadata.Labels) @@ -65,7 +64,7 @@ func TestSetReset(t *testing.T) { assert.Equal(t, metadataCloud{}, m.Metadata.Cloud) assert.Equal(t, metadataService{}, m.Metadata.Service) assert.Equal(t, metadataSystem{}, m.Metadata.System) - assert.Equal(t, metadataUser{}, m.Metadata.User) + assert.Equal(t, user{}, m.Metadata.User) assert.Empty(t, m.Metadata.Labels) assert.Empty(t, m.Metadata.Process.Pid) assert.Empty(t, m.Metadata.Process.Ppid) @@ -75,38 +74,89 @@ func TestSetReset(t *testing.T) { assert.Greater(t, cap(m.Metadata.Process.Argv), 0) } -func TestValidationRules(t *testing.T) { - type testcase struct { +func TestResetMetadataOnRelease(t *testing.T) { + inp := `{"metadata":{"service":{"name":"service-a"}}}` + m := fetchMetadataRoot() + require.NoError(t, decoder.NewJSONIteratorDecoder(strings.NewReader(inp)).Decode(m)) + require.True(t, m.IsSet()) + releaseMetadataRoot(m) + assert.False(t, m.IsSet()) +} + +func TestDecodeMetadata(t *testing.T) { + for _, tc := range []struct { name string - errorKey string - data string + input string + decodeFn func(decoder.Decoder, *model.Metadata) error + }{ + {name: "decodeMetadata", decodeFn: DecodeMetadata, + input: `{"service":{"name":"user-service","agent":{"name":"go","version":"1.0.0"}}}`}, + {name: "decodeNestedMetadata", decodeFn: DecodeNestedMetadata, + input: `{"metadata":{"service":{"name":"user-service","agent":{"name":"go","version":"1.0.0"}}}}`}, + } { + t.Run("decode", func(t *testing.T) { + var out model.Metadata + dec := decoder.NewJSONIteratorDecoder(strings.NewReader(tc.input)) + require.NoError(t, tc.decodeFn(dec, &out)) + assert.Equal(t, model.Metadata{Service: model.Service{ + Name: "user-service", + Agent: model.Agent{Name: "go", Version: "1.0.0"}}}, out) + + err := tc.decodeFn(decoder.NewJSONIteratorDecoder(strings.NewReader(`malformed`)), &out) + require.Error(t, err) + assert.Contains(t, err.Error(), "decode") + }) + + t.Run("validate", func(t *testing.T) { + inp := `{}` + var out model.Metadata + err := tc.decodeFn(decoder.NewJSONIteratorDecoder(strings.NewReader(inp)), &out) + require.Error(t, err) + assert.Contains(t, err.Error(), "validation") + }) } +} - strBuilder := func(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = '⌘' - } - return string(b) +func TestDecodeMapToMetadataModel(t *testing.T) { + // setup: + // create initialized modeldecoder and empty model metadata + // map modeldecoder to model metadata and manually set + // enhanced data that are never set by the modeldecoder + var m metadata + modeldecodertest.SetStructValues(&m, "init", 5000, false, time.Now()) + var modelM model.Metadata + ip := net.ParseIP("127.0.0.1") + modelM.System.IP, modelM.Client.IP = ip, ip + mapToMetadataModel(&m, &modelM) + + exceptions := func(key string) bool { + return strings.HasPrefix(key, "UserAgent") } + // iterate through model and assert values are set + modeldecodertest.AssertStructValues(t, &modelM, exceptions, "init", 5000, false, ip, time.Now()) + + // overwrite model metadata with specified Values + // then iterate through model and assert values are overwritten + modeldecodertest.SetStructValues(&m, "overwritten", 12, true, time.Now()) + mapToMetadataModel(&m, &modelM) + modeldecodertest.AssertStructValues(t, &modelM, exceptions, "overwritten", 12, true, ip, time.Now()) + + // map an empty modeldecoder metadata to the model + // and assert values are unchanged + modeldecodertest.SetZeroStructValues(&m) + mapToMetadataModel(&m, &modelM) + modeldecodertest.AssertStructValues(t, &modelM, exceptions, "overwritten", 12, true, ip, time.Now()) +} + +func TestMetadataValidationRules(t *testing.T) { testMetadata := func(t *testing.T, key string, tc testcase) { - // load data - // set testcase data for given key - var data map[string]interface{} - require.NoError(t, decoder.NewJSONIteratorDecoder(testdata(t)).Decode(&data)) - meta := data["metadata"].(map[string]interface{}) - var keyData map[string]interface{} - require.NoError(t, json.Unmarshal([]byte(tc.data), &keyData)) - meta[key] = keyData - - // unmarshal data into metdata struct var m metadata - b, err := json.Marshal(meta) - require.NoError(t, err) - require.NoError(t, decoder.NewJSONIteratorDecoder(bytes.NewReader(b)).Decode(&m)) + r := reader(t, "metadata") + modeldecodertest.DecodeDataWithReplacement(t, r, "metadata", key, tc.data, &m) + // run validation and checks - err = m.validate() + err := m.validate() if tc.errorKey == "" { assert.NoError(t, err) } else { @@ -121,8 +171,8 @@ func TestValidationRules(t *testing.T) { {name: "id-int", data: `{"id":44}`}, {name: "id-float", errorKey: "types", data: `{"id":45.6}`}, {name: "id-bool", errorKey: "types", data: `{"id":true}`}, - {name: "id-string-max-len", data: `{"id":"` + strBuilder(1024) + `"}`}, - {name: "id-string-max-len", errorKey: "max", data: `{"id":"` + strBuilder(1025) + `"}`}, + {name: "id-string-max-len", data: `{"id":"` + modeldecodertest.BuildString(1024) + `"}`}, + {name: "id-string-max-len-exceeded", errorKey: "max", data: `{"id":"` + modeldecodertest.BuildString(1025) + `"}`}, } { t.Run(tc.name, func(t *testing.T) { testMetadata(t, "user", tc) @@ -153,8 +203,8 @@ func TestValidationRules(t *testing.T) { {name: "key-dot", errorKey: "patternKeys", data: `{"k.1":"v1"}`}, {name: "key-asterisk", errorKey: "patternKeys", data: `{"k*1":"v1"}`}, {name: "key-quotemark", errorKey: "patternKeys", data: `{"k\"1":"v1"}`}, - {name: "max-len", data: `{"k1":"` + strBuilder(1024) + `"}`}, - {name: "max-len-exceeded", errorKey: "maxVals", data: `{"k1":"` + strBuilder(1025) + `"}`}, + {name: "max-len", data: `{"k1":"` + modeldecodertest.BuildString(1024) + `"}`}, + {name: "max-len-exceeded", errorKey: "maxVals", data: `{"k1":"` + modeldecodertest.BuildString(1025) + `"}`}, } { t.Run(tc.name, func(t *testing.T) { testMetadata(t, "labels", tc) @@ -165,9 +215,9 @@ func TestValidationRules(t *testing.T) { t.Run("max-len", func(t *testing.T) { // check that `max` on strings is respected on an arbitrary field for _, tc := range []testcase{ - {name: "title-max-len", data: `{"pid":1,"title":"` + strBuilder(1024) + `"}`}, + {name: "title-max-len", data: `{"pid":1,"title":"` + modeldecodertest.BuildString(1024) + `"}`}, {name: "title-max-len-exceeded", errorKey: "max", - data: `{"pid":1,"title":"` + strBuilder(1025) + `"}`}, + data: `{"pid":1,"title":"` + modeldecodertest.BuildString(1025) + `"}`}, } { t.Run(tc.name, func(t *testing.T) { testMetadata(t, "process", tc) diff --git a/model/modeldecoder/v2/model.go b/model/modeldecoder/v2/model.go index a8003993bac..a061a1e8b08 100644 --- a/model/modeldecoder/v2/model.go +++ b/model/modeldecoder/v2/model.go @@ -18,6 +18,7 @@ package v2 import ( + "encoding/json" "regexp" "github.com/elastic/beats/v7/libbeat/common" @@ -26,21 +27,135 @@ import ( ) var ( - alphaNumericExtRegex = regexp.MustCompile("^[a-zA-Z0-9 _-]+$") - labelsRegex = regexp.MustCompile("^[^.*\"]*$") //do not allow '.' '*' '"' + regexpAlphaNumericExt = regexp.MustCompile("^[a-zA-Z0-9 _-]+$") + regexpNoDotAsteriskQuote = regexp.MustCompile("^[^.*\"]*$") //do not allow '.' '*' '"' + + enumOutcome = []string{"success", "failure", "unknown"} ) +// entry points + type metadataRoot struct { Metadata metadata `json:"metadata" validate:"required"` } +type transactionRoot struct { + Transaction transaction `json:"transaction" validate:"required"` +} + +// other structs + +type context struct { + Custom common.MapStr `json:"custom" validate:"patternKeys=regexpNoDotAsteriskQuote"` + Message contextMessage `json:"message"` + Page contextPage `json:"page"` + Response contextResponse `json:"response"` + Request contextRequest `json:"request"` + Service contextService `json:"service"` + Tags common.MapStr `json:"tags" validate:"patternKeys=regexpNoDotAsteriskQuote,typesVals=string;bool;number,maxVals=1024"` + User user `json:"user"` +} + +type contextMessage struct { + Body nullable.String `json:"body"` + Headers nullable.HTTPHeader `json:"headers"` + Age contextMessageAge `json:"age"` + Queue contextMessageQueue `json:"queue"` +} + +type contextMessageAge struct { + Milliseconds nullable.Int `json:"ms"` +} + +type contextMessageQueue struct { + Name nullable.String `json:"name" validate:"max=1024"` +} + +type contextPage struct { + URL nullable.String `json:"url"` + Referer nullable.String `json:"referer"` +} + +type contextRequest struct { + Cookies nullable.Interface `json:"cookies"` + Body nullable.Interface `json:"body" validate:"types=string;map[string]interface"` + Env nullable.Interface `json:"env"` + Headers nullable.HTTPHeader `json:"headers"` + HTTPVersion nullable.String `json:"http_version" validate:"max=1024"` + Method nullable.String `json:"method" validate:"required,max=1024"` + Socket contextRequestSocket `json:"socket"` + URL contextRequestURL `json:"url"` //TODO(simitt): check validate:"required"` +} + +type contextRequestURL struct { + Full nullable.String `json:"full" validate:"max=1024"` + Hash nullable.String `json:"hash" validate:"max=1024"` + Hostname nullable.String `json:"hostname" validate:"max=1024"` + Path nullable.String `json:"pathname" validate:"max=1024"` + Port nullable.Interface `json:"port" validate:"max=1024,types=string;int"` + Protocol nullable.String `json:"protocol" validate:"max=1024"` + Raw nullable.String `json:"raw" validate:"max=1024"` + Search nullable.String `json:"search" validate:"max=1024"` +} + +type contextRequestSocket struct { + RemoteAddress nullable.String `json:"remote_address"` + Encrypted nullable.Bool `json:"encrypted"` +} + +type contextResponse struct { + DecodedBodySize nullable.Float64 `json:"decoded_body_size"` + EncodedBodySize nullable.Float64 `json:"encoded_body_size"` + Finished nullable.Bool `json:"finished"` + Headers nullable.HTTPHeader `json:"headers"` + HeadersSent nullable.Bool `json:"headers_sent"` + StatusCode nullable.Int `json:"status_code"` + TransferSize nullable.Float64 `json:"transfer_size"` +} + +type contextService struct { + Agent contextServiceAgent `json:"agent"` + Environment nullable.String `json:"environment" validate:"max=1024"` + Framework contextServiceFramework `json:"framework"` + Language contextServiceLanguage `json:"language"` + Name nullable.String `json:"name" validate:"max=1024,pattern=regexpAlphaNumericExt"` + Node contextServiceNode `json:"node"` + Runtime contextServiceRuntime `json:"runtime"` + Version nullable.String `json:"version" validate:"max=1024"` +} + +type contextServiceAgent struct { + EphemeralID nullable.String `json:"ephemeral_id" validate:"max=1024"` + Name nullable.String `json:"name" validate:"max=1024"` + Version nullable.String `json:"version" validate:"max=1024"` +} + +type contextServiceFramework struct { + Name nullable.String `json:"name" validate:"max=1024"` + Version nullable.String `json:"version" validate:"max=1024"` +} + +type contextServiceLanguage struct { + Name nullable.String `json:"name" validate:"max=1024"` + Version nullable.String `json:"version" validate:"max=1024"` +} + +type contextServiceNode struct { + Name nullable.String `json:"configured_name" validate:"max=1024"` +} + +type contextServiceRuntime struct { + Name nullable.String `json:"name" validate:"max=1024"` + Version nullable.String `json:"version" validate:"max=1024"` +} + type metadata struct { Cloud metadataCloud `json:"cloud"` - Labels common.MapStr `json:"labels" validate:"patternKeys=labelsRegex,typesVals=string;bool;number,maxVals=1024"` + Labels common.MapStr `json:"labels" validate:"patternKeys=regexpNoDotAsteriskQuote,typesVals=string;bool;number,maxVals=1024"` Process metadataProcess `json:"process"` Service metadataService `json:"service" validate:"required"` System metadataSystem `json:"system"` - User metadataUser `json:"user"` + User user `json:"user"` } type metadataCloud struct { @@ -84,7 +199,7 @@ type metadataService struct { Environment nullable.String `json:"environment" validate:"max=1024"` Framework metadataServiceFramework `json:"framework"` Language metadataServiceLanguage `json:"language"` - Name nullable.String `json:"name" validate:"required,max=1024,pattern=alphaNumericExtRegex"` + Name nullable.String `json:"name" validate:"required,min=1,max=1024,pattern=regexpAlphaNumericExt"` Node metadataServiceNode `json:"node"` Runtime metadataServiceRuntime `json:"runtime"` Version nullable.String `json:"version" validate:"max=1024"` @@ -92,7 +207,7 @@ type metadataService struct { type metadataServiceAgent struct { EphemeralID nullable.String `json:"ephemeral_id" validate:"max=1024"` - Name nullable.String `json:"name" validate:"required,max=1024"` + Name nullable.String `json:"name" validate:"required,min=1,max=1024"` Version nullable.String `json:"version" validate:"required,max=1024"` } @@ -147,8 +262,65 @@ type metadataSystemKubernetesPod struct { UID nullable.String `json:"uid" validate:"max=1024"` } -type metadataUser struct { - ID nullable.Interface `json:"id,omitempty" validate:"max=1024,types=string;int"` +type transaction struct { + Context context `json:"context"` + Duration nullable.Float64 `json:"duration" validate:"required,min=0"` + ID nullable.String `json:"id" validate:"required,max=1024"` + Marks transactionMarks `json:"marks"` + Name nullable.String `json:"name" validate:"max=1024"` + Outcome nullable.String `json:"outcome" validate:"enum=enumOutcome"` + ParentID nullable.String `json:"parent_id" validate:"max=1024"` + Result nullable.String `json:"result" validate:"max=1024"` + Sampled nullable.Bool `json:"sampled"` + SampleRate nullable.Float64 `json:"sample_rate"` + SpanCount transactionSpanCount `json:"span_count" validate:"required"` + Timestamp nullable.TimeMicrosUnix `json:"timestamp"` + TraceID nullable.String `json:"trace_id" validate:"required,max=1024"` + Type nullable.String `json:"type" validate:"required,max=1024"` + UserExperience transactionUserExperience `json:"experience"` + Experimental nullable.Interface `json:"experimental"` +} + +type transactionMarks struct { + Events map[string]transactionMarkEvents `json:"-" validate:"patternKeys=regexpNoDotAsteriskQuote"` +} + +//TODO(simitt): generate +func (m *transactionMarks) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &m.Events) +} + +type transactionMarkEvents struct { + Measurements map[string]float64 `json:"-" validate:"patternKeys=regexpNoDotAsteriskQuote"` +} + +//TODO(simitt): generate +func (m *transactionMarkEvents) UnmarshalJSON(data []byte) error { + return json.Unmarshal(data, &m.Measurements) +} + +type transactionSpanCount struct { + Dropped nullable.Int `json:"dropped"` + Started nullable.Int `json:"started" validate:"required"` +} + +// transactionUserExperience holds real user (browser) experience metrics. +type transactionUserExperience struct { + // CumulativeLayoutShift holds the Cumulative Layout Shift (CLS) metric value, + // or a negative value if CLS is unknown. See https://web.dev/cls/ + CumulativeLayoutShift nullable.Float64 `json:"cls" validate:"min=0"` + + // FirstInputDelay holds the First Input Delay (FID) metric value, + // or a negative value if FID is unknown. See https://web.dev/fid/ + FirstInputDelay nullable.Float64 `json:"fid" validate:"min=0"` + + // TotalBlockingTime holds the Total Blocking Time (TBT) metric value, + // or a negative value if TBT is unknown. See https://web.dev/tbt/ + TotalBlockingTime nullable.Float64 `json:"tbt" validate:"min=0"` +} + +type user struct { + ID nullable.Interface `json:"id" validate:"max=1024,types=string;int"` Email nullable.String `json:"email" validate:"max=1024"` Name nullable.String `json:"username" validate:"max=1024"` } diff --git a/model/modeldecoder/v2/model_generated.go b/model/modeldecoder/v2/model_generated.go index 519c6155244..151bbacea90 100644 --- a/model/modeldecoder/v2/model_generated.go +++ b/model/modeldecoder/v2/model_generated.go @@ -20,54 +20,55 @@ package v2 import ( - "encoding/json" "fmt" + + "encoding/json" "unicode/utf8" ) -func (m *metadataRoot) IsSet() bool { - return m.Metadata.IsSet() +func (val *metadataRoot) IsSet() bool { + return val.Metadata.IsSet() } -func (m *metadataRoot) Reset() { - m.Metadata.Reset() +func (val *metadataRoot) Reset() { + val.Metadata.Reset() } -func (m *metadataRoot) validate() error { - if err := m.Metadata.validate(); err != nil { +func (val *metadataRoot) validate() error { + if err := val.Metadata.validate(); err != nil { return err } - if !m.Metadata.IsSet() { + if !val.Metadata.IsSet() { return fmt.Errorf("'metadata' required") } return nil } -func (m *metadata) IsSet() bool { - return m.Cloud.IsSet() || len(m.Labels) > 0 || m.Process.IsSet() || m.Service.IsSet() || m.System.IsSet() || m.User.IsSet() +func (val *metadata) IsSet() bool { + return val.Cloud.IsSet() || len(val.Labels) > 0 || val.Process.IsSet() || val.Service.IsSet() || val.System.IsSet() || val.User.IsSet() } -func (m *metadata) Reset() { - m.Cloud.Reset() - for k := range m.Labels { - delete(m.Labels, k) - } - m.Process.Reset() - m.Service.Reset() - m.System.Reset() - m.User.Reset() +func (val *metadata) Reset() { + val.Cloud.Reset() + for k := range val.Labels { + delete(val.Labels, k) + } + val.Process.Reset() + val.Service.Reset() + val.System.Reset() + val.User.Reset() } -func (m *metadata) validate() error { - if !m.IsSet() { +func (val *metadata) validate() error { + if !val.IsSet() { return nil } - if err := m.Cloud.validate(); err != nil { + if err := val.Cloud.validate(); err != nil { return err } - for k, v := range m.Labels { - if !labelsRegex.MatchString(k) { - return fmt.Errorf("validation rule 'patternKeys(labelsRegex)' violated for 'metadata.labels'") + for k, v := range val.Labels { + if k != "" && !regexpNoDotAsteriskQuote.MatchString(k) { + return fmt.Errorf("validation rule 'patternKeys(regexpNoDotAsteriskQuote)' violated for 'metadata.labels'") } switch t := v.(type) { case nil: @@ -81,513 +82,1154 @@ func (m *metadata) validate() error { return fmt.Errorf("validation rule 'typesVals(string;bool;number)' violated for 'metadata.labels' for key %s", k) } } - if err := m.Process.validate(); err != nil { + if err := val.Process.validate(); err != nil { return err } - if err := m.Service.validate(); err != nil { + if err := val.Service.validate(); err != nil { return err } - if !m.Service.IsSet() { + if !val.Service.IsSet() { return fmt.Errorf("'metadata.service' required") } - if err := m.System.validate(); err != nil { + if err := val.System.validate(); err != nil { return err } - if err := m.User.validate(); err != nil { + if err := val.User.validate(); err != nil { return err } return nil } -func (m *metadataCloud) IsSet() bool { - return m.Account.IsSet() || m.AvailabilityZone.IsSet() || m.Instance.IsSet() || m.Machine.IsSet() || m.Project.IsSet() || m.Provider.IsSet() || m.Region.IsSet() +func (val *metadataCloud) IsSet() bool { + return val.Account.IsSet() || val.AvailabilityZone.IsSet() || val.Instance.IsSet() || val.Machine.IsSet() || val.Project.IsSet() || val.Provider.IsSet() || val.Region.IsSet() } -func (m *metadataCloud) Reset() { - m.Account.Reset() - m.AvailabilityZone.Reset() - m.Instance.Reset() - m.Machine.Reset() - m.Project.Reset() - m.Provider.Reset() - m.Region.Reset() +func (val *metadataCloud) Reset() { + val.Account.Reset() + val.AvailabilityZone.Reset() + val.Instance.Reset() + val.Machine.Reset() + val.Project.Reset() + val.Provider.Reset() + val.Region.Reset() } -func (m *metadataCloud) validate() error { - if !m.IsSet() { +func (val *metadataCloud) validate() error { + if !val.IsSet() { return nil } - if err := m.Account.validate(); err != nil { + if err := val.Account.validate(); err != nil { return err } - if utf8.RuneCountInString(m.AvailabilityZone.Val) > 1024 { + if utf8.RuneCountInString(val.AvailabilityZone.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.cloud.availability_zone'") } - if err := m.Instance.validate(); err != nil { + if err := val.Instance.validate(); err != nil { return err } - if err := m.Machine.validate(); err != nil { + if err := val.Machine.validate(); err != nil { return err } - if err := m.Project.validate(); err != nil { + if err := val.Project.validate(); err != nil { return err } - if utf8.RuneCountInString(m.Provider.Val) > 1024 { + if utf8.RuneCountInString(val.Provider.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.cloud.provider'") } - if !m.Provider.IsSet() { + if !val.Provider.IsSet() { return fmt.Errorf("'metadata.cloud.provider' required") } - if utf8.RuneCountInString(m.Region.Val) > 1024 { + if utf8.RuneCountInString(val.Region.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.cloud.region'") } return nil } -func (m *metadataCloudAccount) IsSet() bool { - return m.ID.IsSet() || m.Name.IsSet() +func (val *metadataCloudAccount) IsSet() bool { + return val.ID.IsSet() || val.Name.IsSet() } -func (m *metadataCloudAccount) Reset() { - m.ID.Reset() - m.Name.Reset() +func (val *metadataCloudAccount) Reset() { + val.ID.Reset() + val.Name.Reset() } -func (m *metadataCloudAccount) validate() error { - if !m.IsSet() { +func (val *metadataCloudAccount) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.ID.Val) > 1024 { + if utf8.RuneCountInString(val.ID.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.cloud.account.id'") } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.cloud.account.name'") } return nil } -func (m *metadataCloudInstance) IsSet() bool { - return m.ID.IsSet() || m.Name.IsSet() +func (val *metadataCloudInstance) IsSet() bool { + return val.ID.IsSet() || val.Name.IsSet() } -func (m *metadataCloudInstance) Reset() { - m.ID.Reset() - m.Name.Reset() +func (val *metadataCloudInstance) Reset() { + val.ID.Reset() + val.Name.Reset() } -func (m *metadataCloudInstance) validate() error { - if !m.IsSet() { +func (val *metadataCloudInstance) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.ID.Val) > 1024 { + if utf8.RuneCountInString(val.ID.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.cloud.instance.id'") } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.cloud.instance.name'") } return nil } -func (m *metadataCloudMachine) IsSet() bool { - return m.Type.IsSet() +func (val *metadataCloudMachine) IsSet() bool { + return val.Type.IsSet() } -func (m *metadataCloudMachine) Reset() { - m.Type.Reset() +func (val *metadataCloudMachine) Reset() { + val.Type.Reset() } -func (m *metadataCloudMachine) validate() error { - if !m.IsSet() { +func (val *metadataCloudMachine) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Type.Val) > 1024 { + if utf8.RuneCountInString(val.Type.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.cloud.machine.type'") } return nil } -func (m *metadataCloudProject) IsSet() bool { - return m.ID.IsSet() || m.Name.IsSet() +func (val *metadataCloudProject) IsSet() bool { + return val.ID.IsSet() || val.Name.IsSet() } -func (m *metadataCloudProject) Reset() { - m.ID.Reset() - m.Name.Reset() +func (val *metadataCloudProject) Reset() { + val.ID.Reset() + val.Name.Reset() } -func (m *metadataCloudProject) validate() error { - if !m.IsSet() { +func (val *metadataCloudProject) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.ID.Val) > 1024 { + if utf8.RuneCountInString(val.ID.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.cloud.project.id'") } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.cloud.project.name'") } return nil } -func (m *metadataProcess) IsSet() bool { - return len(m.Argv) > 0 || m.Pid.IsSet() || m.Ppid.IsSet() || m.Title.IsSet() +func (val *metadataProcess) IsSet() bool { + return len(val.Argv) > 0 || val.Pid.IsSet() || val.Ppid.IsSet() || val.Title.IsSet() } -func (m *metadataProcess) Reset() { - m.Argv = m.Argv[:0] - m.Pid.Reset() - m.Ppid.Reset() - m.Title.Reset() +func (val *metadataProcess) Reset() { + val.Argv = val.Argv[:0] + val.Pid.Reset() + val.Ppid.Reset() + val.Title.Reset() } -func (m *metadataProcess) validate() error { - if !m.IsSet() { +func (val *metadataProcess) validate() error { + if !val.IsSet() { return nil } - if !m.Pid.IsSet() { + if !val.Pid.IsSet() { return fmt.Errorf("'metadata.process.pid' required") } - if utf8.RuneCountInString(m.Title.Val) > 1024 { + if utf8.RuneCountInString(val.Title.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.process.title'") } return nil } -func (m *metadataService) IsSet() bool { - return m.Agent.IsSet() || m.Environment.IsSet() || m.Framework.IsSet() || m.Language.IsSet() || m.Name.IsSet() || m.Node.IsSet() || m.Runtime.IsSet() || m.Version.IsSet() +func (val *metadataService) IsSet() bool { + return val.Agent.IsSet() || val.Environment.IsSet() || val.Framework.IsSet() || val.Language.IsSet() || val.Name.IsSet() || val.Node.IsSet() || val.Runtime.IsSet() || val.Version.IsSet() } -func (m *metadataService) Reset() { - m.Agent.Reset() - m.Environment.Reset() - m.Framework.Reset() - m.Language.Reset() - m.Name.Reset() - m.Node.Reset() - m.Runtime.Reset() - m.Version.Reset() +func (val *metadataService) Reset() { + val.Agent.Reset() + val.Environment.Reset() + val.Framework.Reset() + val.Language.Reset() + val.Name.Reset() + val.Node.Reset() + val.Runtime.Reset() + val.Version.Reset() } -func (m *metadataService) validate() error { - if !m.IsSet() { +func (val *metadataService) validate() error { + if !val.IsSet() { return nil } - if err := m.Agent.validate(); err != nil { + if err := val.Agent.validate(); err != nil { return err } - if !m.Agent.IsSet() { + if !val.Agent.IsSet() { return fmt.Errorf("'metadata.service.agent' required") } - if utf8.RuneCountInString(m.Environment.Val) > 1024 { + if utf8.RuneCountInString(val.Environment.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.environment'") } - if err := m.Framework.validate(); err != nil { + if err := val.Framework.validate(); err != nil { return err } - if err := m.Language.validate(); err != nil { + if err := val.Language.validate(); err != nil { return err } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.name'") } - if !alphaNumericExtRegex.MatchString(m.Name.Val) { - return fmt.Errorf("validation rule 'pattern(alphaNumericExtRegex)' violated for 'metadata.service.name'") + if utf8.RuneCountInString(val.Name.Val) < 1 { + return fmt.Errorf("validation rule 'min(1)' violated for 'metadata.service.name'") + } + if val.Name.Val != "" && !regexpAlphaNumericExt.MatchString(val.Name.Val) { + return fmt.Errorf("validation rule 'pattern(regexpAlphaNumericExt)' violated for 'metadata.service.name'") } - if !m.Name.IsSet() { + if !val.Name.IsSet() { return fmt.Errorf("'metadata.service.name' required") } - if err := m.Node.validate(); err != nil { + if err := val.Node.validate(); err != nil { return err } - if err := m.Runtime.validate(); err != nil { + if err := val.Runtime.validate(); err != nil { return err } - if utf8.RuneCountInString(m.Version.Val) > 1024 { + if utf8.RuneCountInString(val.Version.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.version'") } return nil } -func (m *metadataServiceAgent) IsSet() bool { - return m.EphemeralID.IsSet() || m.Name.IsSet() || m.Version.IsSet() +func (val *metadataServiceAgent) IsSet() bool { + return val.EphemeralID.IsSet() || val.Name.IsSet() || val.Version.IsSet() } -func (m *metadataServiceAgent) Reset() { - m.EphemeralID.Reset() - m.Name.Reset() - m.Version.Reset() +func (val *metadataServiceAgent) Reset() { + val.EphemeralID.Reset() + val.Name.Reset() + val.Version.Reset() } -func (m *metadataServiceAgent) validate() error { - if !m.IsSet() { +func (val *metadataServiceAgent) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.EphemeralID.Val) > 1024 { + if utf8.RuneCountInString(val.EphemeralID.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.agent.ephemeral_id'") } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.agent.name'") } - if !m.Name.IsSet() { + if utf8.RuneCountInString(val.Name.Val) < 1 { + return fmt.Errorf("validation rule 'min(1)' violated for 'metadata.service.agent.name'") + } + if !val.Name.IsSet() { return fmt.Errorf("'metadata.service.agent.name' required") } - if utf8.RuneCountInString(m.Version.Val) > 1024 { + if utf8.RuneCountInString(val.Version.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.agent.version'") } - if !m.Version.IsSet() { + if !val.Version.IsSet() { return fmt.Errorf("'metadata.service.agent.version' required") } return nil } -func (m *metadataServiceFramework) IsSet() bool { - return m.Name.IsSet() || m.Version.IsSet() +func (val *metadataServiceFramework) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() } -func (m *metadataServiceFramework) Reset() { - m.Name.Reset() - m.Version.Reset() +func (val *metadataServiceFramework) Reset() { + val.Name.Reset() + val.Version.Reset() } -func (m *metadataServiceFramework) validate() error { - if !m.IsSet() { +func (val *metadataServiceFramework) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.framework.name'") } - if utf8.RuneCountInString(m.Version.Val) > 1024 { + if utf8.RuneCountInString(val.Version.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.framework.version'") } return nil } -func (m *metadataServiceLanguage) IsSet() bool { - return m.Name.IsSet() || m.Version.IsSet() +func (val *metadataServiceLanguage) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() } -func (m *metadataServiceLanguage) Reset() { - m.Name.Reset() - m.Version.Reset() +func (val *metadataServiceLanguage) Reset() { + val.Name.Reset() + val.Version.Reset() } -func (m *metadataServiceLanguage) validate() error { - if !m.IsSet() { +func (val *metadataServiceLanguage) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.language.name'") } - if !m.Name.IsSet() { + if !val.Name.IsSet() { return fmt.Errorf("'metadata.service.language.name' required") } - if utf8.RuneCountInString(m.Version.Val) > 1024 { + if utf8.RuneCountInString(val.Version.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.language.version'") } return nil } -func (m *metadataServiceNode) IsSet() bool { - return m.Name.IsSet() +func (val *metadataServiceNode) IsSet() bool { + return val.Name.IsSet() } -func (m *metadataServiceNode) Reset() { - m.Name.Reset() +func (val *metadataServiceNode) Reset() { + val.Name.Reset() } -func (m *metadataServiceNode) validate() error { - if !m.IsSet() { +func (val *metadataServiceNode) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.node.configured_name'") } return nil } -func (m *metadataServiceRuntime) IsSet() bool { - return m.Name.IsSet() || m.Version.IsSet() +func (val *metadataServiceRuntime) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() } -func (m *metadataServiceRuntime) Reset() { - m.Name.Reset() - m.Version.Reset() +func (val *metadataServiceRuntime) Reset() { + val.Name.Reset() + val.Version.Reset() } -func (m *metadataServiceRuntime) validate() error { - if !m.IsSet() { +func (val *metadataServiceRuntime) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.runtime.name'") } - if !m.Name.IsSet() { + if !val.Name.IsSet() { return fmt.Errorf("'metadata.service.runtime.name' required") } - if utf8.RuneCountInString(m.Version.Val) > 1024 { + if utf8.RuneCountInString(val.Version.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.service.runtime.version'") } - if !m.Version.IsSet() { + if !val.Version.IsSet() { return fmt.Errorf("'metadata.service.runtime.version' required") } return nil } -func (m *metadataSystem) IsSet() bool { - return m.Architecture.IsSet() || m.ConfiguredHostname.IsSet() || m.Container.IsSet() || m.DetectedHostname.IsSet() || m.HostnameDeprecated.IsSet() || m.Kubernetes.IsSet() || m.Platform.IsSet() +func (val *metadataSystem) IsSet() bool { + return val.Architecture.IsSet() || val.ConfiguredHostname.IsSet() || val.Container.IsSet() || val.DetectedHostname.IsSet() || val.HostnameDeprecated.IsSet() || val.Kubernetes.IsSet() || val.Platform.IsSet() } -func (m *metadataSystem) Reset() { - m.Architecture.Reset() - m.ConfiguredHostname.Reset() - m.Container.Reset() - m.DetectedHostname.Reset() - m.HostnameDeprecated.Reset() - m.Kubernetes.Reset() - m.Platform.Reset() +func (val *metadataSystem) Reset() { + val.Architecture.Reset() + val.ConfiguredHostname.Reset() + val.Container.Reset() + val.DetectedHostname.Reset() + val.HostnameDeprecated.Reset() + val.Kubernetes.Reset() + val.Platform.Reset() } -func (m *metadataSystem) validate() error { - if !m.IsSet() { +func (val *metadataSystem) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Architecture.Val) > 1024 { + if utf8.RuneCountInString(val.Architecture.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.system.architecture'") } - if utf8.RuneCountInString(m.ConfiguredHostname.Val) > 1024 { + if utf8.RuneCountInString(val.ConfiguredHostname.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.system.configured_hostname'") } - if err := m.Container.validate(); err != nil { + if err := val.Container.validate(); err != nil { return err } - if utf8.RuneCountInString(m.DetectedHostname.Val) > 1024 { + if utf8.RuneCountInString(val.DetectedHostname.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.system.detected_hostname'") } - if utf8.RuneCountInString(m.HostnameDeprecated.Val) > 1024 { + if utf8.RuneCountInString(val.HostnameDeprecated.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.system.hostname'") } - if err := m.Kubernetes.validate(); err != nil { + if err := val.Kubernetes.validate(); err != nil { return err } - if utf8.RuneCountInString(m.Platform.Val) > 1024 { + if utf8.RuneCountInString(val.Platform.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.system.platform'") } return nil } -func (m *metadataSystemContainer) IsSet() bool { - return m.ID.IsSet() +func (val *metadataSystemContainer) IsSet() bool { + return val.ID.IsSet() } -func (m *metadataSystemContainer) Reset() { - m.ID.Reset() +func (val *metadataSystemContainer) Reset() { + val.ID.Reset() } -func (m *metadataSystemContainer) validate() error { - if !m.IsSet() { +func (val *metadataSystemContainer) validate() error { + if !val.IsSet() { return nil } return nil } -func (m *metadataSystemKubernetes) IsSet() bool { - return m.Namespace.IsSet() || m.Node.IsSet() || m.Pod.IsSet() +func (val *metadataSystemKubernetes) IsSet() bool { + return val.Namespace.IsSet() || val.Node.IsSet() || val.Pod.IsSet() } -func (m *metadataSystemKubernetes) Reset() { - m.Namespace.Reset() - m.Node.Reset() - m.Pod.Reset() +func (val *metadataSystemKubernetes) Reset() { + val.Namespace.Reset() + val.Node.Reset() + val.Pod.Reset() } -func (m *metadataSystemKubernetes) validate() error { - if !m.IsSet() { +func (val *metadataSystemKubernetes) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Namespace.Val) > 1024 { + if utf8.RuneCountInString(val.Namespace.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.system.kubernetes.namespace'") } - if err := m.Node.validate(); err != nil { + if err := val.Node.validate(); err != nil { return err } - if err := m.Pod.validate(); err != nil { + if err := val.Pod.validate(); err != nil { return err } return nil } -func (m *metadataSystemKubernetesNode) IsSet() bool { - return m.Name.IsSet() +func (val *metadataSystemKubernetesNode) IsSet() bool { + return val.Name.IsSet() } -func (m *metadataSystemKubernetesNode) Reset() { - m.Name.Reset() +func (val *metadataSystemKubernetesNode) Reset() { + val.Name.Reset() } -func (m *metadataSystemKubernetesNode) validate() error { - if !m.IsSet() { +func (val *metadataSystemKubernetesNode) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.system.kubernetes.node.name'") } return nil } -func (m *metadataSystemKubernetesPod) IsSet() bool { - return m.Name.IsSet() || m.UID.IsSet() +func (val *metadataSystemKubernetesPod) IsSet() bool { + return val.Name.IsSet() || val.UID.IsSet() } -func (m *metadataSystemKubernetesPod) Reset() { - m.Name.Reset() - m.UID.Reset() +func (val *metadataSystemKubernetesPod) Reset() { + val.Name.Reset() + val.UID.Reset() } -func (m *metadataSystemKubernetesPod) validate() error { - if !m.IsSet() { +func (val *metadataSystemKubernetesPod) validate() error { + if !val.IsSet() { return nil } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.system.kubernetes.pod.name'") } - if utf8.RuneCountInString(m.UID.Val) > 1024 { + if utf8.RuneCountInString(val.UID.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.system.kubernetes.pod.uid'") } return nil } -func (m *metadataUser) IsSet() bool { - return m.ID.IsSet() || m.Email.IsSet() || m.Name.IsSet() +func (val *user) IsSet() bool { + return val.ID.IsSet() || val.Email.IsSet() || val.Name.IsSet() } -func (m *metadataUser) Reset() { - m.ID.Reset() - m.Email.Reset() - m.Name.Reset() +func (val *user) Reset() { + val.ID.Reset() + val.Email.Reset() + val.Name.Reset() } -func (m *metadataUser) validate() error { - if !m.IsSet() { +func (val *user) validate() error { + if !val.IsSet() { return nil } - switch t := m.ID.Val.(type) { + switch t := val.ID.Val.(type) { case string: if utf8.RuneCountInString(t) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.user.id'") } + case int: case json.Number: if _, err := t.Int64(); err != nil { return fmt.Errorf("validation rule 'types(string;int)' violated for 'metadata.user.id'") } - case int: case nil: default: return fmt.Errorf("validation rule 'types(string;int)' violated for 'metadata.user.id'") } - if utf8.RuneCountInString(m.Email.Val) > 1024 { + if utf8.RuneCountInString(val.Email.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.user.email'") } - if utf8.RuneCountInString(m.Name.Val) > 1024 { + if utf8.RuneCountInString(val.Name.Val) > 1024 { return fmt.Errorf("validation rule 'max(1024)' violated for 'metadata.user.username'") } return nil } + +func (val *transactionRoot) IsSet() bool { + return val.Transaction.IsSet() +} + +func (val *transactionRoot) Reset() { + val.Transaction.Reset() +} + +func (val *transactionRoot) validate() error { + if err := val.Transaction.validate(); err != nil { + return err + } + if !val.Transaction.IsSet() { + return fmt.Errorf("'transaction' required") + } + return nil +} + +func (val *transaction) IsSet() bool { + return val.Context.IsSet() || val.Duration.IsSet() || val.ID.IsSet() || val.Marks.IsSet() || val.Name.IsSet() || val.Outcome.IsSet() || val.ParentID.IsSet() || val.Result.IsSet() || val.Sampled.IsSet() || val.SampleRate.IsSet() || val.SpanCount.IsSet() || val.Timestamp.IsSet() || val.TraceID.IsSet() || val.Type.IsSet() || val.UserExperience.IsSet() || val.Experimental.IsSet() +} + +func (val *transaction) Reset() { + val.Context.Reset() + val.Duration.Reset() + val.ID.Reset() + val.Marks.Reset() + val.Name.Reset() + val.Outcome.Reset() + val.ParentID.Reset() + val.Result.Reset() + val.Sampled.Reset() + val.SampleRate.Reset() + val.SpanCount.Reset() + val.Timestamp.Reset() + val.TraceID.Reset() + val.Type.Reset() + val.UserExperience.Reset() + val.Experimental.Reset() +} + +func (val *transaction) validate() error { + if !val.IsSet() { + return nil + } + if err := val.Context.validate(); err != nil { + return err + } + if val.Duration.Val < 0 { + return fmt.Errorf("validation rule 'min(0)' violated for 'transaction.duration'") + } + if !val.Duration.IsSet() { + return fmt.Errorf("'transaction.duration' required") + } + if utf8.RuneCountInString(val.ID.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.id'") + } + if !val.ID.IsSet() { + return fmt.Errorf("'transaction.id' required") + } + if err := val.Marks.validate(); err != nil { + return err + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.name'") + } + if val.Outcome.Val != "" { + var matchEnum bool + for _, s := range enumOutcome { + if val.Outcome.Val == s { + matchEnum = true + break + } + } + if !matchEnum { + return fmt.Errorf("validation rule 'enum(enumOutcome)' violated for 'transaction.outcome'") + } + } + if utf8.RuneCountInString(val.ParentID.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.parent_id'") + } + if utf8.RuneCountInString(val.Result.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.result'") + } + if err := val.SpanCount.validate(); err != nil { + return err + } + if !val.SpanCount.IsSet() { + return fmt.Errorf("'transaction.span_count' required") + } + if utf8.RuneCountInString(val.TraceID.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.trace_id'") + } + if !val.TraceID.IsSet() { + return fmt.Errorf("'transaction.trace_id' required") + } + if utf8.RuneCountInString(val.Type.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.type'") + } + if !val.Type.IsSet() { + return fmt.Errorf("'transaction.type' required") + } + if err := val.UserExperience.validate(); err != nil { + return err + } + return nil +} + +func (val *context) IsSet() bool { + return len(val.Custom) > 0 || val.Message.IsSet() || val.Page.IsSet() || val.Response.IsSet() || val.Request.IsSet() || val.Service.IsSet() || len(val.Tags) > 0 || val.User.IsSet() +} + +func (val *context) Reset() { + for k := range val.Custom { + delete(val.Custom, k) + } + val.Message.Reset() + val.Page.Reset() + val.Response.Reset() + val.Request.Reset() + val.Service.Reset() + for k := range val.Tags { + delete(val.Tags, k) + } + val.User.Reset() +} + +func (val *context) validate() error { + if !val.IsSet() { + return nil + } + for k := range val.Custom { + if k != "" && !regexpNoDotAsteriskQuote.MatchString(k) { + return fmt.Errorf("validation rule 'patternKeys(regexpNoDotAsteriskQuote)' violated for 'transaction.context.custom'") + } + } + if err := val.Message.validate(); err != nil { + return err + } + if err := val.Page.validate(); err != nil { + return err + } + if err := val.Response.validate(); err != nil { + return err + } + if err := val.Request.validate(); err != nil { + return err + } + if err := val.Service.validate(); err != nil { + return err + } + for k, v := range val.Tags { + if k != "" && !regexpNoDotAsteriskQuote.MatchString(k) { + return fmt.Errorf("validation rule 'patternKeys(regexpNoDotAsteriskQuote)' violated for 'transaction.context.tags'") + } + switch t := v.(type) { + case nil: + case string: + if utf8.RuneCountInString(t) > 1024 { + return fmt.Errorf("validation rule 'maxVals(1024)' violated for 'transaction.context.tags'") + } + case bool: + case json.Number: + default: + return fmt.Errorf("validation rule 'typesVals(string;bool;number)' violated for 'transaction.context.tags' for key %s", k) + } + } + if err := val.User.validate(); err != nil { + return err + } + return nil +} + +func (val *contextMessage) IsSet() bool { + return val.Body.IsSet() || val.Headers.IsSet() || val.Age.IsSet() || val.Queue.IsSet() +} + +func (val *contextMessage) Reset() { + val.Body.Reset() + val.Headers.Reset() + val.Age.Reset() + val.Queue.Reset() +} + +func (val *contextMessage) validate() error { + if !val.IsSet() { + return nil + } + if err := val.Age.validate(); err != nil { + return err + } + if err := val.Queue.validate(); err != nil { + return err + } + return nil +} + +func (val *contextMessageAge) IsSet() bool { + return val.Milliseconds.IsSet() +} + +func (val *contextMessageAge) Reset() { + val.Milliseconds.Reset() +} + +func (val *contextMessageAge) validate() error { + if !val.IsSet() { + return nil + } + return nil +} + +func (val *contextMessageQueue) IsSet() bool { + return val.Name.IsSet() +} + +func (val *contextMessageQueue) Reset() { + val.Name.Reset() +} + +func (val *contextMessageQueue) validate() error { + if !val.IsSet() { + return nil + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.message.queue.name'") + } + return nil +} + +func (val *contextPage) IsSet() bool { + return val.URL.IsSet() || val.Referer.IsSet() +} + +func (val *contextPage) Reset() { + val.URL.Reset() + val.Referer.Reset() +} + +func (val *contextPage) validate() error { + if !val.IsSet() { + return nil + } + return nil +} + +func (val *contextResponse) IsSet() bool { + return val.DecodedBodySize.IsSet() || val.EncodedBodySize.IsSet() || val.Finished.IsSet() || val.Headers.IsSet() || val.HeadersSent.IsSet() || val.StatusCode.IsSet() || val.TransferSize.IsSet() +} + +func (val *contextResponse) Reset() { + val.DecodedBodySize.Reset() + val.EncodedBodySize.Reset() + val.Finished.Reset() + val.Headers.Reset() + val.HeadersSent.Reset() + val.StatusCode.Reset() + val.TransferSize.Reset() +} + +func (val *contextResponse) validate() error { + if !val.IsSet() { + return nil + } + return nil +} + +func (val *contextRequest) IsSet() bool { + return val.Cookies.IsSet() || val.Body.IsSet() || val.Env.IsSet() || val.Headers.IsSet() || val.HTTPVersion.IsSet() || val.Method.IsSet() || val.Socket.IsSet() || val.URL.IsSet() +} + +func (val *contextRequest) Reset() { + val.Cookies.Reset() + val.Body.Reset() + val.Env.Reset() + val.Headers.Reset() + val.HTTPVersion.Reset() + val.Method.Reset() + val.Socket.Reset() + val.URL.Reset() +} + +func (val *contextRequest) validate() error { + if !val.IsSet() { + return nil + } + switch val.Body.Val.(type) { + case string: + case map[string]interface{}: + case nil: + default: + return fmt.Errorf("validation rule 'types(string;map[string]interface)' violated for 'transaction.context.request.body'") + } + if utf8.RuneCountInString(val.HTTPVersion.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.request.http_version'") + } + if utf8.RuneCountInString(val.Method.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.request.method'") + } + if !val.Method.IsSet() { + return fmt.Errorf("'transaction.context.request.method' required") + } + if err := val.Socket.validate(); err != nil { + return err + } + if err := val.URL.validate(); err != nil { + return err + } + return nil +} + +func (val *contextRequestSocket) IsSet() bool { + return val.RemoteAddress.IsSet() || val.Encrypted.IsSet() +} + +func (val *contextRequestSocket) Reset() { + val.RemoteAddress.Reset() + val.Encrypted.Reset() +} + +func (val *contextRequestSocket) validate() error { + if !val.IsSet() { + return nil + } + return nil +} + +func (val *contextRequestURL) IsSet() bool { + return val.Full.IsSet() || val.Hash.IsSet() || val.Hostname.IsSet() || val.Path.IsSet() || val.Port.IsSet() || val.Protocol.IsSet() || val.Raw.IsSet() || val.Search.IsSet() +} + +func (val *contextRequestURL) Reset() { + val.Full.Reset() + val.Hash.Reset() + val.Hostname.Reset() + val.Path.Reset() + val.Port.Reset() + val.Protocol.Reset() + val.Raw.Reset() + val.Search.Reset() +} + +func (val *contextRequestURL) validate() error { + if !val.IsSet() { + return nil + } + if utf8.RuneCountInString(val.Full.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.request.url.full'") + } + if utf8.RuneCountInString(val.Hash.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.request.url.hash'") + } + if utf8.RuneCountInString(val.Hostname.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.request.url.hostname'") + } + if utf8.RuneCountInString(val.Path.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.request.url.pathname'") + } + switch t := val.Port.Val.(type) { + case string: + if utf8.RuneCountInString(t) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.request.url.port'") + } + case int: + case json.Number: + if _, err := t.Int64(); err != nil { + return fmt.Errorf("validation rule 'types(string;int)' violated for 'transaction.context.request.url.port'") + } + case nil: + default: + return fmt.Errorf("validation rule 'types(string;int)' violated for 'transaction.context.request.url.port'") + } + if utf8.RuneCountInString(val.Protocol.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.request.url.protocol'") + } + if utf8.RuneCountInString(val.Raw.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.request.url.raw'") + } + if utf8.RuneCountInString(val.Search.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.request.url.search'") + } + return nil +} + +func (val *contextService) IsSet() bool { + return val.Agent.IsSet() || val.Environment.IsSet() || val.Framework.IsSet() || val.Language.IsSet() || val.Name.IsSet() || val.Node.IsSet() || val.Runtime.IsSet() || val.Version.IsSet() +} + +func (val *contextService) Reset() { + val.Agent.Reset() + val.Environment.Reset() + val.Framework.Reset() + val.Language.Reset() + val.Name.Reset() + val.Node.Reset() + val.Runtime.Reset() + val.Version.Reset() +} + +func (val *contextService) validate() error { + if !val.IsSet() { + return nil + } + if err := val.Agent.validate(); err != nil { + return err + } + if utf8.RuneCountInString(val.Environment.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.environment'") + } + if err := val.Framework.validate(); err != nil { + return err + } + if err := val.Language.validate(); err != nil { + return err + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.name'") + } + if val.Name.Val != "" && !regexpAlphaNumericExt.MatchString(val.Name.Val) { + return fmt.Errorf("validation rule 'pattern(regexpAlphaNumericExt)' violated for 'transaction.context.service.name'") + } + if err := val.Node.validate(); err != nil { + return err + } + if err := val.Runtime.validate(); err != nil { + return err + } + if utf8.RuneCountInString(val.Version.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.version'") + } + return nil +} + +func (val *contextServiceAgent) IsSet() bool { + return val.EphemeralID.IsSet() || val.Name.IsSet() || val.Version.IsSet() +} + +func (val *contextServiceAgent) Reset() { + val.EphemeralID.Reset() + val.Name.Reset() + val.Version.Reset() +} + +func (val *contextServiceAgent) validate() error { + if !val.IsSet() { + return nil + } + if utf8.RuneCountInString(val.EphemeralID.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.agent.ephemeral_id'") + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.agent.name'") + } + if utf8.RuneCountInString(val.Version.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.agent.version'") + } + return nil +} + +func (val *contextServiceFramework) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() +} + +func (val *contextServiceFramework) Reset() { + val.Name.Reset() + val.Version.Reset() +} + +func (val *contextServiceFramework) validate() error { + if !val.IsSet() { + return nil + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.framework.name'") + } + if utf8.RuneCountInString(val.Version.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.framework.version'") + } + return nil +} + +func (val *contextServiceLanguage) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() +} + +func (val *contextServiceLanguage) Reset() { + val.Name.Reset() + val.Version.Reset() +} + +func (val *contextServiceLanguage) validate() error { + if !val.IsSet() { + return nil + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.language.name'") + } + if utf8.RuneCountInString(val.Version.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.language.version'") + } + return nil +} + +func (val *contextServiceNode) IsSet() bool { + return val.Name.IsSet() +} + +func (val *contextServiceNode) Reset() { + val.Name.Reset() +} + +func (val *contextServiceNode) validate() error { + if !val.IsSet() { + return nil + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.node.configured_name'") + } + return nil +} + +func (val *contextServiceRuntime) IsSet() bool { + return val.Name.IsSet() || val.Version.IsSet() +} + +func (val *contextServiceRuntime) Reset() { + val.Name.Reset() + val.Version.Reset() +} + +func (val *contextServiceRuntime) validate() error { + if !val.IsSet() { + return nil + } + if utf8.RuneCountInString(val.Name.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.runtime.name'") + } + if utf8.RuneCountInString(val.Version.Val) > 1024 { + return fmt.Errorf("validation rule 'max(1024)' violated for 'transaction.context.service.runtime.version'") + } + return nil +} + +func (val *transactionMarks) IsSet() bool { + return len(val.Events) > 0 +} + +func (val *transactionMarks) Reset() { + for k := range val.Events { + delete(val.Events, k) + } +} + +func (val *transactionMarks) validate() error { + if !val.IsSet() { + return nil + } + for k, v := range val.Events { + if k != "" && !regexpNoDotAsteriskQuote.MatchString(k) { + return fmt.Errorf("validation rule 'patternKeys(regexpNoDotAsteriskQuote)' violated for 'transaction.marks.events'") + } + if err := v.validate(); err != nil { + return err + } + } + return nil +} + +func (val *transactionMarkEvents) IsSet() bool { + return len(val.Measurements) > 0 +} + +func (val *transactionMarkEvents) Reset() { + for k := range val.Measurements { + delete(val.Measurements, k) + } +} + +func (val *transactionMarkEvents) validate() error { + if !val.IsSet() { + return nil + } + for k := range val.Measurements { + if k != "" && !regexpNoDotAsteriskQuote.MatchString(k) { + return fmt.Errorf("validation rule 'patternKeys(regexpNoDotAsteriskQuote)' violated for 'transaction.marks.events.measurements'") + } + } + return nil +} + +func (val *transactionSpanCount) IsSet() bool { + return val.Dropped.IsSet() || val.Started.IsSet() +} + +func (val *transactionSpanCount) Reset() { + val.Dropped.Reset() + val.Started.Reset() +} + +func (val *transactionSpanCount) validate() error { + if !val.IsSet() { + return nil + } + if !val.Started.IsSet() { + return fmt.Errorf("'transaction.span_count.started' required") + } + return nil +} + +func (val *transactionUserExperience) IsSet() bool { + return val.CumulativeLayoutShift.IsSet() || val.FirstInputDelay.IsSet() || val.TotalBlockingTime.IsSet() +} + +func (val *transactionUserExperience) Reset() { + val.CumulativeLayoutShift.Reset() + val.FirstInputDelay.Reset() + val.TotalBlockingTime.Reset() +} + +func (val *transactionUserExperience) validate() error { + if !val.IsSet() { + return nil + } + if val.CumulativeLayoutShift.Val < 0 { + return fmt.Errorf("validation rule 'min(0)' violated for 'transaction.experience.cls'") + } + if val.FirstInputDelay.Val < 0 { + return fmt.Errorf("validation rule 'min(0)' violated for 'transaction.experience.fid'") + } + if val.TotalBlockingTime.Val < 0 { + return fmt.Errorf("validation rule 'min(0)' violated for 'transaction.experience.tbt'") + } + return nil +} diff --git a/model/modeldecoder/v2/transaction_test.go b/model/modeldecoder/v2/transaction_test.go new file mode 100644 index 00000000000..2c83307fa2a --- /dev/null +++ b/model/modeldecoder/v2/transaction_test.go @@ -0,0 +1,385 @@ +// Licensed to Elasticsearch B.V. under one or more contributor +// license agreements. See the NOTICE file distributed with +// this work for additional information regarding copyright +// ownership. Elasticsearch B.V. licenses this file to you under +// the Apache License, Version 2.0 (the "License"); you may +// not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +package v2 + +import ( + "net" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/elastic/apm-server/decoder" + "github.com/elastic/apm-server/model" + "github.com/elastic/apm-server/model/modeldecoder" + "github.com/elastic/apm-server/model/modeldecoder/modeldecodertest" + "github.com/elastic/beats/v7/libbeat/common" +) + +func TestTransactionSetResetIsSet(t *testing.T) { + var tRoot transactionRoot + modeldecodertest.DecodeData(t, reader(t, "transactions"), "transaction", &tRoot) + require.True(t, tRoot.IsSet()) + // call Reset and ensure initial state, except for array capacity + tRoot.Reset() + assert.False(t, tRoot.IsSet()) +} + +func TestResetTransactionOnRelease(t *testing.T) { + inp := `{"transaction":{"name":"tr-a"}}` + tr := fetchTransactionRoot() + require.NoError(t, decoder.NewJSONIteratorDecoder(strings.NewReader(inp)).Decode(tr)) + require.True(t, tr.IsSet()) + releaseTransactionRoot(tr) + assert.False(t, tr.IsSet()) +} + +func TestDecodeNestedTransaction(t *testing.T) { + t.Run("decode", func(t *testing.T) { + now := time.Now() + input := modeldecoder.Input{Metadata: model.Metadata{}, RequestTime: now, Config: modeldecoder.Config{Experimental: true}} + str := `{"transaction":{"duration":100,"timestamp":1599996822281000,"id":"100","trace_id":"1","type":"request","span_count":{"started":2},"experimental":"exp"}}` + dec := decoder.NewJSONIteratorDecoder(strings.NewReader(str)) + var out model.Transaction + require.NoError(t, DecodeNestedTransaction(dec, &input, &out)) + assert.Equal(t, "request", out.Type) + assert.Equal(t, "exp", out.Experimental) + assert.Equal(t, "2020-09-13 11:33:42.281 +0000 UTC", out.Timestamp.String()) + + input = modeldecoder.Input{Metadata: model.Metadata{}, RequestTime: now, Config: modeldecoder.Config{Experimental: false}} + str = `{"transaction":{"duration":100,"id":"100","trace_id":"1","type":"request","span_count":{"started":2},"experimental":"exp"}}` + dec = decoder.NewJSONIteratorDecoder(strings.NewReader(str)) + out = model.Transaction{} + require.NoError(t, DecodeNestedTransaction(dec, &input, &out)) + // experimental should only be set if allowed by configuration + assert.Nil(t, out.Experimental) + // if no timestamp is provided, fall back to request time + assert.Equal(t, now, out.Timestamp) + + err := DecodeNestedTransaction(decoder.NewJSONIteratorDecoder(strings.NewReader(`malformed`)), &input, &out) + require.Error(t, err) + assert.Contains(t, err.Error(), "decode") + }) + + t.Run("validate", func(t *testing.T) { + var out model.Transaction + err := DecodeNestedTransaction(decoder.NewJSONIteratorDecoder(strings.NewReader(`{}`)), &modeldecoder.Input{}, &out) + require.Error(t, err) + assert.Contains(t, err.Error(), "validation") + }) +} + +func TestDecodeMapToTransactionModel(t *testing.T) { + localhostIP := net.ParseIP("127.0.0.1") + gatewayIP := net.ParseIP("192.168.0.1") + randomIP := net.ParseIP("71.0.54.1") + exceptions := func(key string) bool { + return key == "RepresentativeCount" + } + + initializedMeta := func() *model.Metadata { + var inputMeta metadata + var meta model.Metadata + modeldecodertest.SetStructValues(&inputMeta, "meta", 1, false, time.Now()) + mapToMetadataModel(&inputMeta, &meta) + // initialize values that are not set by input + meta.UserAgent = model.UserAgent{Name: "meta", Original: "meta"} + meta.Client.IP = localhostIP + meta.System.IP = localhostIP + return &meta + } + + t.Run("set-metadata", func(t *testing.T) { + // do not overwrite metadata with zero transaction values + var inputTr transaction + var tr model.Transaction + mapToTransactionModel(&inputTr, initializedMeta(), time.Now(), true, &tr) + // iterate through metadata model and assert values are set + modeldecodertest.AssertStructValues(t, &tr.Metadata, exceptions, "meta", 1, false, localhostIP, time.Now()) + }) + + t.Run("overwrite-metadata", func(t *testing.T) { + // overwrite defined metadata with transaction metadata values + var inputTr transaction + var tr model.Transaction + modeldecodertest.SetStructValues(&inputTr, "overwritten", 5000, false, time.Now()) + inputTr.Context.Request.Headers.Val.Add("user-agent", "first") + inputTr.Context.Request.Headers.Val.Add("user-agent", "second") + inputTr.Context.Request.Headers.Val.Add("x-real-ip", gatewayIP.String()) + mapToTransactionModel(&inputTr, initializedMeta(), time.Now(), true, &tr) + + // user-agent should be set to context request header values + assert.Equal(t, "first, second", tr.Metadata.UserAgent.Original) + // do not overwrite client.ip if already set in metadata + assert.Equal(t, localhostIP, tr.Metadata.Client.IP, tr.Metadata.Client.IP.String()) + // metadata labels and transaction labels should not be merged + assert.Equal(t, common.MapStr{"meta": "meta"}, tr.Metadata.Labels) + assert.Equal(t, &model.Labels{"overwritten": "overwritten"}, tr.Labels) + // service values should be set + modeldecodertest.AssertStructValues(t, &tr.Metadata.Service, exceptions, "overwritten", 100, true, localhostIP, time.Now()) + // user values should be set + modeldecodertest.AssertStructValues(t, &tr.Metadata.User, exceptions, "overwritten", 100, true, localhostIP, time.Now()) + }) + + t.Run("client-ip-header", func(t *testing.T) { + var inputTr transaction + var tr model.Transaction + inputTr.Context.Request.Headers.Set(http.Header{}) + inputTr.Context.Request.Headers.Val.Add("x-real-ip", gatewayIP.String()) + inputTr.Context.Request.Socket.RemoteAddress.Set(randomIP.String()) + mapToTransactionModel(&inputTr, &model.Metadata{}, time.Now(), false, &tr) + assert.Equal(t, gatewayIP, tr.Metadata.Client.IP, tr.Metadata.Client.IP.String()) + }) + + t.Run("client-ip-socket", func(t *testing.T) { + var inputTr transaction + var tr model.Transaction + inputTr.Context.Request.Socket.RemoteAddress.Set(randomIP.String()) + mapToTransactionModel(&inputTr, &model.Metadata{}, time.Now(), false, &tr) + assert.Equal(t, randomIP, tr.Metadata.Client.IP, tr.Metadata.Client.IP.String()) + }) + + t.Run("overwrite-user", func(t *testing.T) { + // user should be populated by metadata or event specific, but not merged + var inputTr transaction + var tr model.Transaction + inputTr.Context.User.Email.Set("test@user.com") + mapToTransactionModel(&inputTr, initializedMeta(), time.Now(), false, &tr) + assert.Equal(t, "test@user.com", tr.Metadata.User.Email) + assert.Zero(t, tr.Metadata.User.ID) + assert.Zero(t, tr.Metadata.User.Name) + }) + + t.Run("other-transaction-values", func(t *testing.T) { + exceptions := func(key string) bool { + // metadata are tested separately + // URL parts are derived from url (separately tested) + // experimental is tested separately + // RepresentativeCount is not set by decoder + if strings.HasPrefix(key, "Metadata") || strings.HasPrefix(key, "Page.URL") || + key == "Experimental" || key == "RepresentativeCount" { + return true + } + + return false + } + + var inputTr transaction + var tr model.Transaction + eventTime, reqTime := time.Now(), time.Now().Add(time.Second) + modeldecodertest.SetStructValues(&inputTr, "overwritten", 5000, true, eventTime) + mapToTransactionModel(&inputTr, initializedMeta(), reqTime, true, &tr) + modeldecodertest.AssertStructValues(t, &tr, exceptions, "overwritten", 5000, true, localhostIP, eventTime) + + // set requestTime if eventTime is zero + modeldecodertest.SetStructValues(&inputTr, "overwritten", 5000, true, time.Time{}) + mapToTransactionModel(&inputTr, initializedMeta(), reqTime, true, &tr) + modeldecodertest.AssertStructValues(t, &tr, exceptions, "overwritten", 5000, true, localhostIP, reqTime) + + }) + + t.Run("page.URL", func(t *testing.T) { + var inputTr transaction + inputTr.Context.Page.URL.Set("https://my.site.test:9201") + var tr model.Transaction + mapToTransactionModel(&inputTr, initializedMeta(), time.Now(), false, &tr) + assert.Equal(t, "https://my.site.test:9201", *tr.Page.URL.Full) + assert.Equal(t, 9201, *tr.Page.URL.Port) + assert.Equal(t, "https", *tr.Page.URL.Scheme) + }) + +} + +func TestTransactionValidationRules(t *testing.T) { + testTransaction := func(t *testing.T, key string, tc testcase) { + var event transaction + r := reader(t, "transactions") + modeldecodertest.DecodeDataWithReplacement(t, r, "transaction", key, tc.data, &event) + + // run validation and checks + err := event.validate() + if tc.errorKey == "" { + assert.NoError(t, err) + } else { + require.Error(t, err) + assert.Contains(t, err.Error(), tc.errorKey) + } + } + + t.Run("context", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "custom", data: `{"custom":{"k1":{"v1":123,"v2":"value"},"k2":34,"k3":[{"a.1":1,"b*\"":2}]}}`}, + {name: "custom-key-dot", errorKey: "patternKeys", data: `{"custom":{"k1.":{"v1":123,"v2":"value"}}}`}, + {name: "custom-key-asterisk", errorKey: "patternKeys", data: `{"custom":{"k1*":{"v1":123,"v2":"value"}}}`}, + {name: "custom-key-quote", errorKey: "patternKeys", data: `{"custom":{"k1\"":{"v1":123,"v2":"value"}}}`}, + {name: "tags", data: `{"tags":{"k1":"v1.s*\"","k2":34,"k3":23.56,"k4":true}}`}, + {name: "tags-key-dot", errorKey: "patternKeys", data: `{"tags":{"k1.":"v1"}}`}, + {name: "tags-key-asterisk", errorKey: "patternKeys", data: `{"tags":{"k1*":"v1"}}`}, + {name: "tags-key-quote", errorKey: "patternKeys", data: `{"tags":{"k1\"":"v1"}}`}, + {name: "tags-invalid-type", errorKey: "typesVals", data: `{"tags":{"k1":{"v1":"abc"}}}`}, + {name: "tags-invalid-type", errorKey: "typesVals", data: `{"tags":{"k1":{"v1":[1,2,3]}}}`}, + {name: "tags-maxVal", data: `{"tags":{"k1":"` + modeldecodertest.BuildString(1024) + `"}}`}, + {name: "tags-maxVal-exceeded", errorKey: "maxVals", data: `{"tags":{"k1":"` + modeldecodertest.BuildString(1025) + `"}}`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "context", tc) + }) + } + }) + + // this tests an arbitrary field to ensure the max rule works as expected + t.Run("max", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "context-message-queue-name", data: `{"message":{"queue":{"name":"` + modeldecodertest.BuildString(1024) + `"}}}`}, + {name: "context-message-queue-name", errorKey: "max", data: `{"message":{"queue":{"name":"` + modeldecodertest.BuildString(1025) + `"}}}`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "context", tc) + }) + } + }) + + t.Run("request", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "request-body-string", data: `"body":"value"`}, + {name: "request-body-object", data: `"body":{"a":"b"}`}, + {name: "request-body-array", errorKey: "transaction.context.request.body", data: `"body":[1,2]`}, + } { + t.Run(tc.name, func(t *testing.T) { + tc.data = `{"request":{"method":"get",` + tc.data + `}}` + testTransaction(t, "context", tc) + }) + } + }) + + t.Run("service", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "service-name-az", data: `{"service":{"name":"abcdefghijklmnopqrstuvwxyz"}}`}, + {name: "service-name-AZ", data: `{"service":{"name":"ABCDEFGHIJKLMNOPQRSTUVWXYZ"}}`}, + {name: "service-name-09 _-", data: `{"service":{"name":"0123456789 -_"}}`}, + {name: "service-name-invalid", errorKey: "regexpAlphaNumericExt", data: `{"service":{"name":"⌘"}}`}, + {name: "service-name-max", data: `{"service":{"name":"` + modeldecodertest.BuildStringWith(1024, '-') + `"}}`}, + {name: "service-name-max-exceeded", errorKey: "max", data: `{"service":{"name":"` + modeldecodertest.BuildStringWith(1025, '-') + `"}}`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "context", tc) + }) + } + }) + + t.Run("duration", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "duration", data: `0.0`}, + {name: "duration", errorKey: "min", data: `-0.09`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "duration", tc) + }) + } + }) + + t.Run("marks", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "marks", data: `{"k1":{"v1":12.3}}`}, + {name: "marks-dot", errorKey: "patternKeys", data: `{"k.1":{"v1":12.3}}`}, + {name: "marks-events-dot", errorKey: "patternKeys", data: `{"k1":{"v.1":12.3}}`}, + {name: "marks-asterisk", errorKey: "patternKeys", data: `{"k*1":{"v1":12.3}}`}, + {name: "marks-events-asterisk", errorKey: "patternKeys", data: `{"k1":{"v*1":12.3}}`}, + {name: "marks-quote", errorKey: "patternKeys", data: `{"k\"1":{"v1":12.3}}`}, + {name: "marks-events-quote", errorKey: "patternKeys", data: `{"k1":{"v\"1":12.3}}`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "marks", tc) + }) + } + }) + + t.Run("outcome", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "outcome-success", data: `"success"`}, + {name: "outcome-failure", data: `"failure"`}, + {name: "outcome-unknown", data: `"unknown"`}, + {name: "outcome-invalid", errorKey: "enum", data: `"anything"`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "outcome", tc) + }) + } + }) + + t.Run("url", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "port-string", data: `"port":"8200"`}, + {name: "port-int", data: `"port":8200`}, + {name: "port-invalid-type", errorKey: "types", data: `"port":[8200,8201]`}, + {name: "port-invalid-type", errorKey: "types", data: `"port":{"val":8200}`}, + } { + t.Run(tc.name, func(t *testing.T) { + tc.data = `{"request":{"method":"get","url":{` + tc.data + `}}}` + testTransaction(t, "context", tc) + }) + } + }) + + t.Run("user", func(t *testing.T) { + for _, tc := range []testcase{ + {name: "id-string", data: `{"user":{"id":"user123"}}`}, + {name: "id-int", data: `{"user":{"id":44}}`}, + {name: "id-float", errorKey: "types", data: `{"user":{"id":45.6}}`}, + {name: "id-bool", errorKey: "types", data: `{"user":{"id":true}}`}, + {name: "id-string-max-len", data: `{"user":{"id":"` + modeldecodertest.BuildString(1024) + `"}}`}, + {name: "id-string-max-len-exceeded", errorKey: "max", data: `{"user":{"id":"` + modeldecodertest.BuildString(1025) + `"}}`}, + } { + t.Run(tc.name, func(t *testing.T) { + testTransaction(t, "context", tc) + }) + } + }) + + t.Run("required", func(t *testing.T) { + // setup: create full metadata struct with arbitrary values set + var event transaction + modeldecodertest.InitStructValues(&event) + // test vanilla struct is valid + require.NoError(t, event.validate()) + + // iterate through struct, remove every key one by one + // and test that validation behaves as expected + requiredKeys := map[string]interface{}{ + "duration": nil, + "id": nil, + "span_count": nil, + "span_count.started": nil, + "trace_id": nil, + "type": nil, + "context.request.method": nil, + } + modeldecodertest.SetZeroStructValue(&event, func(key string) { + err := event.validate() + if _, ok := requiredKeys[key]; ok { + require.Error(t, err, key) + assert.Contains(t, err.Error(), key) + } else { + assert.NoError(t, err, key) + } + }) + }) +}