From 936303a53930bf50d1be1b4b5573c2be0401f331 Mon Sep 17 00:00:00 2001 From: oleiade Date: Fri, 13 Jan 2023 14:12:18 +0100 Subject: [PATCH] Expose the tracing client publicly as k6/experimental/tracing.Client This commit exposes the Client constructor publicly as part of the k6/experimental/tracing module. From this point forward users will be able to instantiate the Client, and perform instrumented HTTP requests using it. This commit also adds a bunch of integration tests covering the expected behavior of the module's API. --- cmd/integration_test.go | 125 ++++++++++++++++++ js/modules/k6/experimental/tracing/module.go | 11 +- js/modules/k6/experimental/tracing/tracing.go | 35 +++++ samples/experimental/tracing-client.js | 46 +++++++ 4 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 js/modules/k6/experimental/tracing/tracing.go create mode 100644 samples/experimental/tracing-client.js diff --git a/cmd/integration_test.go b/cmd/integration_test.go index 901273b4a5b2..8726bef85db0 100644 --- a/cmd/integration_test.go +++ b/cmd/integration_test.go @@ -1534,3 +1534,128 @@ func TestPrometheusRemoteWriteOutput(t *testing.T) { assert.Contains(t, stdOut, "output: Prometheus remote write") } + +func TestTracingClient(t *testing.T) { + t.Parallel() + tb := httpmultibin.NewHTTPMultiBin(t) + + gotRequests := 0 + + tb.Mux.HandleFunc("/tracing", func(w http.ResponseWriter, r *http.Request) { + gotRequests++ + assert.NotEmpty(t, r.Header.Get("traceparent")) + assert.Len(t, r.Header.Get("traceparent"), 55) + }) + + script := tb.Replacer.Replace(` + import http from "k6/http"; + import { check } from "k6"; + import tracing from "k6/experimental/tracing"; + + const instrumentedHTTP = new tracing.Client({ + propagator: "w3c", + }) + + export default function () { + instrumentedHTTP.del("HTTPBIN_IP_URL/tracing"); + instrumentedHTTP.get("HTTPBIN_IP_URL/tracing"); + instrumentedHTTP.head("HTTPBIN_IP_URL/tracing"); + instrumentedHTTP.options("HTTPBIN_IP_URL/tracing"); + instrumentedHTTP.patch("HTTPBIN_IP_URL/tracing"); + instrumentedHTTP.post("HTTPBIN_IP_URL/tracing"); + instrumentedHTTP.put("HTTPBIN_IP_URL/tracing"); + instrumentedHTTP.request("GET", "HTTPBIN_IP_URL/tracing"); + }; + `) + + ts := getSingleFileTestState(t, script, []string{"--out", "json=results.json"}, 0) + newRootCommand(ts.globalState).execute() + + assert.Equal(t, 8, gotRequests) + + jsonResults, err := afero.ReadFile(ts.fs, "results.json") + require.NoError(t, err) + + gotHTTPDataPoints := false + + for _, jsonLine := range bytes.Split(jsonResults, []byte("\n")) { + if len(jsonLine) == 0 { + continue + } + + var line sampleEnvelope + require.NoError(t, json.Unmarshal(jsonLine, &line)) + + if line.Type != "Point" { + continue + } + + // Filter metric samples which are not related to http + if !strings.HasPrefix(line.Metric, "http_") { + continue + } + + gotHTTPDataPoints = true + + anyTraceID, hasTraceID := line.Data.Metadata["trace_id"] + require.True(t, hasTraceID) + + traceID, gotTraceID := anyTraceID.(string) + require.True(t, gotTraceID) + + assert.Len(t, traceID, 32) + } + + assert.True(t, gotHTTPDataPoints) +} + +func TestTracingClient_DoesNotInterfereWithHTTPModule(t *testing.T) { + t.Parallel() + tb := httpmultibin.NewHTTPMultiBin(t) + + gotRequests := 0 + gotInstrumentedRequests := 0 + + tb.Mux.HandleFunc("/tracing", func(w http.ResponseWriter, r *http.Request) { + gotRequests++ + + if r.Header.Get("traceparent") != "" { + gotInstrumentedRequests++ + assert.Len(t, r.Header.Get("traceparent"), 55) + } + }) + + script := tb.Replacer.Replace(` + import http from "k6/http"; + import { check } from "k6"; + import tracing from "k6/experimental/tracing"; + + const instrumentedHTTP = new tracing.Client({ + propagator: "w3c", + }) + + export default function () { + instrumentedHTTP.get("HTTPBIN_IP_URL/tracing"); + http.get("HTTPBIN_IP_URL/tracing"); + instrumentedHTTP.head("HTTPBIN_IP_URL/tracing"); + }; + `) + + ts := getSingleFileTestState(t, script, []string{"--out", "json=results.json"}, 0) + newRootCommand(ts.globalState).execute() + + assert.Equal(t, 3, gotRequests) + assert.Equal(t, 2, gotInstrumentedRequests) +} + +// sampleEnvelope is a trimmed version of the struct found +// in output/json/wrapper.go +// TODO: use the json output's wrapper struct instead if it's ever exported +type sampleEnvelope struct { + Metric string `json:"metric"` + Type string `json:"type"` + Data struct { + Value float64 `json:"value"` + Metadata map[string]interface{} `json:"metadata"` + } `json:"data"` +} diff --git a/js/modules/k6/experimental/tracing/module.go b/js/modules/k6/experimental/tracing/module.go index 6d802a661b33..e127d053e130 100644 --- a/js/modules/k6/experimental/tracing/module.go +++ b/js/modules/k6/experimental/tracing/module.go @@ -13,6 +13,8 @@ type ( // ModuleInstance represents an instance of the JS module. ModuleInstance struct { vu modules.VU + + *Tracing } ) @@ -32,11 +34,18 @@ func New() *RootModule { func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance { return &ModuleInstance{ vu: vu, + Tracing: &Tracing{ + vu: vu, + }, } } // Exports implements the modules.Instance interface and returns // the exports of the JS module. func (mi *ModuleInstance) Exports() modules.Exports { - return modules.Exports{} + return modules.Exports{ + Named: map[string]interface{}{ + "Client": mi.NewClient, + }, + } } diff --git a/js/modules/k6/experimental/tracing/tracing.go b/js/modules/k6/experimental/tracing/tracing.go new file mode 100644 index 000000000000..1f5f0ba8db14 --- /dev/null +++ b/js/modules/k6/experimental/tracing/tracing.go @@ -0,0 +1,35 @@ +package tracing + +import ( + "errors" + "fmt" + + "github.com/dop251/goja" + "go.k6.io/k6/js/common" + "go.k6.io/k6/js/modules" +) + +// Tracing is the JS module instance that will be created for each VU. +type Tracing struct { + vu modules.VU +} + +// NewClient is the JS constructor for the tracing.Client +// +// It expects a single configuration object as argument, which +// will be used to instantiate an `Object` instance internally, +// and will be used by the client to configure itself. +func (t *Tracing) NewClient(cc goja.ConstructorCall) *goja.Object { + rt := t.vu.Runtime() + + if len(cc.Arguments) < 1 { + common.Throw(rt, errors.New("Client constructor expects a single configuration object as argument; none given")) + } + + var opts options + if err := rt.ExportTo(cc.Arguments[0], &opts); err != nil { + common.Throw(rt, fmt.Errorf("unable to parse options object; reason: %w", err)) + } + + return rt.ToValue(NewClient(t.vu, opts)).ToObject(rt) +} diff --git a/samples/experimental/tracing-client.js b/samples/experimental/tracing-client.js new file mode 100644 index 000000000000..572554241792 --- /dev/null +++ b/samples/experimental/tracing-client.js @@ -0,0 +1,46 @@ +import http from "k6/http"; +import { check } from "k6"; +import tracing from "k6/experimental/tracing"; + +// Explicitly instantiating a tracing client allows to distringuish +// instrumented from non-instrumented HTTP calls, by keeping APIs separate. +// It also allows for finer-grained configuration control, by letting +// users changing the tracing configuration on the fly during their +// script's execution. +let instrumentedHTTP = new tracing.Client({ + propagator: "w3c", +}); + +const testData = { name: "Bert" }; + +export default () => { + // Using the tracing client instance, HTTP calls will have + // their trace context headers set. + let res = instrumentedHTTP.request("GET", "http://httpbin.org/get", null, { + headers: { + "X-Example-Header": "instrumented/request", + }, + }); + check(res, { + "status is 200": (r) => r.status === 200, + }); + + // The tracing client offers more flexibility over + // the `instrumentHTTP` function, as it leaves the + // imported standard http module untouched. Thus, + // one can still perform non-instrumented HTTP calls + // using it. + res = http.post("http://httpbin.org/post", JSON.stringify(testData), { + headers: { "X-Example-Header": "noninstrumented/post" }, + }); + check(res, { + "status is 200": (r) => r.status === 200, + }); + + res = instrumentedHTTP.del("http://httpbin.org/delete", null, { + headers: { "X-Example-Header": "instrumented/delete" }, + }); + check(res, { + "status is 200": (r) => r.status === 200, + }); +};