Skip to content

Commit

Permalink
Merge 0834272 into 8da4149
Browse files Browse the repository at this point in the history
  • Loading branch information
mstoykov authored Jan 31, 2023
2 parents 8da4149 + 0834272 commit ad74aaa
Show file tree
Hide file tree
Showing 7 changed files with 468 additions and 62 deletions.
10 changes: 6 additions & 4 deletions cmd/tests/tracing_module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func TestTracingModuleClient(t *testing.T) {
propagator: "w3c",
})
export default function () {
export default async function () {
instrumentedHTTP.del("HTTPBIN_IP_URL/tracing");
instrumentedHTTP.get("HTTPBIN_IP_URL/tracing");
instrumentedHTTP.head("HTTPBIN_IP_URL/tracing");
Expand All @@ -46,13 +46,14 @@ func TestTracingModuleClient(t *testing.T) {
instrumentedHTTP.post("HTTPBIN_IP_URL/tracing");
instrumentedHTTP.put("HTTPBIN_IP_URL/tracing");
instrumentedHTTP.request("GET", "HTTPBIN_IP_URL/tracing");
await instrumentedHTTP.asyncRequest("GET", "HTTPBIN_IP_URL/tracing");
};
`)

ts := getSingleFileTestState(t, script, []string{"--out", "json=results.json"}, 0)
cmd.ExecuteWithGlobalState(ts.GlobalState)

assert.Equal(t, int64(8), atomic.LoadInt64(&gotRequests))
assert.Equal(t, int64(9), atomic.LoadInt64(&gotRequests))

jsonResults, err := afero.ReadFile(ts.FS, "results.json")
require.NoError(t, err)
Expand Down Expand Up @@ -119,7 +120,7 @@ func TestTracingInstrumentHTTP_W3C(t *testing.T) {
propagator: "w3c",
})
export default function () {
export default async function () {
http.del("HTTPBIN_IP_URL/tracing");
http.get("HTTPBIN_IP_URL/tracing");
http.head("HTTPBIN_IP_URL/tracing");
Expand All @@ -128,13 +129,14 @@ func TestTracingInstrumentHTTP_W3C(t *testing.T) {
http.post("HTTPBIN_IP_URL/tracing");
http.put("HTTPBIN_IP_URL/tracing");
http.request("GET", "HTTPBIN_IP_URL/tracing");
await http.asyncRequest("GET", "HTTPBIN_IP_URL/tracing");
};
`)

ts := getSingleFileTestState(t, script, []string{"--out", "json=results.json"}, 0)
cmd.ExecuteWithGlobalState(ts.GlobalState)

assert.Equal(t, int64(8), atomic.LoadInt64(&gotRequests))
assert.Equal(t, int64(9), atomic.LoadInt64(&gotRequests))

jsonResults, err := afero.ReadFile(ts.FS, "results.json")
require.NoError(t, err)
Expand Down
138 changes: 87 additions & 51 deletions js/modules/k6/experimental/tracing/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"time"

"github.com/dop251/goja"
"go.k6.io/k6/js/common"
"go.k6.io/k6/js/modules"
httpmodule "go.k6.io/k6/js/modules/k6/http"
"go.k6.io/k6/metrics"
Expand All @@ -30,47 +29,54 @@ type Client struct {
// uses it under the hood to emit the requests it
// instruments.
requestFunc HTTPRequestFunc

// asyncRequestFunc holds the http module's asyncRequest function
// used to emit HTTP requests in k6 script. The client
// uses it under the hood to emit the requests it
// instruments.
asyncRequestFunc HTTPAsyncRequestFunc
}

// HTTPRequestFunc is a type alias representing the prototype of
// k6's http module's request function
type HTTPRequestFunc func(method string, url goja.Value, args ...goja.Value) (*httpmodule.Response, error)
type (
HTTPRequestFunc func(method string, url goja.Value, args ...goja.Value) (*httpmodule.Response, error)
HTTPAsyncRequestFunc func(method string, url goja.Value, args ...goja.Value) (*goja.Promise, error)
)

// NewClient instantiates a new tracing Client
func NewClient(vu modules.VU, opts options) *Client {
func NewClient(vu modules.VU, opts options) (*Client, error) {
rt := vu.Runtime()

// Get the http module's request function
httpModuleRequest, err := rt.RunString("require('k6/http').request")
// Get the http module
httpModule, err := rt.RunString("require('k6/http')")
if err != nil {
common.Throw(
rt,
fmt.Errorf(
"failed initializing tracing client, "+
"unable to require http.request method; reason: %w",
err,
),
)
return nil,
fmt.Errorf("failed initializing tracing client, unable to require http.request method; reason: %w", err)
}
httpModuleObject := httpModule.ToObject(rt)

// Export the http module's request function goja.Callable as a Go function
var requestFunc HTTPRequestFunc
if err := rt.ExportTo(httpModuleRequest, &requestFunc); err != nil {
common.Throw(
rt,
fmt.Errorf("failed initializing tracing client, unable to export http.request method; reason: %w", err),
)
if err := rt.ExportTo(httpModuleObject.Get("request"), &requestFunc); err != nil {
return nil,
fmt.Errorf("failed initializing tracing client, unable to require http.request method; reason: %w", err)
}
// Export the http module's syncRequest function goja.Callable as a Go function
var asyncRequestFunc HTTPAsyncRequestFunc
if err := rt.ExportTo(httpModuleObject.Get("asyncRequest"), &asyncRequestFunc); err != nil {
return nil,
fmt.Errorf("failed initializing tracing client, unable to require http.asyncRequest method; reason: %w",
err)
}

client := &Client{vu: vu, requestFunc: requestFunc}
client := &Client{vu: vu, requestFunc: requestFunc, asyncRequestFunc: asyncRequestFunc}
if err := client.Configure(opts); err != nil {
common.Throw(
rt,
fmt.Errorf("failed initializing tracing client, invalid configuration; reason: %w", err),
)
return nil,
fmt.Errorf("failed initializing tracing client, invalid configuration; reason: %w", err)
}

return client
return client, nil
}

// Configure configures the tracing client with the given options.
Expand All @@ -93,15 +99,7 @@ func (c *Client) Configure(opts options) error {
return nil
}

// Request instruments the http module's request function with tracing headers,
// and ensures the trace_id is emitted as part of the output's data points metadata.
func (c *Client) Request(method string, url goja.Value, args ...goja.Value) (*httpmodule.Response, error) {
// The http module's request function expects the first argument to be the
// request body. If no body is provided, we need to pass null to the function.
if len(args) == 0 {
args = []goja.Value{goja.Null()}
}

func (c *Client) generateTraceContext() (http.Header, string, error) {
traceID := TraceID{
Prefix: k6Prefix,
Code: k6CloudCode,
Expand All @@ -110,41 +108,79 @@ func (c *Client) Request(method string, url goja.Value, args ...goja.Value) (*ht

encodedTraceID, err := traceID.Encode()
if err != nil {
return nil, fmt.Errorf("failed to encode the generated trace ID; reason: %w", err)
return http.Header{}, "", fmt.Errorf("failed to encode the generated trace ID; reason: %w", err)
}

// Produce a trace header in the format defined by the
// configured propagator.
// Produce a trace header in the format defined by the configured propagator.
traceContextHeader, err := c.propagator.Propagate(encodedTraceID)
if err != nil {
return nil, fmt.Errorf("failed to propagate trace ID; reason: %w", err)
return http.Header{}, "", fmt.Errorf("failed to propagate trace ID; reason: %w", err)
}

return traceContextHeader, encodedTraceID, nil
}

// Request instruments the http module's request function with tracing headers,
// and ensures the trace_id is emitted as part of the output's data points metadata.
func (c *Client) Request(method string, url goja.Value, args ...goja.Value) (*httpmodule.Response, error) {
var result *httpmodule.Response
var err error
err = c.instrumentedCall(func(args ...goja.Value) error {
result, err = c.requestFunc(method, url, args...)
return err
}, args...)

if err != nil {
return nil, err
}
return result, nil
}

// AsyncRequest instruments the http module's asyncRequest function with tracing headers,
// and ensures the trace_id is emitted as part of the output's data points metadata.
func (c *Client) AsyncRequest(method string, url goja.Value, args ...goja.Value) (*goja.Promise, error) {
var result *goja.Promise
var err error
err = c.instrumentedCall(func(args ...goja.Value) error {
result, err = c.asyncRequestFunc(method, url, args...)
return err
}, args...)

if err != nil {
return nil, err
}
return result, nil
}

func (c *Client) instrumentedCall(call func(args ...goja.Value) error, args ...goja.Value) error {
if len(args) == 0 {
args = []goja.Value{goja.Null()}
}

traceContextHeader, encodedTraceID, err := c.generateTraceContext()
if err != nil {
return err
}
// update the `params` argument with the trace context header
// so that it can be used by the http module's request function.
args, err = c.instrumentArguments(traceContextHeader, args...)
if err != nil {
return nil, fmt.Errorf("failed to instrument request arguments; reason: %w", err)
return fmt.Errorf("failed to instrument request arguments; reason: %w", err)
}

// Add the trace ID to the VU's state, so that it can be
// used in the metrics emitted by the HTTP module.
c.vu.State().Tags.Modify(func(t *metrics.TagsAndMeta) {
t.SetMetadata(metadataTraceIDKeyName, encodedTraceID)
})

response, err := c.requestFunc(method, url, args...)
if err != nil {
return nil, err
}

// Remove the trace ID from the VU's state, so that it doesn't
// leak into other requests.
c.vu.State().Tags.Modify(func(t *metrics.TagsAndMeta) {
t.DeleteMetadata(metadataTraceIDKeyName)
})

return response, nil
// Remove the trace ID from the VU's state, so that it doesn't leak into other requests.
defer func() {
c.vu.State().Tags.Modify(func(t *metrics.TagsAndMeta) {
t.DeleteMetadata(metadataTraceIDKeyName)
})
}()

return call(args...)
}

// Del instruments the http module's delete method.
Expand Down
13 changes: 11 additions & 2 deletions js/modules/k6/experimental/tracing/module.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,11 @@ func (mi *ModuleInstance) newClient(cc goja.ConstructorCall) *goja.Object {
common.Throw(rt, fmt.Errorf("unable to parse options object; reason: %w", err))
}

return rt.ToValue(NewClient(mi.vu, opts)).ToObject(rt)
client, err := NewClient(mi.vu, opts)
if err != nil {
common.Throw(rt, err)
}
return rt.ToValue(client).ToObject(rt)
}

// InstrumentHTTP instruments the HTTP module with tracing headers.
Expand All @@ -96,7 +100,11 @@ func (mi *ModuleInstance) instrumentHTTP(options options) {

// Initialize the tracing module's instance default client,
// and configure it using the user-supplied set of options.
mi.Client = NewClient(mi.vu, options)
var err error
mi.Client, err = NewClient(mi.vu, options)
if err != nil {
common.Throw(rt, err)
}

// Explicitly inject the http module in the VU's runtime.
// This allows us to later on override the http module's methods
Expand Down Expand Up @@ -136,4 +144,5 @@ func (mi *ModuleInstance) instrumentHTTP(options options) {
mustSetHTTPMethod("post", httpModuleObj, mi.Client.Patch)
mustSetHTTPMethod("put", httpModuleObj, mi.Client.Patch)
mustSetHTTPMethod("request", httpModuleObj, mi.Client.Request)
mustSetHTTPMethod("asyncRequest", httpModuleObj, mi.Client.AsyncRequest)
}
Loading

0 comments on commit ad74aaa

Please sign in to comment.