From a9e74db711c918c8ff5c723536e461e1acb47067 Mon Sep 17 00:00:00 2001 From: Mihail Stoykov Date: Wed, 23 Jun 2021 19:02:48 +0300 Subject: [PATCH] Basic showcase of variant 1 for #1802 This will need more work if we want to help users more and the returned object will likely need some help as well(which possibly can be done by the importing code). --- js/bundle.go | 4 ++ js/initcontext.go | 3 + js/modules/k6/metrics/metrics.go | 90 ++++++++++++++++++++------- js/modules/k6/metrics/metrics_test.go | 7 ++- 4 files changed, 79 insertions(+), 25 deletions(-) diff --git a/js/bundle.go b/js/bundle.go index a1d10ab160b..33dd8eb4af7 100644 --- a/js/bundle.go +++ b/js/bundle.go @@ -287,6 +287,10 @@ func (b *Bundle) instantiate(logger logrus.FieldLogger, rt *goja.Runtime, init * rt.SetParserOptions(parser.WithDisableSourceMaps) rt.SetFieldNameMapper(common.FieldNameMapper{}) rt.SetRandSource(common.NewRandSource()) + // have a way to set get the current value of context + rt.GlobalObject().DefineAccessorProperty("context", rt.ToValue(func() context.Context { + return *init.ctxPtr + }), nil, goja.FLAG_FALSE, goja.FLAG_FALSE) exports := rt.NewObject() rt.Set("exports", exports) diff --git a/js/initcontext.go b/js/initcontext.go index a24fb64616a..7dda09e7a25 100644 --- a/js/initcontext.go +++ b/js/initcontext.go @@ -147,6 +147,9 @@ func (i *InitContext) requireModule(name string) (goja.Value, error) { if perInstance, ok := mod.(modules.HasModuleInstancePerVU); ok { mod = perInstance.NewModuleInstancePerVU() } + if name == "k6/metrics" { // hacks to not wrap a particular module + return i.runtime.ToValue(mod), nil + } return i.runtime.ToValue(common.Bind(i.runtime, mod, i.ctxPtr)), nil } diff --git a/js/modules/k6/metrics/metrics.go b/js/modules/k6/metrics/metrics.go index a0693a5bfab..b06590f1d40 100644 --- a/js/modules/k6/metrics/metrics.go +++ b/js/modules/k6/metrics/metrics.go @@ -49,36 +49,50 @@ type Metric struct { // ErrMetricsAddInInitContext is error returned when adding to metric is done in the init context var ErrMetricsAddInInitContext = common.NewInitContextError("Adding to metrics in the init context is not supported") -func newMetric(ctxPtr *context.Context, name string, t stats.MetricType, isTime []bool) (interface{}, error) { - if lib.GetState(*ctxPtr) != nil { +func newMetric(call goja.ConstructorCall, rt *goja.Runtime, t stats.MetricType) (*goja.Object, error) { + // TODO this can probably be done by a `common.GetContext(rt)` + ctx := rt.Get("context").Export().(context.Context) //nolint:forcetypeassert + if lib.GetState(ctx) != nil { return nil, errors.New("metrics must be declared in the init context") } + // TODO this kind of conversions can possibly be automated by the parts of common.Bind that are curently automating + // it and some wrapping + name := call.Argument(0).String() + isTime := call.Argument(1).ToBoolean() // TODO: move verification outside the JS if !checkName(name) { return nil, common.NewInitContextError(fmt.Sprintf("Invalid metric name: '%s'", name)) } valueType := stats.Default - if len(isTime) > 0 && isTime[0] { + if isTime { valueType = stats.Time } - rt := common.GetRuntime(*ctxPtr) - return common.Bind(rt, Metric{stats.New(name, t, valueType)}, ctxPtr), nil + return rt.ToValue(Metric{stats.New(name, t, valueType)}).ToObject(rt), nil } -func (m Metric) Add(ctx context.Context, v goja.Value, addTags ...map[string]string) (bool, error) { +func (m Metric) Add(call goja.FunctionCall, rt *goja.Runtime) goja.Value { + ctx := rt.Get("context").Export().(context.Context) //nolint:forcetypeassert state := lib.GetState(ctx) if state == nil { - return false, ErrMetricsAddInInitContext + common.Throw(rt, ErrMetricsAddInInitContext) + } + v := call.Argument(0) + var addTags map[string]string + if len(call.Arguments) > 1 { + // Technically the previous implementation support multiple maps but I don't think that is a good design and can + // be reproduced if desired + err := rt.ExportTo(call.Argument(1), &addTags) + if err != nil { + common.Throw(rt, err) + } } tags := state.CloneTags() - for _, ts := range addTags { - for k, v := range ts { - tags[k] = v - } + for k, v := range addTags { + tags[k] = v } vfloat := v.ToFloat() @@ -88,7 +102,7 @@ func (m Metric) Add(ctx context.Context, v goja.Value, addTags ...map[string]str sample := stats.Sample{Time: time.Now(), Metric: m.metric, Value: vfloat, Tags: stats.IntoSampleTags(&tags)} stats.PushIfNotDone(ctx, state.Samples, sample) - return true, nil + return rt.ToValue(true) } // GetName returns the metric name @@ -96,24 +110,54 @@ func (m Metric) GetName() string { return m.metric.Name } -type Metrics struct{} +func New() map[string]interface{} { + // This can definitely be automated more + // One thing that we can add is to differentiate between + // import something from "somewhere"; // where something is the *default* exports + // import * as something from "somewhere"; /// where something is an "object" with all the defined exports + // This likely will need a change once import/export syntax is part of goja as well :( + return map[string]interface{}{ + "Counter": Counter, + "Gauge": Gauge, + "Trend": Trend, + "Rate": Rate, + "returnMetricType": ReturnMetricType, + } +} -func New() *Metrics { - return &Metrics{} +// This is not possible after common.Bind as it wraps the object and doesn't return the original one. +func ReturnMetricType(m Metric) string { + return m.metric.Type.String() } -func (*Metrics) XCounter(ctx *context.Context, name string, isTime ...bool) (interface{}, error) { - return newMetric(ctx, name, stats.Counter, isTime) +func Counter(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object { + v, err := newMetric(call, rt, stats.Counter) + if err != nil { + common.Throw(rt, err) + } + return v } -func (*Metrics) XGauge(ctx *context.Context, name string, isTime ...bool) (interface{}, error) { - return newMetric(ctx, name, stats.Gauge, isTime) +func Gauge(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object { + v, err := newMetric(call, rt, stats.Gauge) + if err != nil { + common.Throw(rt, err) + } + return v } -func (*Metrics) XTrend(ctx *context.Context, name string, isTime ...bool) (interface{}, error) { - return newMetric(ctx, name, stats.Trend, isTime) +func Trend(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object { + v, err := newMetric(call, rt, stats.Trend) + if err != nil { + common.Throw(rt, err) + } + return v } -func (*Metrics) XRate(ctx *context.Context, name string, isTime ...bool) (interface{}, error) { - return newMetric(ctx, name, stats.Rate, isTime) +func Rate(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object { + v, err := newMetric(call, rt, stats.Rate) + if err != nil { + common.Throw(rt, err) + } + return v } diff --git a/js/modules/k6/metrics/metrics_test.go b/js/modules/k6/metrics/metrics_test.go index 50bd49adb66..5f972f685d7 100644 --- a/js/modules/k6/metrics/metrics_test.go +++ b/js/modules/k6/metrics/metrics_test.go @@ -64,7 +64,10 @@ func TestMetrics(t *testing.T) { ctxPtr := new(context.Context) *ctxPtr = common.WithRuntime(context.Background(), rt) - rt.Set("metrics", common.Bind(rt, New(), ctxPtr)) + rt.GlobalObject().DefineAccessorProperty("context", rt.ToValue(func() context.Context { + return *ctxPtr + }), nil, goja.FLAG_FALSE, goja.FLAG_FALSE) + rt.Set("metrics", New()) root, _ := lib.NewGroup("", nil) child, _ := root.Group("child") @@ -86,7 +89,7 @@ func TestMetrics(t *testing.T) { t.Run("ExitInit", func(t *testing.T) { *ctxPtr = lib.WithState(*ctxPtr, state) _, err := rt.RunString(fmt.Sprintf(`new metrics.%s("my_metric")`, fn)) - assert.EqualError(t, err, "metrics must be declared in the init context at apply (native)") + assert.Contains(t, err.Error(), "metrics must be declared in the init context") }) groups := map[string]*lib.Group{