Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

http: add asyncRequest #2877

Merged
merged 2 commits into from
Feb 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 k6/http module; 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)
Copy link
Contributor

@codebien codebien Feb 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I remove this line nothing fails for the http and tracing module's tests.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do think this should be addressed separately. After taking a look this is not new I just refactor the code - if anything my code is less likely to break :P. But writing a test for this seems a bit more involved than I would prefer for this PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did open an issue with it #2889

})
}()

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