Skip to content

Commit

Permalink
k6/execution: Add metadata JS API
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mstoykov committed Apr 26, 2023
1 parent ec094ce commit ce43339
Show file tree
Hide file tree
Showing 3 changed files with 208 additions and 76 deletions.
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 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,
)
}
}
92 changes: 82 additions & 10 deletions js/modules/k6/execution/execution.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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()))
}
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,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)
}
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 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
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 TestVUTagMetadatasSuccessOverwriteSystemTag(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

0 comments on commit ce43339

Please sign in to comment.