From ce433399c32c824d3b8a93b619d637c3c0576184 Mon Sep 17 00:00:00 2001 From: Mihail Stoykov Date: Tue, 25 Apr 2023 11:21:41 +0300 Subject: [PATCH] k6/execution: Add metadata JS API A bare minimum API to set and get metadata set for the whole VU. It is added on top of the vu property and on top of a new `metrics` one so. If `k6/execution` is imported as `exec` - `exec.vu.metrics.metadata["foo"]` will get you the currently set metadata for `foo` the same way `exec.vu.tags["bar"]` will get you the value of the tag `bar`. Setting metadata is the same. It also adds `exec.vu.metrics.tags` which is the same as `exec.vu.tags` for consistency. While leaving the old one for backwards compatibility. This also includes dropping the ability to list metadata keys and values from `exec.vu.tags`. This was likely left in by mistake. This is a breaking change due to the fact that before that `iter` and `vu` could've been accessed this way but this will now need to be done through the new `exec.vu.metrics.metadata`. Closes #2766 --- js/common/tags.go | 33 ++++- js/modules/k6/execution/execution.go | 92 +++++++++++-- js/modules/k6/execution/execution_test.go | 159 +++++++++++++--------- 3 files changed, 208 insertions(+), 76 deletions(-) diff --git a/js/common/tags.go b/js/common/tags.go index 11467f32c0b..d07d822f856 100644 --- a/js/common/tags.go +++ b/js/common/tags.go @@ -20,7 +20,7 @@ func ApplyCustomUserTags(rt *goja.Runtime, tagsAndMeta *metrics.TagsAndMeta, key keyValuesObj := keyValues.ToObject(rt) for _, key := range keyValuesObj.Keys() { - if err := ApplyCustomUserTag(rt, tagsAndMeta, key, keyValuesObj.Get(key)); err != nil { + if err := ApplyCustomUserTag(tagsAndMeta, key, keyValuesObj.Get(key)); err != nil { return err } } @@ -29,8 +29,8 @@ func ApplyCustomUserTags(rt *goja.Runtime, tagsAndMeta *metrics.TagsAndMeta, key } // ApplyCustomUserTag modifies the given metrics.TagsAndMeta object with the -// given custom tag or metadata and theirs value. -func ApplyCustomUserTag(rt *goja.Runtime, tagsAndMeta *metrics.TagsAndMeta, key string, val goja.Value) error { +// given custom tag and theirs value. +func ApplyCustomUserTag(tagsAndMeta *metrics.TagsAndMeta, key string, val goja.Value) error { kind := reflect.Invalid if typ := val.ExportType(); typ != nil { kind = typ.Kind() @@ -54,3 +54,30 @@ func ApplyCustomUserTag(rt *goja.Runtime, tagsAndMeta *metrics.TagsAndMeta, key ) } } + +// ApplyCustomUserMetadata modifies the given metrics.TagsAndMeta object with the +// given custom metadata and theirs value. +func ApplyCustomUserMetadata(tagsAndMeta *metrics.TagsAndMeta, key string, val goja.Value) error { + kind := reflect.Invalid + if typ := val.ExportType(); typ != nil { + kind = typ.Kind() + } + + switch kind { + case + reflect.String, + reflect.Bool, + reflect.Int64, + reflect.Float64: + + tagsAndMeta.SetMetadata(key, val.String()) + return nil + + default: + return fmt.Errorf( + "invalid value for metric metadata '%s': "+ + "only String, Boolean and Number types are accepted as a metric metadata values", + key, + ) + } +} diff --git a/js/modules/k6/execution/execution.go b/js/modules/k6/execution/execution.go index a85fa0e1e7f..1ce9db300bc 100644 --- a/js/modules/k6/execution/execution.go +++ b/js/modules/k6/execution/execution.go @@ -216,11 +216,34 @@ func (mi *ModuleInstance) newVUInfo() (*goja.Object, error) { if err != nil { return o, err } - - err = o.Set("tags", rt.NewDynamicObject(&tagsDynamicObject{ + tagsDynamicObject := rt.NewDynamicObject(&tagsDynamicObject{ runtime: rt, state: vuState, - })) + }) + err = o.Set("tags", tagsDynamicObject) + + if err != nil { + return o, err + } + metrics, err := newInfoObj(rt, map[string]func() interface{}{ + "tags": func() interface{} { return tagsDynamicObject }, + "metadata": func() interface{} { + return rt.NewDynamicObject(&metadataDynamicObject{ + runtime: rt, + state: vuState, + }) + }, + }) + if err != nil { + return o, err + } + + err = o.Set("metrics", metrics) + + if err != nil { + return o, err + } + return o, err } @@ -311,9 +334,6 @@ func (o *tagsDynamicObject) Get(key string) goja.Value { if tag, ok := tcv.Tags.Get(key); ok { return o.runtime.ToValue(tag) } - if metadatum, ok := tcv.Metadata[key]; ok { - return o.runtime.ToValue(metadatum) - } return nil } @@ -323,7 +343,7 @@ func (o *tagsDynamicObject) Get(key string) goja.Value { func (o *tagsDynamicObject) Set(key string, val goja.Value) bool { var err error o.state.Tags.Modify(func(tagsAndMeta *metrics.TagsAndMeta) { - err = common.ApplyCustomUserTag(o.runtime, tagsAndMeta, key, val) + err = common.ApplyCustomUserTag(tagsAndMeta, key, val) if err != nil { panic(o.runtime.NewTypeError(err.Error())) } @@ -337,9 +357,6 @@ func (o *tagsDynamicObject) Has(key string) bool { if _, ok := ctv.Tags.Get(key); ok { return true } - if _, ok := ctv.Metadata[key]; ok { - return true - } return false } @@ -362,6 +379,61 @@ func (o *tagsDynamicObject) Keys() []string { for k := range tagsMap { keys = append(keys, k) } + return keys +} + +type metadataDynamicObject struct { + runtime *goja.Runtime + state *lib.State +} + +// Get a property value for the key. May return nil if the property does not exist. +func (o *metadataDynamicObject) Get(key string) goja.Value { + tcv := o.state.Tags.GetCurrentValues() + if metadatum, ok := tcv.Metadata[key]; ok { + return o.runtime.ToValue(metadatum) + } + return nil +} + +// Set a property value for the key. It returns true if succeed. String, Boolean +// and Number types are implicitly converted to the goja's relative string +// representation. An exception is raised in case a denied type is provided. +func (o *metadataDynamicObject) Set(key string, val goja.Value) bool { + var err error + o.state.Tags.Modify(func(tagsAndMeta *metrics.TagsAndMeta) { + err = common.ApplyCustomUserMetadata(tagsAndMeta, key, val) + if err != nil { + panic(o.runtime.NewTypeError(err.Error())) + } + }) + return true +} + +// Has returns true if the property exists. +func (o *metadataDynamicObject) Has(key string) bool { + ctv := o.state.Tags.GetCurrentValues() + if _, ok := ctv.Metadata[key]; ok { + return true + } + return false +} + +// Delete deletes the property for the key. It returns true on success (note, +// that includes missing property). +func (o *metadataDynamicObject) Delete(key string) bool { + o.state.Tags.Modify(func(tagsAndMeta *metrics.TagsAndMeta) { + tagsAndMeta.DeleteMetadata(key) + }) + return true +} + +// Keys returns a slice with all existing property keys. The order is not +// deterministic. +func (o *metadataDynamicObject) Keys() []string { + ctv := o.state.Tags.GetCurrentValues() + + keys := make([]string, 0, len(ctv.Metadata)) for k := range ctv.Metadata { keys = append(keys, k) } diff --git a/js/modules/k6/execution/execution_test.go b/js/modules/k6/execution/execution_test.go index 9ac9c855868..7a79d6ed5e7 100644 --- a/js/modules/k6/execution/execution_test.go +++ b/js/modules/k6/execution/execution_test.go @@ -25,6 +25,13 @@ import ( "gopkg.in/guregu/null.v3" ) +//nolint:gochecknoglobals +var tagsAndMetricsPropertyNames = map[string]string{ + "tags": "tag", + "metrics.tags": "tag", + "metrics.metadata": "metadata", +} + func setupTagsExecEnv(t *testing.T) *modulestest.Runtime { testRuntime := modulestest.NewRuntime(t) m, ok := New().NewModuleInstance(testRuntime.VU).(*ModuleInstance) @@ -34,43 +41,61 @@ func setupTagsExecEnv(t *testing.T) *modulestest.Runtime { return testRuntime } -func TestVUTagsGet(t *testing.T) { +func TestVUTagMetadatasGet(t *testing.T) { t.Parallel() - tenv := setupTagsExecEnv(t) - tenv.MoveToVUContext(&lib.State{ - Tags: lib.NewVUStateTags(metrics.NewRegistry().RootTagSet().With("vu", "42")), - }) - tag, err := tenv.VU.Runtime().RunString(`exec.vu.tags["vu"]`) - require.NoError(t, err) - assert.Equal(t, "42", tag.String()) - - // not found - tag, err = tenv.VU.Runtime().RunString(`exec.vu.tags["not-existing-tag"]`) - require.NoError(t, err) - assert.Equal(t, "undefined", tag.String()) + for prop, propType := range tagsAndMetricsPropertyNames { + prop, propType := prop, propType + t.Run(prop, func(t *testing.T) { + t.Parallel() + tenv := setupTagsExecEnv(t) + tenv.MoveToVUContext(&lib.State{ + Tags: lib.NewVUStateTags(metrics.NewRegistry().RootTagSet().With("tag-vu", "mytag")), + }) + tenv.VU.StateField.Tags.Modify(func(tagsAndMeta *metrics.TagsAndMeta) { + tagsAndMeta.SetMetadata("metadata-vu", "mymetadata") + }) + tag, err := tenv.VU.Runtime().RunString(fmt.Sprintf(`exec.vu.%s["%s-vu"]`, prop, propType)) + require.NoError(t, err) + assert.Equal(t, "my"+propType, tag.String()) + + // not found + tag, err = tenv.VU.Runtime().RunString(fmt.Sprintf(`exec.vu.%s["not-existing-%s"]`, prop, propType)) + require.NoError(t, err) + assert.Equal(t, "undefined", tag.String()) + }) + } } -func TestVUTagsJSONEncoding(t *testing.T) { +func TestVUTagMetadasJSONEncoding(t *testing.T) { t.Parallel() - tenv := setupTagsExecEnv(t) - tenv.MoveToVUContext(&lib.State{ - Options: lib.Options{ - SystemTags: metrics.NewSystemTagSet(metrics.TagVU), - }, - Tags: lib.NewVUStateTags(metrics.NewRegistry().RootTagSet().With("vu", "42")), - }) - tenv.VU.State().Tags.Modify(func(tagsAndMeta *metrics.TagsAndMeta) { - tagsAndMeta.SetTag("custom-tag", "mytag1") - }) - - encoded, err := tenv.VU.Runtime().RunString(`JSON.stringify(exec.vu.tags)`) - require.NoError(t, err) - assert.JSONEq(t, `{"vu":"42","custom-tag":"mytag1"}`, encoded.String()) + for prop, propType := range tagsAndMetricsPropertyNames { + prop, propType := prop, propType + t.Run(prop, func(t *testing.T) { + t.Parallel() + tenv := setupTagsExecEnv(t) + tenv.MoveToVUContext(&lib.State{ + Options: lib.Options{ + SystemTags: metrics.NewSystemTagSet(metrics.TagVU), + }, + Tags: lib.NewVUStateTags(metrics.NewRegistry().RootTagSet()), + }) + tenv.VU.State().Tags.Modify(func(tagsAndMeta *metrics.TagsAndMeta) { + tagsAndMeta.SetTag("tag-vu", "42") + tagsAndMeta.SetMetadata("metadata-vu", "42") + tagsAndMeta.SetTag("custom-tag", "mytag1") + tagsAndMeta.SetMetadata("custom-metadata", "mymetadata1") + }) + + encoded, err := tenv.VU.Runtime().RunString(fmt.Sprintf(`JSON.stringify(exec.vu.%s)`, prop)) + require.NoError(t, err) + assert.JSONEq(t, fmt.Sprintf(`{"%[1]s-vu":"42","custom-%[1]s":"my%[1]s1"}`, propType), encoded.String()) + }) + } } -func TestVUTagsSetSuccessAccetedTypes(t *testing.T) { +func TestVUTagMetadatasSetSuccessAccetedTypes(t *testing.T) { t.Parallel() // bool and numbers are implicitly converted into string @@ -84,24 +109,29 @@ func TestVUTagsSetSuccessAccetedTypes(t *testing.T) { "int": {v: 101, exp: "101"}, "float": {v: 3.14, exp: "3.14"}, } - - tenv := setupTagsExecEnv(t) - tenv.MoveToVUContext(&lib.State{ - Tags: lib.NewVUStateTags(metrics.NewRegistry().RootTagSet().With("vu", "42")), - }) - - for _, tc := range tests { - _, err := tenv.VU.Runtime().RunString(fmt.Sprintf(`exec.vu.tags["mytag"] = %v`, tc.v)) - require.NoError(t, err) - - val, err := tenv.VU.Runtime().RunString(`exec.vu.tags["mytag"]`) - require.NoError(t, err) - - assert.Equal(t, tc.exp, val.String()) + for prop := range tagsAndMetricsPropertyNames { + prop := prop + t.Run(prop, func(t *testing.T) { + t.Parallel() + tenv := setupTagsExecEnv(t) + tenv.MoveToVUContext(&lib.State{ + Tags: lib.NewVUStateTags(metrics.NewRegistry().RootTagSet().With("vu", "42")), + }) + + for _, tc := range tests { + _, err := tenv.VU.Runtime().RunString(fmt.Sprintf(`exec.vu.%s["mytag"] = %v`, prop, tc.v)) + require.NoError(t, err) + + val, err := tenv.VU.Runtime().RunString(fmt.Sprintf(`exec.vu.%s["mytag"]`, prop)) + require.NoError(t, err) + + assert.Equal(t, tc.exp, val.String()) + } + }) } } -func TestVUTagsSuccessOverwriteSystemTag(t *testing.T) { +func TestVUTagMetadatasSuccessOverwriteSystemTag(t *testing.T) { t.Parallel() tenv := setupTagsExecEnv(t) @@ -116,14 +146,9 @@ func TestVUTagsSuccessOverwriteSystemTag(t *testing.T) { assert.Equal(t, "vu101", val.String()) } -func TestVUTagsErrorOutOnInvalidValues(t *testing.T) { +func TestVUTagsMetadataErrorOutOnInvalidValues(t *testing.T) { t.Parallel() - logHook := testutils.NewLogHook(logrus.WarnLevel) - testLog := logrus.New() - testLog.AddHook(logHook) - testLog.SetOutput(io.Discard) - cases := []string{ "null", "undefined", @@ -133,19 +158,27 @@ func TestVUTagsErrorOutOnInvalidValues(t *testing.T) { `{f1: "value1", f2: 4}`, `{"foo": "bar"}`, } - tenv := setupTagsExecEnv(t) - tenv.MoveToVUContext(&lib.State{ - Options: lib.Options{ - SystemTags: metrics.NewSystemTagSet(metrics.TagVU), - }, - Tags: lib.NewVUStateTags(metrics.NewRegistry().RootTagSet().With("vu", "42")), - Logger: testLog, - }) - for _, val := range cases { - _, err := tenv.VU.Runtime().RunString(`exec.vu.tags["custom-tag"] = ` + val) - require.Error(t, err) - - assert.Contains(t, err.Error(), "TypeError: invalid value for metric tag 'custom-tag'") + for prop, propType := range tagsAndMetricsPropertyNames { + prop, propType := prop, propType + t.Run(prop, func(t *testing.T) { + t.Parallel() + for _, val := range cases { + logHook := testutils.NewLogHook(logrus.WarnLevel) + testLog := logrus.New() + testLog.AddHook(logHook) + testLog.SetOutput(io.Discard) + tenv := setupTagsExecEnv(t) + tenv.MoveToVUContext(&lib.State{ + Options: lib.Options{ + SystemTags: metrics.NewSystemTagSet(metrics.TagVU), + }, + Tags: lib.NewVUStateTags(metrics.NewRegistry().RootTagSet().With("vu", "42")), + Logger: testLog, + }) + _, err := tenv.VU.Runtime().RunString(fmt.Sprintf(`exec.vu.%s["custom"] = %s`, prop, val)) + assert.ErrorContains(t, err, "TypeError: invalid value for metric "+propType+" 'custom'") + } + }) } }