diff --git a/.github/renovate.json5 b/.github/renovate.json5 index da98142a8..e0fd5935f 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -19,13 +19,17 @@ ], "customManagers": [ { - // Update k6 version in Dockerfiles. + // Update xk6-sm "customType": "regex", + "depNameTemplate": "grafana/xk6-sm", "datasourceTemplate": "github-releases", - "depNameTemplate": "grafana/k6", - "fileMatch": [".*\\.mk"], + "versioningTemplate": "semver", + "fileMatch": [ + "Dockerfile", + "scripts/make/sm-k6.mk" + ], "matchStrings": [ - "(?:^|\\n)[ \\t]*K6_VERSION\\s*:=\\s*(?\\S+)[ \\t]*(?:$|\\n)" + "https://github.com/grafana/xk6-sm/releases/download/(?[^/]+)/" ] } ] diff --git a/.github/workflows/build_and_publish.yaml b/.github/workflows/build_and_publish.yaml index c06d89a1c..12cf6a8ec 100644 --- a/.github/workflows/build_and_publish.yaml +++ b/.github/workflows/build_and_publish.yaml @@ -77,6 +77,9 @@ jobs: - name: build run: make build-native + - name: Download sm-k6 + run: make sm-k6-native + - name: lint run: make lint diff --git a/.github/workflows/validate_pr.yaml b/.github/workflows/validate_pr.yaml index 6f1f1f82c..d3eace029 100644 --- a/.github/workflows/validate_pr.yaml +++ b/.github/workflows/validate_pr.yaml @@ -54,6 +54,9 @@ jobs: - name: build run: make build-native + - name: Download sm-k6 + run: make sm-k6-native + - name: lint run: make lint diff --git a/Dockerfile b/Dockerfile index 20abc5c12..9f50c5a3f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -10,8 +10,8 @@ ARG TARGETOS ARG TARGETARCH ARG HOST_DIST=$TARGETOS-$TARGETARCH +ADD --chmod=0555 https://github.com/grafana/xk6-sm/releases/download/v0.0.3-pre/sm-k6-${TARGETOS}-${TARGETARCH} /usr/local/bin/sm-k6 COPY dist/${HOST_DIST}/synthetic-monitoring-agent /usr/local/bin/synthetic-monitoring-agent -COPY dist/${HOST_DIST}/k6 /usr/local/bin/sm-k6 COPY scripts/pre-stop.sh /usr/local/lib/synthetic-monitoring-agent/pre-stop.sh COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt diff --git a/Dockerfile.build b/Dockerfile.build index 20abc5c12..9f50c5a3f 100644 --- a/Dockerfile.build +++ b/Dockerfile.build @@ -10,8 +10,8 @@ ARG TARGETOS ARG TARGETARCH ARG HOST_DIST=$TARGETOS-$TARGETARCH +ADD --chmod=0555 https://github.com/grafana/xk6-sm/releases/download/v0.0.3-pre/sm-k6-${TARGETOS}-${TARGETARCH} /usr/local/bin/sm-k6 COPY dist/${HOST_DIST}/synthetic-monitoring-agent /usr/local/bin/synthetic-monitoring-agent -COPY dist/${HOST_DIST}/k6 /usr/local/bin/sm-k6 COPY scripts/pre-stop.sh /usr/local/lib/synthetic-monitoring-agent/pre-stop.sh COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt diff --git a/Makefile b/Makefile index 531c18555..e86b0e657 100644 --- a/Makefile +++ b/Makefile @@ -23,5 +23,5 @@ include $(ROOTDIR)/scripts/make/build.mk include $(ROOTDIR)/scripts/make/testing.mk include $(ROOTDIR)/scripts/make/linters.mk include $(ROOTDIR)/scripts/make/release.mk +include $(ROOTDIR)/scripts/make/sm-k6.mk include $(ROOTDIR)/scripts/make/helpers.mk -include $(ROOTDIR)/scripts/make/xk6.mk diff --git a/internal/prober/multihttp/script_test.go b/internal/prober/multihttp/script_test.go index d8fa7a9ce..2ac2e0250 100644 --- a/internal/prober/multihttp/script_test.go +++ b/internal/prober/multihttp/script_test.go @@ -6,6 +6,7 @@ import ( "net/http" "net/http/httptest" "path/filepath" + "runtime" "strings" "testing" @@ -852,7 +853,8 @@ func TestSettingsToScript(t *testing.T) { t.Cleanup(cancel) // logger := zerolog.New(zerolog.NewTestWriter(t)) logger := zerolog.Nop() - k6path := filepath.Join(testhelper.ModuleDir(t), "dist", "k6") + k6path := filepath.Join(testhelper.ModuleDir(t), "dist", runtime.GOOS+"-"+runtime.GOARCH, "sm-k6") + t.Log(k6path) runner := k6runner.New(k6runner.RunnerOpts{Uri: k6path}) prober, err := NewProber(ctx, check, logger, runner, http.Header{}) diff --git a/scripts/make/sm-k6.mk b/scripts/make/sm-k6.mk new file mode 100644 index 000000000..0136cc598 --- /dev/null +++ b/scripts/make/sm-k6.mk @@ -0,0 +1,21 @@ +XK6_PLATFORMS := $(filter-out linux/arm,$(PLATFORMS)) darwin/arm64 darwin/amd64 + +.PHONY: sm-k6 +sm-k6: + @true + +define sm-k6 +$(DISTDIR)/$(1)-$(2)/sm-k6: + mkdir -p "$(DISTDIR)/$(1)-$(2)" + # Renovate updates the following line. Keep its syntax as it is. + curl -sSL https://github.com/grafana/xk6-sm/releases/download/v0.0.3/sm-k6-$(1)-$(2) -o "$$@" + chmod +x "$$@" + +sm-k6: $(DISTDIR)/$(1)-$(2)/sm-k6 +endef + +$(foreach BUILD_PLATFORM,$(XK6_PLATFORMS), \ + $(eval $(call sm-k6,$(word 1,$(subst /, ,$(BUILD_PLATFORM))),$(word 2,$(subst /, ,$(BUILD_PLATFORM)))))) + +.PHONY: sm-k6-native +sm-k6-native: $(DISTDIR)/$(HOST_OS)-$(HOST_ARCH)/sm-k6 diff --git a/scripts/make/vars.mk b/scripts/make/vars.mk index 54698706c..dc3e1423e 100644 --- a/scripts/make/vars.mk +++ b/scripts/make/vars.mk @@ -23,5 +23,4 @@ ifeq ($(USE_LOCAL_TOOLS),true) GOLANGCI_LINT := golangci-lint GOTESTSUM := gotestsum SHELLCHECK := shellcheck -XK6 := xk6 endif diff --git a/scripts/make/xk6.mk b/scripts/make/xk6.mk deleted file mode 100644 index 8cfa871f3..000000000 --- a/scripts/make/xk6.mk +++ /dev/null @@ -1,88 +0,0 @@ -XK6_PKG_DIR := $(ROOTDIR)/xk6/sm -XK6_OUT_DIR := $(DISTDIR)/$(HOST_OS)-$(HOST_ARCH) -K6_BIN := $(XK6_OUT_DIR)/k6 -K6_VERSION := v0.54.0 - -LOCAL_GOPATH ?= $(shell go env GOPATH) - -ifeq ($(CI),true) -XK6 ?= xk6 -endif - -ifeq ($(origin XK6),undefined) -XK6 ?= $(ROOTDIR)/scripts/docker-run xk6 -endif - -define build_xk6_template -BUILD_XK6_TARGETS += build-xk6-$(1)-$(2) - -build-xk6-$(1)-$(2) : GOOS := $(1) -build-xk6-$(1)-$(2) : GOARCH := $(2) -build-xk6-$(1)-$(2) : DIST_FILENAME := $(dir $(firstword $(OUTPUT_FILE) $(DISTDIR)/$(1)-$(2)/))k6 -$(DISTDIR)/$(1)-$(2)/k6) : $(wildcard $(ROOTDIR)/xk6/sm/*.go $(ROOTDIR)/xk6/sm/go.mod) - -endef - -define build_dummy_xk6_template -BUILD_DUMMY_XK6_TARGETS += build-dummy-xk6-$(1)-$(2) - -build-dummy-xk6-$(1)-$(2) : GOOS := $(1) -build-dummy-xk6-$(1)-$(2) : GOARCH := $(2) -build-dummy-xk6-$(1)-$(2) : DIST_FILENAME := $(dir $(firstword $(OUTPUT_FILE) $(DISTDIR)/$(1)-$(2)/))k6 -$(DISTDIR)/$(1)-$(2)/k6) : - -endef - -# TODO(mem): xk6 does not build on linux/arm yet -DUMMY_XK6_PLATFORMS := $(filter linux/arm,$(PLATFORMS)) - -XK6_PLATFORMS := $(filter-out $(DUMMY_XK6_PLATFORMS),$(PLATFORMS)) - -$(foreach BUILD_PLATFORM,$(XK6_PLATFORMS), \ - $(eval $(call build_xk6_template,$(word 1,$(subst /, ,$(BUILD_PLATFORM))),$(word 2,$(subst /, ,$(BUILD_PLATFORM)))))) - -$(foreach BUILD_PLATFORM,$(DUMMY_XK6_PLATFORMS), \ - $(eval $(call build_dummy_xk6_template,$(word 1,$(subst /, ,$(BUILD_PLATFORM))),$(word 2,$(subst /, ,$(BUILD_PLATFORM)))))) - -BUILD_XK6_NATIVE_TARGETS := $(filter build-xk6-$(HOST_OS)-$(HOST_ARCH), $(BUILD_XK6_TARGETS)) - -define build_xk6_command - $(S) echo 'Building $(notdir $(DIST_FILENAME)) ($(GOOS)-$(GOARCH))' - $(S) mkdir -p $(DISTDIR)/$(GOOS)-$(GOARCH) - $(V) GOOS=$(GOOS) GOARCH=$(GOARCH) $(XK6) \ - build $(K6_VERSION) \ - --with xk6-sm='$(XK6_PKG_DIR)' \ - --output '$(DIST_FILENAME)' - $(V) test '$(GOOS)' = '$(HOST_OS)' -a '$(GOARCH)' = '$(HOST_ARCH)' && \ - cp -a '$(DIST_FILENAME)' '$(DISTDIR)/$(notdir $(DIST_FILENAME))' || \ - true -endef - -ifneq ($(strip $(BUILD_XK6_TARGETS)),) -.PHONY: $(BUILD_XK6_TARGETS) -$(BUILD_XK6_TARGETS) : build-xk6-% : - $(call build_xk6_command) - -build: $(BUILD_XK6_TARGETS) -endif - -define build_dummy_xk6_command - $(S) echo 'Building $(notdir $(DIST_FILENAME)) ($(GOOS)-$(GOARCH))' - $(S) mkdir -p $(DISTDIR)/$(GOOS)-$(GOARCH) - $(V) install -m 0755 $(ROOTDIR)/scripts/dummy-k6.sh '$(DIST_FILENAME)' - $(V) test '$(GOOS)' = '$(HOST_OS)' -a '$(GOARCH)' = '$(HOST_ARCH)' && \ - cp -a '$(DIST_FILENAME)' '$(DISTDIR)/$(notdir $(DIST_FILENAME))' || \ - true -endef - -ifneq ($(strip $(BUILD_DUMMY_XK6_TARGETS)),) -.PHONY: $(BUILD_DUMMY_XK6_TARGETS) -$(BUILD_DUMMY_XK6_TARGETS) : build-dummy-xk6-% : - $(call build_dummy_xk6_command) - -build: $(BUILD_DUMMY_XK6_TARGETS) -endif - -.PHONY: native-k6 -native-k6: build-xk6-$(HOST_OS)-$(HOST_ARCH) -build-native: native-k6 diff --git a/scripts/package/nfpm.yaml.template b/scripts/package/nfpm.yaml.template index 17dbba2fd..bde5f4dd0 100644 --- a/scripts/package/nfpm.yaml.template +++ b/scripts/package/nfpm.yaml.template @@ -21,7 +21,7 @@ contents: dst: /etc/systemd/system/synthetic-monitoring-agent.service # Copy k6 as sm-k6 to prevent clashing with k6 if it's installed. -- src: {{.DISTDIR}}/{{.Os}}-{{.Arch}}/k6 +- src: {{.DISTDIR}}/{{.Os}}-{{.Arch}}/sm-k6 dst: /usr/bin/sm-k6 rpm: deb: diff --git a/xk6/sm/output.go b/xk6/sm/output.go deleted file mode 100644 index 324da9e76..000000000 --- a/xk6/sm/output.go +++ /dev/null @@ -1,768 +0,0 @@ -package sm - -import ( - "errors" - "fmt" - "io" - "math" - "regexp" - "sort" - "strconv" - "strings" - "time" - - "github.com/sirupsen/logrus" - "go.k6.io/k6/metrics" - "go.k6.io/k6/output" -) - -const ( - ExtensionName = "sm" - RawURLTagName = "__raw_url__" -) - -func init() { - output.RegisterExtension(ExtensionName, New) -} - -// Output is a k6 output plugin that writes metrics to an io.Writer in -// Prometheus text exposition format. -type Output struct { - logger logrus.FieldLogger - buffer output.SampleBuffer - out io.WriteCloser - start time.Time -} - -// New creates a new instance of the output. -func New(p output.Params) (output.Output, error) { - fn := p.ConfigArgument - if len(fn) == 0 { - return nil, errors.New("output filename required") - } - - fh, err := p.FS.Create(fn) - if err != nil { - return nil, err - } - - return &Output{logger: p.Logger, out: fh}, nil -} - -// Description returns a human-readable description of the output that will be -// shown in `k6 run`. For extensions it probably should include the version as -// well. -func (o *Output) Description() string { - return "Synthetic Monitoring output" -} - -// Start is called before the Engine tries to use the output and should be -// used for any long initialization tasks, as well as for starting a -// goroutine to asynchronously flush metrics to the output. -func (o *Output) Start() error { - o.start = time.Now() - o.logger.WithFields(logrus.Fields{ - "output": o.Description(), - "ts": o.start.UnixMilli(), - }).Debug("starting output") - return nil -} - -// AddMetricSamples receives the latest metric samples from the Engine. -// -// This method is called synchronously, so do not do anything blocking here -// that might take a long time. Preferably, just use the SampleBuffer or -// something like it to buffer metrics until they are flushed. -func (o *Output) AddMetricSamples(samples []metrics.SampleContainer) { - o.buffer.AddMetricSamples(samples) -} - -// Stop flushes all remaining metrics and finalize the test run. -func (o *Output) Stop() error { - duration := time.Since(o.start) - o.logger.WithFields(logrus.Fields{ - "output": o.Description(), - "duration": duration, - }).Debug("stopping output") - - defer o.out.Close() - - genericMetrics := newGenericMetricsCollection() - targetMetrics := newTargetMetricsCollection() - - genericMetrics.Update("script_duration_seconds", "Returns how long the script took to complete in seconds", "", "", duration.Seconds(), nil) - - for _, samples := range o.buffer.GetBufferedSamples() { - for _, sample := range samples.GetSamples() { - tags := getTags(sample) - - scenario := tags["scenario"] - group := tags["group"] - - if _, found := tags["name"]; found { - targetMetrics.Update(sample, scenario, group, tags) - continue - } - - // The samples that don't have "name" in their tags seem to be generic metrics about various - // things. - // - // Seen so far: - // - // * checks -- this seems to be the number of checks performed (the number of times the check - // function is called?) and the "check" tag might be different each time? But it seems to emit - // one instance of "check" each time the function is called, even if it's with the same - // "check" tag. - // * data_received -- this is the total for all requests in the scenario - // * data_sent -- this is the total for all requests in the scenario - // * iteration_duration -- the duration for the iteration in ms; with scenarios there seems to - // be one iteration per scenario. - // * iterations -- not interesting, how many iterations in each scenario - - metricName, value := deriveMetricNameAndValue(sample) - switch metricName { - case "": - continue - - case "checks_total": - // "checks" is a little weird. It seems to be the number of checks performed - // (the number of times the check function is called?) and the "check" tag might - // be different each time becuase it's the name of the check provided as an - // argument to the check function. One of these samples seems to be emitted each - // time the check function is called. - // - // The tag describing the check is called "check", so we end up with a metric - // "check" with a label "check". - // - // The problem with this is that the check name seems to be free-form, so we - // might end up with invalid label values. This is probably a job for Loki, - // meaning we need an structured way of storing this information in logs. - fields := logrus.Fields{ - "source": ExtensionName, - "metric": metricName, - "scenario": scenario, - "value": value, - } - if group != "" { - fields["group"] = group - } - for k, v := range tags { - fields[k] = v - } - entry := o.logger.WithFields(fields) - entry.Info("check result") - - delete(tags, "check") - - // Now we need to do something weird: because the _value_ of the check metric is 0 if - // the check fails. If that's the case, add a tag result="fail" and set the value to 1 - // (so that the metric is counting failures), otherwise add result="pass". - if value == 0 { - tags["result"] = "fail" - value = 1 - } else { - tags["result"] = "pass" - } - } - - genericMetrics.Update(metricName, "", scenario, group, value, tags) - } - } - - // It might be a good idea to remove the tags from each of the metrics and instead create an scenario_info - // metric. The problem with this is that 1) there might be no scenario (in which case it might be named - // default?); 2) it's technically possible to add tags to invidual requests via request options. - - genericMetrics.Write(o.out) - - targetMetrics.Write(o.out) - - return nil -} - -func getTags(sample metrics.Sample) map[string]string { - var tags map[string]string - if sample.Tags != nil { - tags = sample.Tags.Map() - } - - // The documentation at https://k6.io/docs/using-k6/tags-and-groups/ seems to suggest that - // "group" should not be empty (it shouldn't be there if there's a single group), but I keep - // seeing instances of an empty group name. - if group, found := tags["group"]; found && group == "" { - delete(tags, "group") - } - - return tags -} - -func deriveMetricNameAndValue(sample metrics.Sample) (string, float64) { - metricName := sample.TimeSeries.Metric.Name - value := sample.Value - - switch metricName { - case "iterations": - metricName = "" - value = 0 - - case "checks": - metricName = "checks_total" - - case "iteration_duration": - metricName = "iteration_duration_seconds" - value /= 1000 - - case "data_sent": - metricName = "data_sent_bytes" - - case "data_received": - metricName = "data_received_bytes" - } - - return metricName, value -} - -type targetId struct { - url string - method string - scenario string - group string - name string -} - -type targetMetrics struct { - requests int - failed int - expectedResponse bool - group string - scenario string - - // HTTP info - proto string - tlsVersion string - status []string - - // timings - duration []float64 - blocked []float64 - connecting []float64 - sending []float64 - waiting []float64 - receiving []float64 - tlsHandshaking []float64 - - tags map[string]string -} - -type targetMetricsCollection map[targetId]targetMetrics - -func newTargetMetricsCollection() targetMetricsCollection { - return make(targetMetricsCollection) -} - -func (collection targetMetricsCollection) Update(sample metrics.Sample, scenario, group string, tags map[string]string) { - key := targetId{ - url: getURL(tags), - method: tags["method"], - scenario: scenario, - group: group, - name: tags["name"], - } - - // the metrics for this target - tm := collection[key] - - tm.scenario = scenario - tm.group = group - - switch sample.TimeSeries.Metric.Name { - case "http_reqs": - tm.requests += int(sample.Value) - tm.proto = tags["proto"] - tm.tlsVersion = tags["tls_version"] - tm.status = append(tm.status, tags["status"]) - case "http_req_duration": - tm.duration = append(tm.duration, sample.Value/1000) // ms - case "http_req_blocked": - tm.blocked = append(tm.blocked, sample.Value/1000) // ms - case "http_req_connecting": - tm.connecting = append(tm.connecting, sample.Value/1000) // ms - case "http_req_tls_handshaking": - tm.tlsHandshaking = append(tm.tlsHandshaking, sample.Value/1000) // ms - case "http_req_sending": - tm.sending = append(tm.sending, sample.Value/1000) // ms - case "http_req_waiting": - tm.waiting = append(tm.waiting, sample.Value/1000) // ms - case "http_req_receiving": - tm.receiving = append(tm.receiving, sample.Value/1000) // ms - case "http_req_failed": - tm.failed += int(sample.Value) - } - - // Remove elements from tags because the following are stored in dedicated fields. - - delete(tags, "url") - delete(tags, RawURLTagName) - delete(tags, "method") - delete(tags, "scenario") - delete(tags, "group") - delete(tags, "name") - - delete(tags, "proto") - delete(tags, "tls_version") - delete(tags, "status") - - tm.tags = tags - - collection[key] = tm -} - -func (c targetMetricsCollection) Write(w io.Writer) { - for key, ti := range c { - out := newBufferedMetricTextOutput(w, "url", key.url, "method", key.method) - if key.scenario != "" { - out.Tags("scenario", key.scenario) - } - if key.group != "" { - out.Tags("group", key.group) - } - if key.name != "" { - out.Tags("name", key.name) - } - - // Remove expected_reponse from tags and write it as a separate - // metric. It reads weirdly as a label, specially one that is - // applied to all the metrics. - expectedResponse := ti.tags["expected_response"] - delete(ti.tags, "expected_response") - - out.Name("probe_http_got_expected_response") - if expectedResponse == "false" { - out.Value(0) - } else { - out.Value(1) - } - - // Remove error code from tags and write it as a separate - // metric because the possible values span ~ 700 values. - errorCode := ti.tags["error_code"] - delete(ti.tags, "error_code") - - out.Name("probe_http_error_code") - if errorCode == "" || errorCode == "0" { - out.Value(0) - } else if v, err := strconv.Atoi(errorCode); err != nil { - out.Value(-1) - } else { - out.Value(v) - } - - out.Name("probe_http_info") - if ti.tlsVersion != "" { - out.KeyValue(`tls_version`, strings.TrimPrefix(ti.tlsVersion, "tls")) - } - // If the request failed, proto might be empty because there - // was no response. - if len(ti.proto) > 0 { - out.KeyValue("proto", ti.proto) - } - - for k, v := range ti.tags { - out.KeyValue(k, v) - } - out.Value(1) - - out.Name("probe_http_requests_total") - out.Value(ti.requests) - - out.Name("probe_http_requests_failed_total") - out.Value(ti.failed) - - // TODO(mem): decide what to do with failed requests. - // - // If a request fails, depending on the reason, some of the - // timings might be missing. This means that we might skew the - // results towards 0 if we try to do over-time aggregations. - - out.Name(`probe_http_status_code`) - out.Value(ti.status[0]) - - if protoVersion := strings.TrimPrefix(strings.ToLower(ti.proto), "http/"); len(protoVersion) > 0 { - out.Name(`probe_http_version`) - out.Value(protoVersion) // XXX - } - - out.Name(`probe_http_ssl`) - if ti.tlsVersion == "" { - out.Value(0) - } else { - out.Value(1) - } - - if ti.requests == 1 { - out.Name("probe_http_duration_seconds") - out.KeyValue("phase", "resolve") - out.Value(0) - - out.Name("probe_http_duration_seconds") - out.KeyValue("phase", "connect") - out.Value(ti.connecting[0]) - - out.Name("probe_http_duration_seconds") - out.KeyValue("phase", "tls") - out.Value(ti.tlsHandshaking[0]) - - out.Name("probe_http_duration_seconds") - out.KeyValue("phase", "processing") - out.Value(ti.waiting[0]) - - out.Name("probe_http_duration_seconds") - out.KeyValue("phase", "transfer") - out.Value(ti.receiving[0]) - - out.Name("probe_http_total_duration_seconds") - out.Value(ti.duration[0]) - - // ti.sending: writing the request - // ti.blocked: waiting for the connection to be available - } else { - out.Name("probe_http_duration_seconds") - out.KeyValue("phase", "resolve") - out.Stats(make([]float64, ti.requests)) - - out.Name("probe_http_duration_seconds") - out.KeyValue("phase", "connect") - out.Stats(ti.connecting) - - out.Name("probe_http_duration_seconds") - out.KeyValue("phase", "tls") - out.Stats(ti.tlsHandshaking) - - out.Name("probe_http_duration_seconds") - out.KeyValue("phase", "processing") - out.Stats(ti.waiting) - - out.Name("probe_http_duration_seconds") - out.KeyValue("phase", "transfer") - out.Stats(ti.receiving) - - out.Name("probe_http_total_duration_seconds") - out.Stats(ti.duration) - } - } -} - -type genericMetric struct { - name string - value float64 - tags map[string]string - help string -} - -type genericMetricsCollection map[string]genericMetric - -func newGenericMetricsCollection() genericMetricsCollection { - return make(genericMetricsCollection) -} - -func (c genericMetricsCollection) Update(metric, help, scenario, group string, delta float64, tags map[string]string) { - var key strings.Builder - - key.WriteString(metric) - key.WriteString(scenario) - key.WriteString(group) - - keys := make([]string, 0, len(tags)) - for k := range tags { - keys = append(keys, k) - } - sort.Strings(keys) - for _, k := range keys { - key.WriteString(k) - key.WriteString(tags[k]) - } - - keyStr := key.String() - - m := c[keyStr] - m.name = metric - m.help = help - m.value += delta - if len(tags) > 0 { - m.tags = tags - } - c[keyStr] = m -} - -func (c genericMetricsCollection) Write(w io.Writer) { - for _, metric := range c { - out := newBufferedMetricTextOutput(w) - out.Name("probe_" + metric.name) - out.Help(metric.help) - for key, value := range metric.tags { - out.Tags(key, value) - } - // output stats instead? - out.Value(metric.value) - } -} - -type immediateMetricTextOutput struct { - dest io.Writer - commonKeysAndValues []string - count int -} - -func newMetricTextOutput(dest io.Writer, keysAndValues ...string) *immediateMetricTextOutput { - return &immediateMetricTextOutput{dest: dest, commonKeysAndValues: keysAndValues} -} - -func (o *immediateMetricTextOutput) Name(name string) { - fmt.Fprint(o.dest, name) - fmt.Fprint(o.dest, "{") - o.count = 0 -} - -func (o *immediateMetricTextOutput) KeyValue(key, value string) { - if o.count > 0 { - fmt.Fprint(o.dest, ",") - } - - if !isValidMetricName(key) { - key = sanitizeLabelName(key) - } - - fmt.Fprint(o.dest, key) - fmt.Fprint(o.dest, `="`) - fmt.Fprint(o.dest, value) - fmt.Fprint(o.dest, `"`) - - o.count++ -} - -func (o *immediateMetricTextOutput) Value(v any) { - for i := 0; i < len(o.commonKeysAndValues); i += 2 { - key := o.commonKeysAndValues[i] - if !isValidMetricName(key) { - key = sanitizeLabelName(key) - } - o.KeyValue(key, o.commonKeysAndValues[i+1]) - } - fmt.Fprint(o.dest, "} ") - fmt.Fprintln(o.dest, v) -} - -type bufferedMetricTextOutput struct { - dest io.Writer - commonKeysAndValues []string - name string - help string - buf strings.Builder -} - -func newBufferedMetricTextOutput(dest io.Writer, keysAndValues ...string) *bufferedMetricTextOutput { - return &bufferedMetricTextOutput{dest: dest, commonKeysAndValues: keysAndValues} -} - -func (o *bufferedMetricTextOutput) Name(name string) { - o.name = name - o.buf.Reset() -} - -func (o *bufferedMetricTextOutput) Help(str string) { - o.help = str -} - -func (o *bufferedMetricTextOutput) Tags(keysAndValues ...string) { - o.commonKeysAndValues = append(o.commonKeysAndValues, keysAndValues...) -} - -func (o *bufferedMetricTextOutput) KeyValue(key, value string) { - if o.buf.Len() > 0 { - o.buf.WriteRune(',') - } - - if !isValidMetricName(key) { - key = sanitizeLabelName(key) - } - - o.buf.WriteString(key) - o.buf.WriteRune('=') - o.buf.WriteRune('"') - o.buf.WriteString(value) - o.buf.WriteRune('"') -} - -func (o *bufferedMetricTextOutput) Value(v any) { - for i := 0; i < len(o.commonKeysAndValues); i += 2 { - if o.buf.Len() > 0 { - o.buf.WriteRune(',') - } - - key := o.commonKeysAndValues[i] - if !isValidMetricName(key) { - key = sanitizeLabelName(key) - } - - o.buf.WriteString(key) - o.buf.WriteRune('=') - o.buf.WriteRune('"') - o.buf.WriteString(o.commonKeysAndValues[i+1]) - o.buf.WriteRune('"') - } - - if o.help != "" { - fmt.Fprintf(o.dest, "# HELP %s %s\n", o.name, o.help) - } - - fmt.Fprint(o.dest, o.name) - fmt.Fprint(o.dest, "{") - fmt.Fprint(o.dest, o.buf.String()) - fmt.Fprint(o.dest, "} ") - fmt.Fprintln(o.dest, v) -} - -func (o *bufferedMetricTextOutput) Stats(v []float64) { - for i := 0; i < len(o.commonKeysAndValues); i += 2 { - if o.buf.Len() > 0 { - o.buf.WriteRune(',') - } - - key := o.commonKeysAndValues[i] - if !isValidMetricName(key) { - key = sanitizeLabelName(key) - } - - o.buf.WriteString(key) - o.buf.WriteRune('=') - o.buf.WriteRune('"') - o.buf.WriteString(o.commonKeysAndValues[i+1]) - o.buf.WriteRune('"') - } - - stats := getStats(v) - - fmt.Fprint(o.dest, o.name) - fmt.Fprint(o.dest, "_min") - fmt.Fprint(o.dest, "{") - fmt.Fprint(o.dest, o.buf.String()) - fmt.Fprint(o.dest, "} ") - fmt.Fprintln(o.dest, stats.min) - - fmt.Fprint(o.dest, o.name) - fmt.Fprint(o.dest, "_max") - fmt.Fprint(o.dest, "{") - fmt.Fprint(o.dest, o.buf.String()) - fmt.Fprint(o.dest, "} ") - fmt.Fprintln(o.dest, stats.max) - - fmt.Fprint(o.dest, o.name) - // fmt.Fprint(o.dest, "_mean") - fmt.Fprint(o.dest, "{") - fmt.Fprint(o.dest, o.buf.String()) - fmt.Fprint(o.dest, "} ") - fmt.Fprintln(o.dest, stats.med) - - fmt.Fprint(o.dest, o.name) - fmt.Fprint(o.dest, "_count") - fmt.Fprint(o.dest, "{") - fmt.Fprint(o.dest, o.buf.String()) - fmt.Fprint(o.dest, "} ") - fmt.Fprintln(o.dest, stats.n) - - fmt.Fprint(o.dest, o.name) - fmt.Fprint(o.dest, "_sum") - fmt.Fprint(o.dest, "{") - fmt.Fprint(o.dest, o.buf.String()) - fmt.Fprint(o.dest, "} ") - fmt.Fprintln(o.dest, stats.sum) -} - -type stats struct { - n int - min float64 - max float64 - med float64 - sum float64 -} - -func getStats(a []float64) stats { - sort.Float64s(a) - - out := stats{ - n: len(a), - min: a[0], - max: a[len(a)-1], - } - - for _, v := range a { - out.sum += v - } - - if out.n > 1 { - p, f := modf(float64(out.n-1) * 0.5) - out.med = lerp(a[p], a[p+1], f) - } else { - out.med = out.min - } - - return out -} - -// lerp returns the linear interpolation between a and b at t. -func lerp(a, b, t float64) float64 { - return (1-t)*a + t*b -} - -// modf returns the integer and fractional parts of n. -func modf(n float64) (int, float64) { - i, f := math.Modf(n) - return int(i), f -} - -var validMetricNameRe = regexp.MustCompile(`^[a-zA-Z_:][a-zA-Z0-9_:]*$`) - -// isValidMetricNameRe returns true iff s is a valid metric name. -func isValidMetricNameRe(s string) bool { - return validMetricNameRe.MatchString(s) -} - -// isValidMetricName returns true iff s is a valid metric name. -// -// This function is a faster hardcoded implementation wrt to the regular expression. -func isValidMetricName(s string) bool { - if len(s) == 0 { - return false - } - - for i, r := range s { - if !((r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_' || r == ':' || (r >= '0' && r <= '9' && i > 0)) { - return false - } - } - - return true -} - -// sanitizeLabelName replaces all invalid characters in s with '_'. -func sanitizeLabelName(s string) string { - var builder strings.Builder - - for i, r := range s { - if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || r == '_' || r == ':' || (r >= '0' && r <= '9' && i > 0) { - builder.WriteRune(r) - } else { - builder.WriteRune('_') - } - } - - return builder.String() -} - -func getURL(m map[string]string) string { - if u := m[RawURLTagName]; u != "" { - return u - } - - return m["url"] -} diff --git a/xk6/sm/output_test.go b/xk6/sm/output_test.go deleted file mode 100644 index 34a2fb739..000000000 --- a/xk6/sm/output_test.go +++ /dev/null @@ -1,615 +0,0 @@ -package sm - -import ( - "bytes" - "strings" - "testing" - - "github.com/spf13/afero" - "github.com/stretchr/testify/require" - "go.k6.io/k6/metrics" - "go.k6.io/k6/output" -) - -func TestOutputNew(t *testing.T) { - testcases := map[string]struct { - input output.Params - expectError bool - }{ - "happy path": { - input: output.Params{ConfigArgument: "test.out", FS: afero.NewMemMapFs()}, - expectError: false, - }, - "no filename": { - input: output.Params{ConfigArgument: "", FS: afero.NewMemMapFs()}, - expectError: true, - }, - "cannot create file": { - input: output.Params{ConfigArgument: "test.out", FS: afero.NewReadOnlyFs(afero.NewMemMapFs())}, - expectError: true, - }, - } - - for name, tc := range testcases { - t.Run(name, func(t *testing.T) { - actual, err := New(tc.input) - if tc.expectError { - require.Error(t, err) - require.Nil(t, actual) - } else { - require.NoError(t, err) - require.NotNil(t, actual) - } - }) - } -} - -func TestOutputDescription(t *testing.T) { - var out Output - require.NotEmpty(t, out.Description()) -} - -func TestOutputStart(t *testing.T) { - fs := afero.NewMemMapFs() - - out, err := New(output.Params{ConfigArgument: "test.out", FS: fs}) - require.NoError(t, err) - - err = out.Start() - require.NoError(t, err) - - err = out.Stop() - require.NoError(t, err) - - // At this point we should have an empty file. - fi, err := fs.Stat("test.out") - require.NoError(t, err) - require.Equal(t, int64(0), fi.Size()) -} - -// TestOutputStop tests that the metrics are correctly collected and written to the file. -func TestOutputStop(t *testing.T) { - fs := afero.NewMemMapFs() - - out, err := New(output.Params{ConfigArgument: "test.out", FS: fs}) - require.NoError(t, err) - - err = out.Start() - require.NoError(t, err) - - // TODO(mem): add samples - - err = out.Stop() - require.NoError(t, err) - - fi, err := fs.Stat("test.out") - require.NoError(t, err) - require.Equal(t, int64(0), fi.Size()) -} - -func makeSample(name string, value float64) metrics.Sample { - return metrics.Sample{ - TimeSeries: metrics.TimeSeries{ - Metric: &metrics.Metric{ - Name: name, - }, - }, - Value: value, - } -} - -func TestDeriveMetricNameAndValue(t *testing.T) { - - testcases := map[string]struct { - input metrics.Sample - expectedName string - expectedValue float64 - }{ - "iterations": { - input: makeSample("iterations", 1), - expectedName: "", - expectedValue: 0, - }, - "checks": { - input: makeSample("checks", 1), - expectedName: "checks_total", - expectedValue: 1, - }, - "iteration_duration": { - input: makeSample("iteration_duration", 1), - expectedName: "iteration_duration_seconds", - expectedValue: 0.001, - }, - "data_sent": { - input: makeSample("data_sent", 1), - expectedName: "data_sent_bytes", - expectedValue: 1, - }, - "data_received": { - input: makeSample("data_received", 1), - expectedName: "data_received_bytes", - expectedValue: 1, - }, - "something_else": { - input: makeSample("something_else", 42), - expectedName: "something_else", - expectedValue: 42, - }, - } - - for name, tc := range testcases { - t.Run(name, func(t *testing.T) { - actualName, actualValue := deriveMetricNameAndValue(tc.input) - require.Equal(t, tc.expectedName, actualName) - require.Equal(t, tc.expectedValue, actualValue) - }) - } -} - -func TestGetStats(t *testing.T) { - testcases := map[string]struct { - input []float64 - expected stats - }{ - "1": { // single sample - input: []float64{1.0}, - expected: stats{n: 1, min: 1, max: 1, sum: 1, med: 1}, - }, - "2": { // two samples - input: []float64{1.0, 2.0}, - expected: stats{n: 2, min: 1, max: 2, sum: 3, med: 1.5}, - }, - "3": { // three samples, regular - input: []float64{1.0, 2.0, 3.0}, - expected: stats{n: 3, min: 1, max: 3, sum: 6, med: 2.0}, - }, - "3b": { // three samples, irregular - input: []float64{1.0, 2.0, 4.0}, - expected: stats{n: 3, min: 1, max: 4, sum: 7, med: 2.0}, - }, - "4": { // four samples, irregular - input: []float64{1.0, 2.0, 4.0, 5.0}, - expected: stats{n: 4, min: 1, max: 5, sum: 12, med: 3.0}, - }, - } - - for name, tc := range testcases { - t.Run(name, func(t *testing.T) { - actual := getStats(tc.input) - if tc.expected != actual { - t.Log("expected:", tc.expected, "actual:", actual) - t.Fail() - } - }) - } -} - -func TestIsValidMetricName(t *testing.T) { - testcases := map[string]struct { - input string - expected bool - }{ - "single letter": {input: "a", expected: true}, - "word": {input: "abc", expected: true}, - "letter and number": {input: "a1", expected: true}, - "number": {input: "1", expected: false}, - "numbers": {input: "123", expected: false}, - "underscore": {input: "_", expected: true}, - "valid with underscore": {input: "a_b_c", expected: true}, - "valid with numbers": {input: "a_1_2", expected: true}, - "colon": {input: ":", expected: true}, - "namespace": {input: "abc::xyz", expected: true}, - "blank": {input: " ", expected: false}, - "words with blank": {input: "abc xyz", expected: false}, - "dash": {input: "-", expected: false}, - "words with dash": {input: "abc-xyz", expected: false}, - "utf8": {input: "á", expected: false}, - "empty": {input: "", expected: false}, - } - - for name, tc := range testcases { - t.Run(name, func(t *testing.T) { - actual := isValidMetricNameRe(tc.input) - if actual != tc.expected { - t.Log("expected:", tc.expected, "actual:", actual, "input:", tc.input) - t.Fail() - } - - actualNonRe := isValidMetricName(tc.input) - if actualNonRe != actual { - t.Log("expected:", actual, "actual:", actualNonRe, "input:", tc.input) - t.Fail() - } - }) - } -} - -func TestSanitizeLabelName(t *testing.T) { - testcases := map[string]struct { - input string - expected string - }{ - "single letter": {input: "a", expected: "a"}, - "word": {input: "abc", expected: "abc"}, - "letter and number": {input: "a1", expected: "a1"}, - "number": {input: "1", expected: "_"}, - "numbers": {input: "123", expected: "_23"}, - "underscore": {input: "_", expected: "_"}, - "valid with underscore": {input: "a_b_c", expected: "a_b_c"}, - "valid with numbers": {input: "a_1_2", expected: "a_1_2"}, - "colon": {input: ":", expected: ":"}, - "namespace": {input: "abc::xyz", expected: "abc::xyz"}, - "blank": {input: " ", expected: "_"}, - "words with blank": {input: "abc xyz", expected: "abc_xyz"}, - "dash": {input: "-", expected: "_"}, - "words with dash": {input: "abc-xyz", expected: "abc_xyz"}, - "utf8": {input: "á", expected: "_"}, - } - - for name, tc := range testcases { - t.Run(name, func(t *testing.T) { - actual := sanitizeLabelName(tc.input) - if actual != tc.expected { - t.Log("expected:", tc.expected, "actual:", actual, "input:", tc.input) - t.Fail() - } - }) - } -} - -func TestBufferedMetricTextOutputValue(t *testing.T) { - type metricData struct { - name string - kvs []string - value float64 - } - - testcases := map[string]struct { - kvs []string - data []metricData - expected string - }{ - "basic": { - data: []metricData{ - { - name: "test", - value: 1, - }, - }, - expected: "test{} 1\n", - }, - "one keyval": { - kvs: []string{"key", "value"}, - data: []metricData{ - { - name: "test", - value: 1, - }, - }, - expected: "test{key=\"value\"} 1\n", - }, - "multiple keyval": { - kvs: []string{"key1", "1", "key2", "2"}, - data: []metricData{ - { - name: "test", - value: 1, - }, - }, - expected: "test{key1=\"1\",key2=\"2\"} 1\n", - }, - "extra keyvals": { - kvs: []string{"key1", "1"}, - data: []metricData{ - { - name: "test", - kvs: []string{"key2", "2"}, - value: 1, - }, - }, - expected: "test{key2=\"2\",key1=\"1\"} 1\n", - }, - "invalid key": { - kvs: []string{"key 1", "1", "key 2", "2"}, - data: []metricData{ - { - name: "test", - value: 1, - }, - }, - expected: "test{key_1=\"1\",key_2=\"2\"} 1\n", - }, - "multiple metrics": { - kvs: []string{"key 1", "1", "key 2", "2"}, - data: []metricData{ - { - name: "a", - value: 1, - kvs: []string{"key3", "3"}, - }, - { - name: "b", - value: 2, - kvs: []string{"key4", "4"}, - }, - }, - expected: "a{key3=\"3\",key_1=\"1\",key_2=\"2\"} 1\nb{key4=\"4\",key_1=\"1\",key_2=\"2\"} 2\n", - }, - } - - for name, tc := range testcases { - t.Run(name, func(t *testing.T) { - var buf bytes.Buffer - to := newBufferedMetricTextOutput(&buf, tc.kvs...) - for _, d := range tc.data { - to.Name(d.name) - for i := 0; i < len(d.kvs); i += 2 { - to.KeyValue(d.kvs[i], d.kvs[i+1]) - } - to.Value(d.value) - } - require.Equal(t, tc.expected, buf.String()) - }) - } -} - -func joinNewline(s ...string) string { - return strings.Join(s, "\n") + "\n" -} - -func TestBufferedMetricTextOutputStats(t *testing.T) { - type metricData struct { - name string - kvs []string - values []float64 - } - - testcases := map[string]struct { - kvs []string - data []metricData - expected string - }{ - "basic": { - data: []metricData{ - { - name: "test", - values: []float64{1}, - }, - }, - expected: joinNewline( - `test_min{} 1`, - `test_max{} 1`, - `test{} 1`, - `test_count{} 1`, - `test_sum{} 1`, - ), - }, - "one keyval": { - kvs: []string{"key", "value"}, - data: []metricData{ - { - name: "test", - values: []float64{1}, - }, - }, - expected: joinNewline( - `test_min{key="value"} 1`, - `test_max{key="value"} 1`, - `test{key="value"} 1`, - `test_count{key="value"} 1`, - `test_sum{key="value"} 1`, - ), - }, - "multiple keyval": { - kvs: []string{"key1", "1", "key2", "2"}, - data: []metricData{ - { - name: "test", - values: []float64{1}, - }, - }, - expected: joinNewline( - `test_min{key1="1",key2="2"} 1`, - `test_max{key1="1",key2="2"} 1`, - `test{key1="1",key2="2"} 1`, - `test_count{key1="1",key2="2"} 1`, - `test_sum{key1="1",key2="2"} 1`, - ), - }, - "extra keyvals": { - kvs: []string{"key1", "1"}, - data: []metricData{ - { - name: "test", - kvs: []string{"key2", "2"}, - values: []float64{1}, - }, - }, - expected: joinNewline( - `test_min{key2="2",key1="1"} 1`, - `test_max{key2="2",key1="1"} 1`, - `test{key2="2",key1="1"} 1`, - `test_count{key2="2",key1="1"} 1`, - `test_sum{key2="2",key1="1"} 1`, - ), - }, - "invalid key": { - kvs: []string{"key 1", "1", "key 2", "2"}, - data: []metricData{ - { - name: "test", - values: []float64{1}, - }, - }, - expected: joinNewline( - `test_min{key_1="1",key_2="2"} 1`, - `test_max{key_1="1",key_2="2"} 1`, - `test{key_1="1",key_2="2"} 1`, - `test_count{key_1="1",key_2="2"} 1`, - `test_sum{key_1="1",key_2="2"} 1`, - ), - }, - "multiple metrics": { - kvs: []string{"key 1", "1", "key 2", "2"}, - data: []metricData{ - { - name: "a", - values: []float64{1, 2, 3}, - kvs: []string{"key3", "3"}, - }, - { - name: "b", - values: []float64{2, 4, 6}, - kvs: []string{"key 4", "4", "key5", "5"}, - }, - }, - expected: joinNewline( - `a_min{key3="3",key_1="1",key_2="2"} 1`, - `a_max{key3="3",key_1="1",key_2="2"} 3`, - `a{key3="3",key_1="1",key_2="2"} 2`, - `a_count{key3="3",key_1="1",key_2="2"} 3`, - `a_sum{key3="3",key_1="1",key_2="2"} 6`, - `b_min{key_4="4",key5="5",key_1="1",key_2="2"} 2`, - `b_max{key_4="4",key5="5",key_1="1",key_2="2"} 6`, - `b{key_4="4",key5="5",key_1="1",key_2="2"} 4`, - `b_count{key_4="4",key5="5",key_1="1",key_2="2"} 3`, - `b_sum{key_4="4",key5="5",key_1="1",key_2="2"} 12`, - ), - }, - } - - for name, tc := range testcases { - t.Run(name, func(t *testing.T) { - var buf bytes.Buffer - to := newBufferedMetricTextOutput(&buf, tc.kvs...) - for _, d := range tc.data { - to.Name(d.name) - for i := 0; i < len(d.kvs); i += 2 { - to.KeyValue(d.kvs[i], d.kvs[i+1]) - } - to.Stats(d.values) - } - require.Equal(t, tc.expected, buf.String()) - }) - } -} - -func TestTargetMetricsCollectionWriteOne(t *testing.T) { - c := newTargetMetricsCollection() - - require.Len(t, c, 0) - - c[targetId{ - url: "http://example.com", - method: "GET", - scenario: "s", - group: "g", - }] = targetMetrics{ - requests: 1, - failed: 0, - expectedResponse: false, - scenario: "s", - group: "g", - proto: "1.1", - tlsVersion: "1.3", - status: []string{"200"}, - duration: []float64{0.001}, - blocked: []float64{0.001}, - connecting: []float64{0.001}, - sending: []float64{0.001}, - waiting: []float64{0.001}, - receiving: []float64{0.001}, - tlsHandshaking: []float64{0.001}, - tags: map[string]string{"k": "v"}, - } - - var buf bytes.Buffer - - c.Write(&buf) - - expected := joinNewline( - `probe_http_info{tls_version="1.3",proto="1.1",k="v",url="http://example.com",method="GET",scenario="s",group="g"} 1`, - `probe_http_requests_total{url="http://example.com",method="GET",scenario="s",group="g"} 1`, - `probe_http_requests_failed_total{url="http://example.com",method="GET",scenario="s",group="g"} 0`, - `probe_http_status_code{url="http://example.com",method="GET",scenario="s",group="g"} 200`, - `probe_http_version{url="http://example.com",method="GET",scenario="s",group="g"} 1.1`, - `probe_http_ssl{url="http://example.com",method="GET",scenario="s",group="g"} 1`, - `probe_http_duration_seconds{phase="resolve",url="http://example.com",method="GET",scenario="s",group="g"} 0`, - `probe_http_duration_seconds{phase="connect",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds{phase="tls",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds{phase="processing",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds{phase="transfer",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - ) - - require.Equal(t, expected, buf.String()) -} - -func TestTargetMetricsCollectionWriteMany(t *testing.T) { - c := newTargetMetricsCollection() - - require.Len(t, c, 0) - - c[targetId{ - url: "http://example.com", - method: "GET", - scenario: "s", - group: "g", - }] = targetMetrics{ - requests: 2, - failed: 1, - expectedResponse: false, - scenario: "s", - group: "g", - proto: "1.1", - tlsVersion: "1.3", - status: []string{"200", "200"}, - duration: []float64{0.001, 0.001}, - blocked: []float64{0.001, 0.001}, - connecting: []float64{0.001, 0.001}, - sending: []float64{0.001, 0.001}, - waiting: []float64{0.001, 0.001}, - receiving: []float64{0.001, 0.001}, - tlsHandshaking: []float64{0.001, 0.001}, - tags: map[string]string{"k": "v"}, - } - - var buf bytes.Buffer - - c.Write(&buf) - - expected := joinNewline( - `probe_http_info{tls_version="1.3",proto="1.1",k="v",url="http://example.com",method="GET",scenario="s",group="g"} 1`, - `probe_http_requests_total{url="http://example.com",method="GET",scenario="s",group="g"} 2`, - `probe_http_requests_failed_total{url="http://example.com",method="GET",scenario="s",group="g"} 1`, - `probe_http_status_code{url="http://example.com",method="GET",scenario="s",group="g"} 200`, - `probe_http_version{url="http://example.com",method="GET",scenario="s",group="g"} 1.1`, - `probe_http_ssl{url="http://example.com",method="GET",scenario="s",group="g"} 1`, - `probe_http_duration_seconds_min{phase="resolve",url="http://example.com",method="GET",scenario="s",group="g"} 0`, - `probe_http_duration_seconds_max{phase="resolve",url="http://example.com",method="GET",scenario="s",group="g"} 0`, - `probe_http_duration_seconds{phase="resolve",url="http://example.com",method="GET",scenario="s",group="g"} 0`, - `probe_http_duration_seconds_count{phase="resolve",url="http://example.com",method="GET",scenario="s",group="g"} 2`, - `probe_http_duration_seconds_sum{phase="resolve",url="http://example.com",method="GET",scenario="s",group="g"} 0`, - `probe_http_duration_seconds_min{phase="connect",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds_max{phase="connect",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds{phase="connect",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds_count{phase="connect",url="http://example.com",method="GET",scenario="s",group="g"} 2`, - `probe_http_duration_seconds_sum{phase="connect",url="http://example.com",method="GET",scenario="s",group="g"} 0.002`, - `probe_http_duration_seconds_min{phase="tls",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds_max{phase="tls",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds{phase="tls",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds_count{phase="tls",url="http://example.com",method="GET",scenario="s",group="g"} 2`, - `probe_http_duration_seconds_sum{phase="tls",url="http://example.com",method="GET",scenario="s",group="g"} 0.002`, - `probe_http_duration_seconds_min{phase="processing",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds_max{phase="processing",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds{phase="processing",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds_count{phase="processing",url="http://example.com",method="GET",scenario="s",group="g"} 2`, - `probe_http_duration_seconds_sum{phase="processing",url="http://example.com",method="GET",scenario="s",group="g"} 0.002`, - `probe_http_duration_seconds_min{phase="transfer",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds_max{phase="transfer",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds{phase="transfer",url="http://example.com",method="GET",scenario="s",group="g"} 0.001`, - `probe_http_duration_seconds_count{phase="transfer",url="http://example.com",method="GET",scenario="s",group="g"} 2`, - `probe_http_duration_seconds_sum{phase="transfer",url="http://example.com",method="GET",scenario="s",group="g"} 0.002`, - ) - - require.Equal(t, expected, buf.String()) -} diff --git a/xk6/sm/probe.go b/xk6/sm/probe.go deleted file mode 100644 index 55139a46a..000000000 --- a/xk6/sm/probe.go +++ /dev/null @@ -1,74 +0,0 @@ -package sm - -import ( - "time" - - "github.com/sirupsen/logrus" - "go.k6.io/k6/js/modules" - "go.k6.io/k6/lib" - "go.k6.io/k6/metrics" -) - -func init() { - modules.Register("k6/x/sm", NewRootModule()) -} - -type RootModule struct{} - -var _ modules.Module = &RootModule{} - -func NewRootModule() *RootModule { - return &RootModule{} -} - -func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance { - return &ModuleInstance{ - vu: vu, - prober: &Prober{vu: vu}, - } -} - -type ModuleInstance struct { - vu modules.VU - prober *Prober -} - -var _ modules.Instance = &ModuleInstance{} - -func (mi *ModuleInstance) Exports() modules.Exports { - return modules.Exports{ - Default: mi.prober, - } -} - -type Prober struct { - vu modules.VU -} - -type Opts struct { - Method string -} - -func (p *Prober) Http(target string, opts Opts) bool { - ctx := p.vu.Context() - state := p.vu.State() - es := lib.GetExecutionState(ctx) - mFooBar, err := es.Test.Registry.NewMetric("foo_bar", metrics.Counter, metrics.Default) - if err != nil { - return false - } - - state.Logger.WithFields(logrus.Fields{"target": target, "opts": opts}).Info("Prober.Http called") - - // do something interesting here - - metrics.PushIfNotDone(ctx, state.Samples, metrics.Sample{ - Time: time.Now().UTC(), - Value: 1, - TimeSeries: metrics.TimeSeries{ - Metric: mFooBar, - }, - }) - - return true -}