Skip to content

Commit

Permalink
Expose the tracing client publicly as k6/experimental/tracing.Client
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
oleiade committed Jan 19, 2023
1 parent 4069d9a commit 936303a
Show file tree
Hide file tree
Showing 4 changed files with 216 additions and 1 deletion.
125 changes: 125 additions & 0 deletions cmd/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
11 changes: 10 additions & 1 deletion js/modules/k6/experimental/tracing/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ type (
// ModuleInstance represents an instance of the JS module.
ModuleInstance struct {
vu modules.VU

*Tracing
}
)

Expand All @@ -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,
},
}
}
35 changes: 35 additions & 0 deletions js/modules/k6/experimental/tracing/tracing.go
Original file line number Diff line number Diff line change
@@ -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)
}
46 changes: 46 additions & 0 deletions samples/experimental/tracing-client.js
Original file line number Diff line number Diff line change
@@ -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,
});
};

0 comments on commit 936303a

Please sign in to comment.