Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

k6/execution: Add metadata JS API #3037

Merged
merged 4 commits into from
May 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 30 additions & 3 deletions js/common/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
Expand All @@ -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()
Expand All @@ -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 their 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 value",
key,
)
}
}
94 changes: 82 additions & 12 deletions js/modules/k6/execution/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,36 @@ 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,
}))
})

// This is kept for backwards compatibility reasons, but should be deprecated,
// since tags are also accessible via vu.metrics.tags.
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
}

Expand Down Expand Up @@ -311,20 +336,15 @@ 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
}

// 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 *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)
if err != nil {
if err := common.ApplyCustomUserTag(tagsAndMeta, key, val); err != nil {
panic(o.runtime.NewTypeError(err.Error()))
}
})
Expand All @@ -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
}

Expand All @@ -362,6 +379,59 @@ 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 successful. 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 {
o.state.Tags.Modify(func(tagsAndMeta *metrics.TagsAndMeta) {
if err := common.ApplyCustomUserMetadata(tagsAndMeta, key, val); 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)
}
Expand Down
159 changes: 96 additions & 63 deletions js/modules/k6/execution/execution_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -34,43 +41,61 @@ func setupTagsExecEnv(t *testing.T) *modulestest.Runtime {
return testRuntime
}

func TestVUTagsGet(t *testing.T) {
func TestVUTagsMetadatasGet(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 TestVUTagsMetadatasJSONEncoding(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 TestVUTagMetadatasSetSuccessAcceptedTypes(t *testing.T) {
t.Parallel()

// bool and numbers are implicitly converted into string
Expand All @@ -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 TestVUTagsMetadatasSuccessOverwriteSystemTag(t *testing.T) {
t.Parallel()

tenv := setupTagsExecEnv(t)
Expand All @@ -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",
Expand All @@ -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'")
}
})
}
}

Expand Down