diff --git a/cmd/tests/tracing_module_test.go b/cmd/tests/tracing_module_test.go deleted file mode 100644 index 155a58472af..00000000000 --- a/cmd/tests/tracing_module_test.go +++ /dev/null @@ -1,523 +0,0 @@ -package tests - -import ( - "bytes" - "encoding/json" - "net/http" - "path/filepath" - "strings" - "sync/atomic" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.k6.io/k6/cmd" - "go.k6.io/k6/lib/fsext" - "go.k6.io/k6/lib/testutils/httpmultibin" - "go.k6.io/k6/metrics" -) - -func TestTracingModuleClient(t *testing.T) { - t.Parallel() - tb := httpmultibin.NewHTTPMultiBin(t) - - var gotRequests int64 - - tb.Mux.HandleFunc("/tracing", func(_ http.ResponseWriter, r *http.Request) { - atomic.AddInt64(&gotRequests, 1) - 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 async 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"); - 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(9), atomic.LoadInt64(&gotRequests)) - - jsonResults, err := fsext.ReadFile(ts.FS, "results.json") - require.NoError(t, err) - - assertHasTraceIDMetadata(t, jsonResults, 9, tb.Replacer.Replace("HTTPBIN_IP_URL/tracing")) -} - -func TestTracingClient_DoesNotInterfereWithHTTPModule(t *testing.T) { - t.Parallel() - tb := httpmultibin.NewHTTPMultiBin(t) - - var gotRequests int64 - var gotInstrumentedRequests int64 - - tb.Mux.HandleFunc("/tracing", func(_ http.ResponseWriter, r *http.Request) { - atomic.AddInt64(&gotRequests, 1) - - if r.Header.Get("traceparent") != "" { - atomic.AddInt64(&gotInstrumentedRequests, 1) - 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) - cmd.ExecuteWithGlobalState(ts.GlobalState) - - assert.Equal(t, int64(3), atomic.LoadInt64(&gotRequests)) - assert.Equal(t, int64(2), atomic.LoadInt64(&gotInstrumentedRequests)) -} - -func TestTracingModuleClient_HundredPercentSampling(t *testing.T) { - t.Parallel() - tb := httpmultibin.NewHTTPMultiBin(t) - - var gotRequests int64 - var gotSampleFlags int64 - - tb.Mux.HandleFunc("/tracing", func(_ http.ResponseWriter, r *http.Request) { - atomic.AddInt64(&gotRequests, 1) - - traceparent := r.Header.Get("traceparent") - require.NotEmpty(t, traceparent) - require.Len(t, traceparent, 55) - - if traceparent[54] == '1' { - atomic.AddInt64(&gotSampleFlags, 1) - } - }) - - script := tb.Replacer.Replace(` - import http from "k6/http"; - import { check } from "k6"; - import tracing from "k6/experimental/tracing"; - - export const options = { - // 100 iterations to make sure we get 100% sampling - iterations: 100, - } - - const instrumentedHTTP = new tracing.Client({ - propagator: "w3c", - - // 100% sampling - sampling: 1.0, - }) - - export default function () { - instrumentedHTTP.get("HTTPBIN_IP_URL/tracing"); - }; - `) - - ts := getSingleFileTestState(t, script, []string{"--out", "json=results.json"}, 0) - cmd.ExecuteWithGlobalState(ts.GlobalState) - - assert.Equal(t, int64(100), atomic.LoadInt64(&gotSampleFlags)) - assert.Equal(t, int64(100), atomic.LoadInt64(&gotRequests)) - - jsonResults, err := fsext.ReadFile(ts.FS, "results.json") - require.NoError(t, err) - - assertHasTraceIDMetadata(t, jsonResults, 100, tb.Replacer.Replace("HTTPBIN_IP_URL/tracing")) -} - -func TestTracingModuleClient_NoSamplingSetShouldAlwaysSample(t *testing.T) { - t.Parallel() - tb := httpmultibin.NewHTTPMultiBin(t) - - var gotRequests int64 - var gotSampleFlags int64 - - tb.Mux.HandleFunc("/tracing", func(_ http.ResponseWriter, r *http.Request) { - atomic.AddInt64(&gotRequests, 1) - - traceparent := r.Header.Get("traceparent") - require.NotEmpty(t, traceparent) - require.Len(t, traceparent, 55) - - if traceparent[54] == '1' { - atomic.AddInt64(&gotSampleFlags, 1) - } - }) - - script := tb.Replacer.Replace(` - import http from "k6/http"; - import { check } from "k6"; - import tracing from "k6/experimental/tracing"; - - export const options = { - // 100 iterations to make sure we get 100% sampling - iterations: 100, - } - - // We do not set the sampling option, thus the default - // behavior should be to always sample. - const instrumentedHTTP = new tracing.Client({ - propagator: "w3c", - }) - - export default function () { - instrumentedHTTP.get("HTTPBIN_IP_URL/tracing"); - }; - `) - - ts := getSingleFileTestState(t, script, []string{"--out", "json=results.json"}, 0) - cmd.ExecuteWithGlobalState(ts.GlobalState) - - assert.Equal(t, int64(100), atomic.LoadInt64(&gotSampleFlags)) - assert.Equal(t, int64(100), atomic.LoadInt64(&gotRequests)) - - jsonResults, err := fsext.ReadFile(ts.FS, "results.json") - require.NoError(t, err) - - assertHasTraceIDMetadata(t, jsonResults, 100, tb.Replacer.Replace("HTTPBIN_IP_URL/tracing")) -} - -func TestTracingModuleClient_ZeroPercentSampling(t *testing.T) { - t.Parallel() - tb := httpmultibin.NewHTTPMultiBin(t) - - var gotRequests int64 - var gotSampleFlags int64 - - tb.Mux.HandleFunc("/tracing", func(_ http.ResponseWriter, r *http.Request) { - atomic.AddInt64(&gotRequests, 1) - - traceparent := r.Header.Get("traceparent") - require.NotEmpty(t, traceparent) - require.Len(t, traceparent, 55) - - if traceparent[54] == '1' { - atomic.AddInt64(&gotSampleFlags, 1) - } - }) - - script := tb.Replacer.Replace(` - import http from "k6/http"; - import { check } from "k6"; - import tracing from "k6/experimental/tracing"; - - export const options = { - // 100 iterations to make sure we get 100% sampling - iterations: 100, - } - - const instrumentedHTTP = new tracing.Client({ - propagator: "w3c", - - // 0% sampling - sampling: 0.0, - }) - - export default function () { - instrumentedHTTP.get("HTTPBIN_IP_URL/tracing"); - }; - `) - - ts := getSingleFileTestState(t, script, []string{}, 0) - cmd.ExecuteWithGlobalState(ts.GlobalState) - - assert.Equal(t, int64(0), atomic.LoadInt64(&gotSampleFlags)) - assert.Equal(t, int64(100), atomic.LoadInt64(&gotRequests)) -} - -func TestTracingInstrumentHTTP_W3C(t *testing.T) { - t.Parallel() - tb := httpmultibin.NewHTTPMultiBin(t) - - var gotRequests int64 - - tb.Mux.HandleFunc("/tracing", func(_ http.ResponseWriter, r *http.Request) { - atomic.AddInt64(&gotRequests, 1) - 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 tracing from "k6/experimental/tracing"; - - tracing.instrumentHTTP({ - propagator: "w3c", - }) - - export default async function () { - http.del("HTTPBIN_IP_URL/tracing"); - http.get("HTTPBIN_IP_URL/tracing"); - http.head("HTTPBIN_IP_URL/tracing"); - http.options("HTTPBIN_IP_URL/tracing"); - http.patch("HTTPBIN_IP_URL/tracing"); - 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(9), atomic.LoadInt64(&gotRequests)) - - jsonResults, err := fsext.ReadFile(ts.FS, "results.json") - require.NoError(t, err) - - assertHasTraceIDMetadata(t, jsonResults, 9, tb.Replacer.Replace("HTTPBIN_IP_URL/tracing")) -} - -func TestTracingInstrumentHTTP_Jaeger(t *testing.T) { - t.Parallel() - tb := httpmultibin.NewHTTPMultiBin(t) - - var gotRequests int64 - - tb.Mux.HandleFunc("/tracing", func(_ http.ResponseWriter, r *http.Request) { - atomic.AddInt64(&gotRequests, 1) - assert.NotEmpty(t, r.Header.Get("uber-trace-id")) - assert.Len(t, r.Header.Get("uber-trace-id"), 45) - }) - - script := tb.Replacer.Replace(` - import http from "k6/http"; - import { check } from "k6"; - import tracing from "k6/experimental/tracing"; - - tracing.instrumentHTTP({ - propagator: "jaeger", - }) - - export default function () { - http.del("HTTPBIN_IP_URL/tracing"); - http.get("HTTPBIN_IP_URL/tracing"); - http.head("HTTPBIN_IP_URL/tracing"); - http.options("HTTPBIN_IP_URL/tracing"); - http.patch("HTTPBIN_IP_URL/tracing"); - http.post("HTTPBIN_IP_URL/tracing"); - http.put("HTTPBIN_IP_URL/tracing"); - http.request("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)) - - jsonResults, err := fsext.ReadFile(ts.FS, "results.json") - require.NoError(t, err) - - assertHasTraceIDMetadata(t, jsonResults, 8, tb.Replacer.Replace("HTTPBIN_IP_URL/tracing")) -} - -func TestTracingInstrumentHTTP_FillsParams(t *testing.T) { - t.Parallel() - tb := httpmultibin.NewHTTPMultiBin(t) - - var gotRequests int64 - - tb.Mux.HandleFunc("/tracing", func(_ http.ResponseWriter, r *http.Request) { - atomic.AddInt64(&gotRequests, 1) - - assert.NotEmpty(t, r.Header.Get("traceparent")) - assert.Len(t, r.Header.Get("traceparent"), 55) - - assert.NotEmpty(t, r.Header.Get("X-Test-Header")) - assert.Equal(t, "test", r.Header.Get("X-Test-Header")) - }) - - script := tb.Replacer.Replace(` - import http from "k6/http"; - import tracing from "k6/experimental/tracing"; - - tracing.instrumentHTTP({ - propagator: "w3c", - }) - - const testHeaders = { - "X-Test-Header": "test", - } - - export default function () { - http.del("HTTPBIN_IP_URL/tracing", null, { headers: testHeaders }); - http.get("HTTPBIN_IP_URL/tracing", { headers: testHeaders }); - http.head("HTTPBIN_IP_URL/tracing", { headers: testHeaders }); - http.options("HTTPBIN_IP_URL/tracing", null, { headers: testHeaders }); - http.patch("HTTPBIN_IP_URL/tracing", null, { headers: testHeaders }); - http.post("HTTPBIN_IP_URL/tracing", null, { headers: testHeaders }); - http.put("HTTPBIN_IP_URL/tracing", null, { headers: testHeaders }); - http.request("GET", "HTTPBIN_IP_URL/tracing", null, { headers: testHeaders }); - }; - `) - - ts := getSingleFileTestState(t, script, []string{"--out", "json=results.json"}, 0) - cmd.ExecuteWithGlobalState(ts.GlobalState) - - assert.Equal(t, int64(8), atomic.LoadInt64(&gotRequests)) - - jsonResults, err := fsext.ReadFile(ts.FS, "results.json") - require.NoError(t, err) - - assertHasTraceIDMetadata(t, jsonResults, 8, tb.Replacer.Replace("HTTPBIN_IP_URL/tracing")) -} - -func TestTracingInstrummentHTTP_SupportsMultipleTestScripts(t *testing.T) { - t.Parallel() - - var gotRequests int64 - - tb := httpmultibin.NewHTTPMultiBin(t) - tb.Mux.HandleFunc("/tracing", func(_ http.ResponseWriter, r *http.Request) { - atomic.AddInt64(&gotRequests, 1) - - assert.NotEmpty(t, r.Header.Get("traceparent")) - assert.Len(t, r.Header.Get("traceparent"), 55) - }) - - mainScript := tb.Replacer.Replace(` - import http from "k6/http"; - import tracing from "k6/experimental/tracing"; - - import { iShouldBeInstrumented } from "./imported.js"; - - tracing.instrumentHTTP({ - propagator: "w3c", - }) - - export default function() { - iShouldBeInstrumented(); - }; - `) - - importedScript := tb.Replacer.Replace(` - import http from "k6/http"; - - export function iShouldBeInstrumented() { - http.head("HTTPBIN_IP_URL/tracing"); - } - `) - - ts := NewGlobalTestState(t) - require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "main.js"), []byte(mainScript), 0o644)) - require.NoError(t, fsext.WriteFile(ts.FS, filepath.Join(ts.Cwd, "imported.js"), []byte(importedScript), 0o644)) - - ts.CmdArgs = []string{"k6", "run", "--out", "json=results.json", "main.js"} - ts.ExpectedExitCode = 0 - - cmd.ExecuteWithGlobalState(ts.GlobalState) - - jsonResults, err := fsext.ReadFile(ts.FS, "results.json") - require.NoError(t, err) - - assert.Equal(t, int64(1), atomic.LoadInt64(&gotRequests)) - assertHasTraceIDMetadata(t, jsonResults, 1, tb.Replacer.Replace("HTTPBIN_IP_URL/tracing")) -} - -// assertHasTraceIDMetadata checks that the trace_id metadata is present and has the correct format -// for all http metrics in the json results file. -// -// The `expectOccurences` parameter is used to check that the trace_id metadata is present for the -// expected number of http metrics. For instance, in a script with 2 http requests, the -// `expectOccurences` parameter should be 2. -// -// The onUrls parameter is used to check that the trace_id metadata is present for the data points -// with the expected URLs. Its value should reflect the URLs used in the script. -func assertHasTraceIDMetadata(t *testing.T, jsonResults []byte, expectOccurences int, onUrls ...string) { - gotHTTPDataPoints := false - - urlHTTPTraceIDs := make(map[string]map[string]int) - - 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) - - if _, ok := urlHTTPTraceIDs[line.Data.Tags["url"]]; !ok { - urlHTTPTraceIDs[line.Data.Tags["url"]] = make(map[string]int) - urlHTTPTraceIDs[line.Data.Tags["url"]][metrics.HTTPReqsName] = 0 - } - urlHTTPTraceIDs[line.Data.Tags["url"]][line.Metric]++ - } - - assert.True(t, gotHTTPDataPoints) - - for _, url := range onUrls { - assert.Contains(t, urlHTTPTraceIDs, url) - assert.Equal(t, urlHTTPTraceIDs[url][metrics.HTTPReqsName], expectOccurences) - assert.Equal(t, urlHTTPTraceIDs[url][metrics.HTTPReqFailedName], expectOccurences) - assert.Equal(t, urlHTTPTraceIDs[url][metrics.HTTPReqDurationName], expectOccurences) - assert.Equal(t, urlHTTPTraceIDs[url][metrics.HTTPReqBlockedName], expectOccurences) - assert.Equal(t, urlHTTPTraceIDs[url][metrics.HTTPReqConnectingName], expectOccurences) - assert.Equal(t, urlHTTPTraceIDs[url][metrics.HTTPReqTLSHandshakingName], expectOccurences) - assert.Equal(t, urlHTTPTraceIDs[url][metrics.HTTPReqSendingName], expectOccurences) - assert.Equal(t, urlHTTPTraceIDs[url][metrics.HTTPReqWaitingName], expectOccurences) - assert.Equal(t, urlHTTPTraceIDs[url][metrics.HTTPReqReceivingName], expectOccurences) - } -} - -// 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"` - Tags map[string]string `json:"tags"` - Metadata map[string]interface{} `json:"metadata"` - } `json:"data"` -} diff --git a/examples/experimental/tracing/tracing-client.js b/examples/experimental/tracing/tracing-client.js deleted file mode 100644 index 57255424179..00000000000 --- a/examples/experimental/tracing/tracing-client.js +++ /dev/null @@ -1,46 +0,0 @@ -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, - }); -}; diff --git a/examples/experimental/tracing/tracing-instrumentHTTP.js b/examples/experimental/tracing/tracing-instrumentHTTP.js deleted file mode 100644 index aaf866089d1..00000000000 --- a/examples/experimental/tracing/tracing-instrumentHTTP.js +++ /dev/null @@ -1,24 +0,0 @@ -import http from "k6/http"; -import { check } from "k6"; -import tracing from "k6/experimental/tracing"; - -// instrumentHTTP will ensure that all requests made by the http module -// will be traced. The first argument is a configuration object that -// can be used to configure the tracer. -// -// Currently supported HTTP methods are: get, post, put, patch, head, -// del, options, and request. -tracing.instrumentHTTP({ - propagator: "w3c", -}); - -export default () => { - let res = http.get("http://httpbin.org/get", { - headers: { - "X-Example-Header": "instrumented/get", - }, - }); - check(res, { - "status is 200": (r) => r.status === 200, - }); -}; diff --git a/examples/experimental/tracing/tracing-sampling.js b/examples/experimental/tracing/tracing-sampling.js deleted file mode 100644 index 99781602806..00000000000 --- a/examples/experimental/tracing/tracing-sampling.js +++ /dev/null @@ -1,31 +0,0 @@ -import http from "k6/http"; -import { check } from "k6"; -import tracing from "k6/experimental/tracing"; - -export const options = { - // As the number of sampled requests will converge towards - // the sampling percentage, we need to increase the number - // of iterations to get a more accurate result. - iterations: 10000, - - vus: 100, -}; - -tracing.instrumentHTTP({ - propagator: "w3c", - - // Only 10% of the requests made will have their trace context - // header's sample flag set to activated. - sampling: 0.1, -}); - -export default () => { - let res = http.get("http://httpbin.org/get", { - headers: { - "X-Example-Header": "instrumented/get", - }, - }); - check(res, { - "status is 200": (r) => r.status === 200, - }); -}; diff --git a/js/jsmodules.go b/js/jsmodules.go index 6bc6868c9a1..e6f96eb2882 100644 --- a/js/jsmodules.go +++ b/js/jsmodules.go @@ -16,7 +16,6 @@ import ( "go.k6.io/k6/js/modules/k6/experimental/csv" "go.k6.io/k6/js/modules/k6/experimental/fs" "go.k6.io/k6/js/modules/k6/experimental/streams" - "go.k6.io/k6/js/modules/k6/experimental/tracing" "go.k6.io/k6/js/modules/k6/grpc" "go.k6.io/k6/js/modules/k6/html" "go.k6.io/k6/js/modules/k6/http" @@ -46,11 +45,10 @@ func getInternalJSModules() map[string]interface{} { "k6/experimental/websockets": &expws.RootModule{}, "k6/experimental/timers": newRemovedModule( "k6/experimental/timers has been graduated, please use k6/timers instead."), - "k6/experimental/tracing": newWarnExperimentalModule(tracing.New(), - "k6/experimental/tracing is now deprecated. All of its functionality is available as pure javascript module."+ - " More info available at the docs:"+ - " https://grafana.com/docs/k6/latest/javascript-api/jslib/http-instrumentation-tempo"+ - " The module will be removed after November 11th, 2024 (v0.55.0). Ensure your scripts are migrated by then."), + "k6/experimental/tracing": newRemovedModule( + "k6/experimental/tracing has been removed. All of it functionality is available as pure javascript module." + + " More info available at the docs:" + + " https://grafana.com/docs/k6/latest/javascript-api/jslib/http-instrumentation-tempo"), "k6/experimental/browser": newWarnExperimentalModule(browser.NewSync(), "Please update your imports to use k6/browser instead of k6/experimental/browser,"+ " which will be removed after September 23rd, 2024 (v0.54.0). Ensure your scripts are migrated by then."+ diff --git a/js/modules/k6/experimental/tracing/client.go b/js/modules/k6/experimental/tracing/client.go deleted file mode 100644 index 99f81cd92b7..00000000000 --- a/js/modules/k6/experimental/tracing/client.go +++ /dev/null @@ -1,287 +0,0 @@ -package tracing - -import ( - "fmt" - "math/rand" - "net/http" - "time" - - "github.com/grafana/sobek" - "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" -) - -// Client represents a HTTP Client instrumenting the requests -// it performs with tracing information. -type Client struct { - vu modules.VU - - // opts holds the client's configuration options. - opts options - - // propagator holds the client's trace propagator, used - // to produce trace context headers for each supported - // formats: w3c, b3, jaeger. - propagator Propagator - - // requestFunc holds the http module's request function - // used to emit HTTP requests in k6 script. The client - // 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 - - // randSource holds the client's random source, used - // to generate random values for the trace ID. - randSource *rand.Rand -} - -type ( - // HTTPRequestFunc is a type alias representing the prototype of - // k6's http module's request function - HTTPRequestFunc func(method string, url sobek.Value, args ...sobek.Value) (*httpmodule.Response, error) - - // HTTPAsyncRequestFunc is a type alias representing the prototype of - // k6's http module's asyncRequest function - HTTPAsyncRequestFunc func(method string, url sobek.Value, args ...sobek.Value) (*sobek.Promise, error) -) - -// NewClient instantiates a new tracing Client -func NewClient(vu modules.VU, opts options) (*Client, error) { - rt := vu.Runtime() - - // Get the http module - httpModule, err := rt.RunString("require('k6/http')") - if err != nil { - 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 sobek.Callable as a Go function - var requestFunc HTTPRequestFunc - 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 sobek.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, - asyncRequestFunc: asyncRequestFunc, - randSource: rand.New(rand.NewSource(time.Now().UnixNano())), //nolint:gosec - } - - if err := client.Configure(opts); err != nil { - return nil, - fmt.Errorf("failed initializing tracing client, invalid configuration; reason: %w", err) - } - - return client, nil -} - -// Configure configures the tracing client with the given options. -func (c *Client) Configure(opts options) error { - if err := opts.validate(); err != nil { - return fmt.Errorf("invalid options: %w", err) - } - - var sampler Sampler = NewAlwaysOnSampler() - if opts.Sampling != 1.0 { - sampler = NewProbabilisticSampler(opts.Sampling) - } - - switch opts.Propagator { - case "w3c": - c.propagator = NewW3CPropagator(sampler) - case "jaeger": - c.propagator = NewJaegerPropagator(sampler) - default: - return fmt.Errorf("unknown propagator: %s", opts.Propagator) - } - - c.opts = opts - - 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 sobek.Value, args ...sobek.Value) (*httpmodule.Response, error) { - var result *httpmodule.Response - - var err error - err = c.instrumentedCall(func(args ...sobek.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 sobek.Value, args ...sobek.Value) (*sobek.Promise, error) { - var result *sobek.Promise - var err error - err = c.instrumentedCall(func(args ...sobek.Value) error { - result, err = c.asyncRequestFunc(method, url, args...) - return err - }, args...) - if err != nil { - return nil, err - } - return result, nil -} - -// Del instruments the http module's delete method. -func (c *Client) Del(url sobek.Value, args ...sobek.Value) (*httpmodule.Response, error) { - return c.Request(http.MethodDelete, url, args...) -} - -// Get instruments the http module's get method. -func (c *Client) Get(url sobek.Value, args ...sobek.Value) (*httpmodule.Response, error) { - // Here we prepend a null value that stands for the body parameter, - // that the request function expects as a first argument implicitly - args = append([]sobek.Value{sobek.Null()}, args...) - return c.Request(http.MethodGet, url, args...) -} - -// Head instruments the http module's head method. -func (c *Client) Head(url sobek.Value, args ...sobek.Value) (*httpmodule.Response, error) { - // NB: here we prepend a null value that stands for the body parameter, - // that the request function expects as a first argument implicitly - args = append([]sobek.Value{sobek.Null()}, args...) - return c.Request(http.MethodHead, url, args...) -} - -// Options instruments the http module's options method. -func (c *Client) Options(url sobek.Value, args ...sobek.Value) (*httpmodule.Response, error) { - return c.Request(http.MethodOptions, url, args...) -} - -// Patch instruments the http module's patch method. -func (c *Client) Patch(url sobek.Value, args ...sobek.Value) (*httpmodule.Response, error) { - return c.Request(http.MethodPatch, url, args...) -} - -// Post instruments the http module's post method. -func (c *Client) Post(url sobek.Value, args ...sobek.Value) (*httpmodule.Response, error) { - return c.Request(http.MethodPost, url, args...) -} - -// Put instruments the http module's put method. -func (c *Client) Put(url sobek.Value, args ...sobek.Value) (*httpmodule.Response, error) { - return c.Request(http.MethodPut, url, args...) -} - -func (c *Client) instrumentedCall(call func(args ...sobek.Value) error, args ...sobek.Value) error { - if len(args) == 0 { - args = []sobek.Value{sobek.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 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) - }) - - // 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...) -} - -func (c *Client) generateTraceContext() (http.Header, string, error) { - traceID, err := newTraceID(k6Prefix, k6CloudCode, time.Now(), c.randSource) - if err != nil { - return http.Header{}, "", fmt.Errorf("failed to generate trace ID; reason: %w", err) - } - - // Produce a trace header in the format defined by the configured propagator. - traceContextHeader, err := c.propagator.Propagate(traceID) - if err != nil { - return http.Header{}, "", fmt.Errorf("failed to propagate trace ID; reason: %w", err) - } - - return traceContextHeader, traceID, nil -} - -// instrumentArguments: expects args to be in the format expected by the -// request method (body, params) -func (c *Client) instrumentArguments(traceContext http.Header, args ...sobek.Value) ([]sobek.Value, error) { - rt := c.vu.Runtime() - - var paramsObj *sobek.Object - - switch len(args) { - case 2: - // We received both a body and a params argument. In the - // event params would be nullish, we'll instantiate - // a new object. - if common.IsNullish(args[1]) { - paramsObj = rt.NewObject() - args[1] = paramsObj - } else { - paramsObj = args[1].ToObject(rt) - } - case 1: - // We only received a body argument - paramsObj = rt.NewObject() - args = append(args, paramsObj) - default: - return nil, fmt.Errorf("invalid number of arguments; expected 1 or 2, got %d", len(args)) - } - - headersObj := rt.NewObject() - - headersValue := paramsObj.Get("headers") - if !common.IsNullish(headersValue) { - headersObj = headersValue.ToObject(rt) - } - - if err := paramsObj.Set("headers", headersObj); err != nil { - return args, err - } - - for key, value := range traceContext { - if err := headersObj.Set(key, value); err != nil { - return args, fmt.Errorf("failed to set the trace header; reason: %w", err) - } - } - - return args, nil -} diff --git a/js/modules/k6/experimental/tracing/client_test.go b/js/modules/k6/experimental/tracing/client_test.go deleted file mode 100644 index 574e1c3d116..00000000000 --- a/js/modules/k6/experimental/tracing/client_test.go +++ /dev/null @@ -1,295 +0,0 @@ -package tracing - -import ( - "math/rand" - "net/http" - "testing" - - "github.com/grafana/sobek" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.k6.io/k6/js/modulestest" - "go.k6.io/k6/lib" - "go.k6.io/k6/lib/testutils/httpmultibin" - "go.k6.io/k6/metrics" -) - -// traceParentHeaderName is the normalized trace header name. -// Although the traceparent header is case insensitive, the -// Go http.Header sets it capitalized. -const traceparentHeaderName string = "Traceparent" - -// testTraceID is a valid trace ID used in tests. -const testTraceID string = "00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-00" - -// testMetadataTraceIDRandomness is the randomness part of the test trace ID encoded -// as hexadecimal. -// -// It is used to test the randomness of the trace ID. As we use a fixed time -// as the test setup's randomness source, we can can assert that the randomness -// part of the trace ID is the same. -// -// Although the randomness part doesn't have a fixed size. We can assume that -// it will be 4 bytes, as the Go time.Time is encoded as nanoseconds, and -// the trace ID is encoded as 8 bytes. -const testMetadataTraceIDRandomness string = "0194fdc2" - -// testTracePrefix is the prefix of the test trace ID encoded as hexadecimal. -// It is equivalent to the first 2 bytes of the trace ID, which is always -// set to `k6Prefix` in the context of tests. -const testTracePrefix string = "dc07" - -// testTraceCode is the code of the test trace ID encoded as hexadecimal. -// It is equivalent to the third byte of the trace ID, which is always set to `k6CloudCode` -// in the context of tests. -const testTraceCode string = "18" - -func TestClientInstrumentArguments(t *testing.T) { - t.Parallel() - - t.Run("no args should fail", func(t *testing.T) { - t.Parallel() - - testCase := newTestCase(t) - - _, err := testCase.client.instrumentArguments(testCase.traceContextHeader) - require.Error(t, err) - }) - - t.Run("1 arg should initialize params successfully", func(t *testing.T) { - t.Parallel() - - testCase := newTestCase(t) - rt := testCase.testSetup.VU.Runtime() - - gotArgs, gotErr := testCase.client.instrumentArguments(testCase.traceContextHeader, sobek.Null()) - - assert.NoError(t, gotErr) - assert.Len(t, gotArgs, 2) - assert.Equal(t, sobek.Null(), gotArgs[0]) - - gotParams := gotArgs[1].ToObject(rt) - assert.NotNil(t, gotParams) - gotHeaders := gotParams.Get("headers").ToObject(rt) - assert.NotNil(t, gotHeaders) - gotTraceParent := gotHeaders.Get(traceparentHeaderName) - assert.NotNil(t, gotTraceParent) - assert.Equal(t, testTraceID, gotTraceParent.String()) - }) - - t.Run("2 args with null params should initialize it", func(t *testing.T) { - t.Parallel() - - testCase := newTestCase(t) - rt := testCase.testSetup.VU.Runtime() - - gotArgs, gotErr := testCase.client.instrumentArguments(testCase.traceContextHeader, sobek.Null(), sobek.Null()) - - assert.NoError(t, gotErr) - assert.Len(t, gotArgs, 2) - assert.Equal(t, sobek.Null(), gotArgs[0]) - - gotParams := gotArgs[1].ToObject(rt) - assert.NotNil(t, gotParams) - gotHeaders := gotParams.Get("headers").ToObject(rt) - assert.NotNil(t, gotHeaders) - gotTraceParent := gotHeaders.Get(traceparentHeaderName) - assert.NotNil(t, gotTraceParent) - assert.Equal(t, testTraceID, gotTraceParent.String()) - }) - - t.Run("2 args with undefined params should initialize it", func(t *testing.T) { - t.Parallel() - - testCase := newTestCase(t) - rt := testCase.testSetup.VU.Runtime() - - gotArgs, gotErr := testCase.client.instrumentArguments(testCase.traceContextHeader, sobek.Null(), sobek.Undefined()) - - assert.NoError(t, gotErr) - assert.Len(t, gotArgs, 2) - assert.Equal(t, sobek.Null(), gotArgs[0]) - - gotParams := gotArgs[1].ToObject(rt) - assert.NotNil(t, gotParams) - gotHeaders := gotParams.Get("headers").ToObject(rt) - assert.NotNil(t, gotHeaders) - gotTraceParent := gotHeaders.Get(traceparentHeaderName) - assert.NotNil(t, gotTraceParent) - assert.Equal(t, testTraceID, gotTraceParent.String()) - }) - - t.Run("2 args with predefined params and headers updates them successfully", func(t *testing.T) { - t.Parallel() - - testCase := newTestCase(t) - rt := testCase.testSetup.VU.Runtime() - - wantHeaders := rt.NewObject() - require.NoError(t, wantHeaders.Set("X-Test-Header", "testvalue")) - wantParams := rt.NewObject() - require.NoError(t, wantParams.Set("headers", wantHeaders)) - - gotArgs, gotErr := testCase.client.instrumentArguments(testCase.traceContextHeader, sobek.Null(), wantParams) - - assert.NoError(t, gotErr) - assert.Len(t, gotArgs, 2) - assert.Equal(t, sobek.Null(), gotArgs[0]) - assert.Equal(t, wantParams, gotArgs[1]) - - gotHeaders := gotArgs[1].ToObject(rt).Get("headers").ToObject(rt) - - gotTraceParent := gotHeaders.Get(traceparentHeaderName) - assert.NotNil(t, gotTraceParent) - assert.Equal(t, testTraceID, gotTraceParent.String()) - - gotTestHeader := gotHeaders.Get("X-Test-Header") - assert.NotNil(t, gotTestHeader) - assert.Equal(t, "testvalue", gotTestHeader.String()) - }) - - t.Run("2 args with predefined params and no headers sets and updates them successfully", func(t *testing.T) { - t.Parallel() - - testCase := newTestCase(t) - rt := testCase.testSetup.VU.Runtime() - wantParams := rt.NewObject() - - gotArgs, gotErr := testCase.client.instrumentArguments(testCase.traceContextHeader, sobek.Null(), wantParams) - - assert.NoError(t, gotErr) - assert.Len(t, gotArgs, 2) - assert.Equal(t, sobek.Null(), gotArgs[0]) - assert.Equal(t, wantParams, gotArgs[1]) - - gotHeaders := gotArgs[1].ToObject(rt).Get("headers").ToObject(rt) - - gotTraceParent := gotHeaders.Get(traceparentHeaderName) - assert.NotNil(t, gotTraceParent) - assert.Equal(t, testTraceID, gotTraceParent.String()) - }) -} - -func TestClientInstrumentedCall(t *testing.T) { - t.Parallel() - - testCase := newTestCase(t) - testCase.testSetup.MoveToVUContext(&lib.State{ - Tags: lib.NewVUStateTags(&metrics.TagSet{}), - }) - testCase.client.propagator = NewW3CPropagator(NewAlwaysOnSampler()) - - callFn := func(_ ...sobek.Value) error { - gotMetadataTraceID, gotTraceIDKey := testCase.client.vu.State().Tags.GetCurrentValues().Metadata["trace_id"] - assert.True(t, gotTraceIDKey) - assert.NotEmpty(t, gotMetadataTraceID) - assert.Equal(t, testTracePrefix, gotMetadataTraceID[:len(testTracePrefix)]) - assert.Equal(t, testTraceCode, gotMetadataTraceID[len(testTracePrefix):len(testTracePrefix)+len(testTraceCode)]) - assert.Equal(t, testMetadataTraceIDRandomness, gotMetadataTraceID[len(gotMetadataTraceID)-len(testMetadataTraceIDRandomness):]) - - return nil - } - - // Assert there is no trace_id key in vu metadata before using intrumentedCall - _, hasTraceIDKey := testCase.client.vu.State().Tags.GetCurrentValues().Metadata["trace_id"] - assert.False(t, hasTraceIDKey) - - // The callFn will assert that the trace_id key is present in vu metadata - // before returning - _ = testCase.client.instrumentedCall(callFn) - - // Assert there is no trace_id key in vu metadata after using intrumentedCall - _, hasTraceIDKey = testCase.client.vu.State().Tags.GetCurrentValues().Metadata["trace_id"] - assert.False(t, hasTraceIDKey) -} - -// This test ensures that the trace_id is added to the vu metadata when -// and instrumented request is called; and that we can find it in the -// produced samples. -// -// It also ensures that the trace_id is removed from the vu metadata -// after the request is done. -func TestCallingInstrumentedRequestEmitsTraceIdMetadata(t *testing.T) { - t.Parallel() - - testCase := newTestSetup(t) - rt := testCase.TestRuntime.VU.Runtime() - - // Making sure the instrumentHTTP is called in the init context - // before the test. - _, err := rt.RunString(` - let http = require('k6/http') - instrumentHTTP({propagator: 'w3c'}) - `) - require.NoError(t, err) - - // Move to VU context. Setup in way that the produced samples are - // written to a channel we own. - samples := make(chan metrics.SampleContainer, 1000) - httpBin := httpmultibin.NewHTTPMultiBin(t) - testCase.TestRuntime.MoveToVUContext(&lib.State{ - BuiltinMetrics: metrics.RegisterBuiltinMetrics(testCase.TestRuntime.VU.InitEnvField.Registry), - Tags: lib.NewVUStateTags(testCase.TestRuntime.VU.InitEnvField.Registry.RootTagSet()), - Transport: httpBin.HTTPTransport, - BufferPool: lib.NewBufferPool(), - Samples: samples, - Options: lib.Options{SystemTags: &metrics.DefaultSystemTagSet}, - }) - - // Inject a function in the JS runtime to assert the trace_id key - // is present in the vu metadata. - err = rt.Set("assert_has_trace_id_metadata", func(expected bool, expectedTraceID string) { - gotTraceID, hasTraceID := testCase.TestRuntime.VU.State().Tags.GetCurrentValues().Metadata["trace_id"] - require.Equal(t, expected, hasTraceID) - - if expectedTraceID != "" { - assert.Equal(t, testTracePrefix, gotTraceID[:len(testTracePrefix)]) - assert.Equal(t, testTraceCode, gotTraceID[len(testTracePrefix):len(testTracePrefix)+len(testTraceCode)]) - assert.Equal(t, testMetadataTraceIDRandomness, gotTraceID[len(gotTraceID)-len(testMetadataTraceIDRandomness):]) - } - }) - require.NoError(t, err) - - // Assert there is no trace_id key in vu metadata before calling an instrumented - // function, and that it's cleaned up after the call. - t.Cleanup(testCase.TestRuntime.EventLoop.WaitOnRegistered) - _, err = testCase.TestRuntime.RunOnEventLoop(httpBin.Replacer.Replace(` - assert_has_trace_id_metadata(false) - http.request("GET", "HTTPBIN_URL") - assert_has_trace_id_metadata(false) - `)) - require.NoError(t, err) - close(samples) - - var sampleRead bool - for sampleContainer := range samples { - for _, sample := range sampleContainer.GetSamples() { - require.NotEmpty(t, sample.Metadata["trace_id"]) - sampleRead = true - } - } - require.True(t, sampleRead) -} - -type tracingClientTestCase struct { - t *testing.T - testSetup *modulestest.Runtime - client Client - traceContextHeader http.Header -} - -func newTestCase(t *testing.T) *tracingClientTestCase { - testSetup := modulestest.NewRuntime(t) - // Here we provide the client with a fixed seed to ensure that the - // generated trace IDs random part is deterministic. - client := Client{vu: testSetup.VU, randSource: rand.New(rand.NewSource(0))} //nolint:gosec - traceContextHeader := http.Header{} - traceContextHeader.Add(traceparentHeaderName, testTraceID) - - return &tracingClientTestCase{ - t: t, - testSetup: testSetup, - client: client, - traceContextHeader: traceContextHeader, - } -} diff --git a/js/modules/k6/experimental/tracing/encoding.go b/js/modules/k6/experimental/tracing/encoding.go deleted file mode 100644 index 7c51087a557..00000000000 --- a/js/modules/k6/experimental/tracing/encoding.go +++ /dev/null @@ -1,38 +0,0 @@ -package tracing - -import ( - "math/rand" -) - -// randHexString returns a random string of n hex characters. -// -// Note that this function uses a non-cryptographic random number generator. -func randHexString(n int) string { - hexRunes := []rune("123456789abcdef") - - b := make([]rune, n) - for i := range b { - b[i] = hexRunes[rand.Intn(len(hexRunes))] //nolint:gosec - } - - return string(b) -} - -// chance returns true with a `percentage` chance, otherwise false. -// the `percentage` argument is expected to be -// within 0 <= percentage <= 100 range. -// -// The chance function works under the assumption that the -// go rand module has been seeded with a non-deterministic -// value. -func chance(r *rand.Rand, percentage float64) bool { - if percentage == 0.0 { - return false - } - - if percentage == 1.0 { - return true - } - - return r.Float64() < percentage -} diff --git a/js/modules/k6/experimental/tracing/module.go b/js/modules/k6/experimental/tracing/module.go deleted file mode 100644 index f0dbcb1c39c..00000000000 --- a/js/modules/k6/experimental/tracing/module.go +++ /dev/null @@ -1,166 +0,0 @@ -// Package tracing implements a k6 JS module for instrumenting k6 scripts with tracing context information. -package tracing - -import ( - "errors" - "fmt" - "math/rand" - "time" - - "github.com/grafana/sobek" - "go.k6.io/k6/js/common" - "go.k6.io/k6/js/modules" -) - -type ( - // RootModule is the global module instance that will create Client - // instances for each VU. - RootModule struct{} - - // ModuleInstance represents an instance of the JS module. - ModuleInstance struct { - vu modules.VU - - // random is a random number generator used by the module. - random *rand.Rand - - // Client holds the module's default tracing client. - *Client - } -) - -// Ensure the interfaces are implemented correctly -var ( - _ modules.Instance = &ModuleInstance{} - _ modules.Module = &RootModule{} -) - -// New returns a pointer to a new RootModule instance -func New() *RootModule { - return &RootModule{} -} - -// NewModuleInstance implements the modules.Module interface and returns -// a new instance for each VU. -func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance { - return &ModuleInstance{ - vu: vu, - - // Seed the random number generator with the current time. - // This ensures that any call to rand.Intn() will return - // less-deterministic results. - //nolint:gosec // we don't need cryptographic randomness here - random: rand.New(rand.NewSource(time.Now().UTC().UnixNano())), - } -} - -// Exports implements the modules.Instance interface and returns -// the exports of the JS module. -func (mi *ModuleInstance) Exports() modules.Exports { - return modules.Exports{ - Named: map[string]interface{}{ - "Client": mi.newClient, - "instrumentHTTP": mi.instrumentHTTP, - }, - } -} - -// 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 (mi *ModuleInstance) newClient(cc sobek.ConstructorCall) *sobek.Object { - rt := mi.vu.Runtime() - - if len(cc.Arguments) < 1 { - common.Throw(rt, errors.New("Client constructor expects a single configuration object as argument; none given")) - } - - opts, err := newOptions(rt, cc.Arguments[0]) - if err != nil { - common.Throw(rt, fmt.Errorf("unable to parse options object; reason: %w", err)) - } - - 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. -// -// When used in the context of a k6 script, it will automatically replace -// the imported http module's methods with instrumented ones. -func (mi *ModuleInstance) instrumentHTTP(options sobek.Value) { - rt := mi.vu.Runtime() - - if mi.vu.State() != nil { - common.Throw(rt, common.NewInitContextError("tracing module's instrumentHTTP can only be called in the init context")) - } - - if mi.Client != nil { - err := errors.New( - "tracing module's instrumentHTTP can only be called once. " + - "if you were attempting to reconfigure the instrumentation, " + - "please consider using the tracing.Client instead", - ) - common.Throw(rt, err) - } - - // Parse the options instance from the JS value. - // This will also validate the options, and set the sampling - // rate to 1.0 if the option was not set. - opts, err := newOptions(rt, options) - if err != nil { - common.Throw(rt, fmt.Errorf("unable to parse options object; reason: %w", err)) - } - - // Initialize the tracing module's instance default client, - // and configure it using the user-supplied set of options. - mi.Client, err = NewClient(mi.vu, opts) - 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 - // with instrumented ones. - httpModuleValue, err := rt.RunString(`require('k6/http')`) - if err != nil { - common.Throw(rt, err) - } - - httpModuleObj := httpModuleValue.ToObject(rt) - - // Closure overriding a method of the provided imported module object. - // - // The `onModule` argument should be a *sobek.Object obtained by requiring - // or importing the 'k6/http' module and converting it to an object. - // - // The `value` argument is expected to be callable. - mustSetHTTPMethod := func(method string, onModule *sobek.Object, value interface{}) { - // Inject the new get function, adding tracing headers - // to the request in the HTTP module object. - err = onModule.Set(method, value) - if err != nil { - common.Throw( - rt, - fmt.Errorf("unable to overwrite http.%s method with instrumented one; reason: %w", method, err), - ) - } - } - - // Overwrite the implementation of the http module's method with the instrumented - // ones exposed by the `tracing.Client` struct. - mustSetHTTPMethod("del", httpModuleObj, mi.Client.Del) - mustSetHTTPMethod("get", httpModuleObj, mi.Client.Get) - mustSetHTTPMethod("head", httpModuleObj, mi.Client.Head) - mustSetHTTPMethod("options", httpModuleObj, mi.Client.Options) - mustSetHTTPMethod("patch", httpModuleObj, mi.Client.Patch) - mustSetHTTPMethod("post", httpModuleObj, mi.Client.Post) - mustSetHTTPMethod("put", httpModuleObj, mi.Client.Put) - mustSetHTTPMethod("request", httpModuleObj, mi.Client.Request) - mustSetHTTPMethod("asyncRequest", httpModuleObj, mi.Client.AsyncRequest) -} diff --git a/js/modules/k6/experimental/tracing/module_test.go b/js/modules/k6/experimental/tracing/module_test.go deleted file mode 100644 index 540d29c9039..00000000000 --- a/js/modules/k6/experimental/tracing/module_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package tracing - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "go.k6.io/k6/js/compiler" - "go.k6.io/k6/js/modules/k6/http" - "go.k6.io/k6/js/modulestest" - "go.k6.io/k6/lib" -) - -func TestInstrumentHTTP_SucceedsInInitContext(t *testing.T) { - t.Parallel() - - ts := newTestSetup(t) - - // Calling in the init context should succeed - _, err := ts.TestRuntime.VU.Runtime().RunString(` - instrumentHTTP({propagator: 'w3c'}) - `) - - assert.NoError(t, err) -} - -func TestInstrumentHTTP_FailsWhenCalledTwice(t *testing.T) { - t.Parallel() - - ts := newTestSetup(t) - - // Calling it twice in the init context should fail - _, err := ts.TestRuntime.VU.Runtime().RunString(` - instrumentHTTP({propagator: 'w3c'}) - instrumentHTTP({propagator: 'w3c'}) - `) - - assert.Error(t, err) -} - -func TestInstrumentHTTP_FailsInVUContext(t *testing.T) { - t.Parallel() - - ts := newTestSetup(t) - ts.TestRuntime.MoveToVUContext(&lib.State{}) - - // Calling in the VU context should fail - _, err := ts.TestRuntime.VU.Runtime().RunString(` - instrumentHTTP({propagator: 'w3c'}) - `) - - assert.Error(t, err) -} - -type testSetup struct { - t *testing.T - TestRuntime *modulestest.Runtime -} - -func newTestSetup(t *testing.T) testSetup { - ts := modulestest.NewRuntime(t) - err := ts.SetupModuleSystem(map[string]interface{}{ - "k6/http": http.New(), - "k6/experimental/tracing": new(RootModule), - }, nil, compiler.New(ts.VU.InitEnvField.Logger)) - require.NoError(t, err) - - _, err = ts.VU.Runtime().RunString("var instrumentHTTP = require('k6/experimental/tracing').instrumentHTTP") - require.NoError(t, err) - - return testSetup{ - t: t, - TestRuntime: ts, - } -} diff --git a/js/modules/k6/experimental/tracing/options.go b/js/modules/k6/experimental/tracing/options.go deleted file mode 100644 index a65de41d189..00000000000 --- a/js/modules/k6/experimental/tracing/options.go +++ /dev/null @@ -1,67 +0,0 @@ -package tracing - -import ( - "errors" - "fmt" - - "github.com/grafana/sobek" - "go.k6.io/k6/js/common" -) - -// options are the options that can be passed to the -// tracing.instrumentHTTP() method. -type options struct { - // Propagation is the propagation format to use for the tracer. - Propagator string `json:"propagator"` - - // Sampling is the sampling rate to use for the - // tracer, expressed in percents within the - // bounds: 0.0 <= n <= 1.0. - Sampling float64 `json:"sampling"` - - // Baggage is a map of baggage items to add to the tracer. - Baggage map[string]string `json:"baggage"` -} - -// defaultSamplingRate is the default sampling rate applied to options. -const defaultSamplingRate float64 = 1.0 - -// newOptions returns a new options object from the given sobek.Value. -// -// Note that if the sampling field value is absent, or nullish, we'll -// set it to the `defaultSamplingRate` value. -func newOptions(rt *sobek.Runtime, from sobek.Value) (options, error) { - var opts options - - if err := rt.ExportTo(from, &opts); err != nil { - return opts, fmt.Errorf("unable to parse options object; reason: %w", err) - } - - fromSamplingValue := from.ToObject(rt).Get("sampling") - if common.IsNullish(fromSamplingValue) { - opts.Sampling = defaultSamplingRate - } - - return opts, nil -} - -func (i *options) validate() error { - var ( - isW3C = i.Propagator == W3CPropagatorName - isJaeger = i.Propagator == JaegerPropagatorName - ) - if !isW3C && !isJaeger { - return fmt.Errorf("unknown propagator: %s", i.Propagator) - } - - if i.Sampling < 0.0 || i.Sampling > 1.0 { - return errors.New("sampling rate must be between 0.0 and 1.0") - } - - // TODO: implement baggage support - if i.Baggage != nil { - return errors.New("baggage is not yet supported") - } - - return nil -} diff --git a/js/modules/k6/experimental/tracing/options_test.go b/js/modules/k6/experimental/tracing/options_test.go deleted file mode 100644 index c608aa33dc7..00000000000 --- a/js/modules/k6/experimental/tracing/options_test.go +++ /dev/null @@ -1,177 +0,0 @@ -package tracing - -import ( - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewOptionsWithoutSamplingProperty(t *testing.T) { - t.Parallel() - - ts := newTestSetup(t) - - _, err := ts.TestRuntime.VU.Runtime().RunString(` - const options = { - propagator: 'w3c', - } - `) - require.NoError(t, err) - optionsValue := ts.TestRuntime.VU.Runtime().Get("options") - gotOptions, gotOptionsErr := newOptions(ts.TestRuntime.VU.Runtime(), optionsValue) - - assert.NoError(t, gotOptionsErr) - assert.Equal(t, 1.0, gotOptions.Sampling) -} - -func TestNewOptionsWithSamplingPropertySetToNullish(t *testing.T) { - t.Parallel() - - ts := newTestSetup(t) - - _, err := ts.TestRuntime.VU.Runtime().RunString(` - const options = { - propagator: 'w3c', - sampling: null, - } - `) - require.NoError(t, err) - optionsValue := ts.TestRuntime.VU.Runtime().Get("options") - gotOptions, gotOptionsErr := newOptions(ts.TestRuntime.VU.Runtime(), optionsValue) - - assert.NoError(t, gotOptionsErr) - assert.Equal(t, 1.0, gotOptions.Sampling) -} - -func TestNewOptionsWithSamplingPropertySet(t *testing.T) { - t.Parallel() - - ts := newTestSetup(t) - - _, err := ts.TestRuntime.VU.Runtime().RunString(` - const options = { - propagator: 'w3c', - sampling: 0.5, - } - `) - require.NoError(t, err) - optionsValue := ts.TestRuntime.VU.Runtime().Get("options") - gotOptions, gotOptionsErr := newOptions(ts.TestRuntime.VU.Runtime(), optionsValue) - - assert.NoError(t, gotOptionsErr) - assert.Equal(t, 0.5, gotOptions.Sampling) -} - -func TestOptionsValidate(t *testing.T) { - t.Parallel() - - // Note that we prefer variables over constants here - // as we need to be able to address them. - var ( - validSampling = 0.1 - lowerBoundSampling = 0.0 - upperBoundSampling = 1.0 - lowerOutOfBoundsSampling = -1.0 - upperOutOfBoundsSampling = 1.01 - ) - - type fields struct { - Propagator string - Sampling float64 - Baggage map[string]string - } - testCases := []struct { - name string - fields fields - wantErr bool - }{ - { - name: "w3c propagator is valid", - fields: fields{ - Propagator: "w3c", - }, - wantErr: false, - }, - { - name: "jaeger propagator is valid", - fields: fields{ - Propagator: "jaeger", - }, - wantErr: false, - }, - { - name: "invalid propagator is invalid", - fields: fields{ - Propagator: "invalid", - }, - wantErr: true, - }, - { - name: "sampling is not rate is valid", - fields: fields{ - Propagator: "w3c", - Sampling: validSampling, - }, - wantErr: false, - }, - { - name: "sampling rate = 0 is valid", - fields: fields{ - Propagator: "w3c", - Sampling: lowerBoundSampling, - }, - wantErr: false, - }, - { - name: "sampling rate = 1.0 is valid", - fields: fields{ - Propagator: "w3c", - Sampling: upperBoundSampling, - }, - wantErr: false, - }, - { - name: "sampling rate < 0 is invalid", - fields: fields{ - Propagator: "w3c", - Sampling: lowerOutOfBoundsSampling, - }, - wantErr: true, - }, - { - name: "sampling rater > 1.0 is invalid", - fields: fields{ - Propagator: "w3c", - Sampling: upperOutOfBoundsSampling, - }, - wantErr: true, - }, - { - name: "baggage is not yet supported", - fields: fields{ - Propagator: "w3c", - Baggage: map[string]string{"key": "value"}, - }, - wantErr: true, - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - i := &options{ - Propagator: tc.fields.Propagator, - Sampling: tc.fields.Sampling, - Baggage: tc.fields.Baggage, - } - - if err := i.validate(); (err != nil) != tc.wantErr { - t.Errorf("instrumentationOptions.validate() error = %v, wantErr %v", err, tc.wantErr) - } - }) - } -} diff --git a/js/modules/k6/experimental/tracing/propagator.go b/js/modules/k6/experimental/tracing/propagator.go deleted file mode 100644 index 3bacd128376..00000000000 --- a/js/modules/k6/experimental/tracing/propagator.go +++ /dev/null @@ -1,124 +0,0 @@ -package tracing - -import ( - "net/http" -) - -// Propagator is an interface for trace context propagation -type Propagator interface { - Propagate(traceID string) (http.Header, error) -} - -const ( - // W3CPropagatorName is the name of the W3C trace context propagator - W3CPropagatorName = "w3c" - - // W3CHeaderName is the name of the W3C trace context header - W3CHeaderName = "traceparent" - - // W3CVersion is the version of the supported W3C trace context header. - // The current specification assumes the version is set to 00. - W3CVersion = "00" - - // W3CUnsampledTraceFlag is the trace-flag value for an unsampled trace. - W3CUnsampledTraceFlag = "00" - - // W3CSampledTraceFlag is the trace-flag value for a sampled trace. - W3CSampledTraceFlag = "01" -) - -// W3CPropagator is a Propagator for the W3C trace context header -type W3CPropagator struct { - // Sampler is used to determine whether or not a trace should be sampled. - Sampler -} - -// NewW3CPropagator returns a new W3CPropagator using the provided sampler -// to base its sampling decision upon. -// -// Note that we allocate the propagator on the heap to ensure we conform -// to the Sampler interface, as the [Sampler.SetSamplingRate] -// method has a pointer receiver. -func NewW3CPropagator(s Sampler) *W3CPropagator { - return &W3CPropagator{ - Sampler: s, - } -} - -// Propagate returns a header with a random trace ID in the W3C format -func (p *W3CPropagator) Propagate(traceID string) (http.Header, error) { - parentID := randHexString(16) - flags := pick(p.ShouldSample(), W3CSampledTraceFlag, W3CUnsampledTraceFlag) - - return http.Header{ - W3CHeaderName: { - W3CVersion + "-" + traceID + "-" + parentID + "-" + flags, - }, - }, nil -} - -const ( - // JaegerPropagatorName is the name of the Jaeger trace context propagator - JaegerPropagatorName = "jaeger" - - // JaegerHeaderName is the name of the Jaeger trace context header - JaegerHeaderName = "uber-trace-id" - - // JaegerRootSpanID is the universal span ID of the root span. - // Its value is zero, which is described in the Jaeger documentation as: - // "0 value is valid and means “root span” (when not ignored)" - JaegerRootSpanID = "0" - - // JaegerUnsampledTraceFlag is the trace-flag value for an unsampled trace. - JaegerUnsampledTraceFlag = "0" - - // JaegerSampledTraceFlag is the trace-flag value for a sampled trace. - JaegerSampledTraceFlag = "1" -) - -// JaegerPropagator is a Propagator for the Jaeger trace context header -type JaegerPropagator struct { - // Sampler is used to determine whether or not a trace should be sampled. - Sampler -} - -// NewJaegerPropagator returns a new JaegerPropagator with the given sampler. -func NewJaegerPropagator(s Sampler) *JaegerPropagator { - return &JaegerPropagator{ - Sampler: s, - } -} - -// Propagate returns a header with a random trace ID in the Jaeger format -func (p *JaegerPropagator) Propagate(traceID string) (http.Header, error) { - spanID := randHexString(8) - flags := pick(p.ShouldSample(), JaegerSampledTraceFlag, JaegerUnsampledTraceFlag) - - return http.Header{ - JaegerHeaderName: {traceID + ":" + spanID + ":" + JaegerRootSpanID + ":" + flags}, - }, nil -} - -// Pick returns either the left or right value, depending on the value of the `decision` -// boolean value. -func pick[T any](decision bool, lhs, rhs T) T { - if decision { - return lhs - } - - return rhs -} - -var ( - // Ensures that W3CPropagator implements the Propagator interface - _ Propagator = &W3CPropagator{} - - // Ensures that W3CPropagator implements the Sampler interface - _ Sampler = &W3CPropagator{} - - // Ensures the JaegerPropagator implements the Propagator interface - _ Propagator = &JaegerPropagator{} - - // Ensures the JaegerPropagator implements the Sampler interface - _ Sampler = &JaegerPropagator{} -) diff --git a/js/modules/k6/experimental/tracing/propagator_test.go b/js/modules/k6/experimental/tracing/propagator_test.go deleted file mode 100644 index ebf795b5816..00000000000 --- a/js/modules/k6/experimental/tracing/propagator_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package tracing - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestPropagate(t *testing.T) { - t.Parallel() - - traceID := "abc123" - - t.Run("W3C Propagator", func(t *testing.T) { - t.Parallel() - - sampler := mockSampler{decision: true} - propagator := NewW3CPropagator(sampler) - - gotHeader, gotErr := propagator.Propagate(traceID) - - assert.NoError(t, gotErr) - assert.Contains(t, gotHeader, W3CHeaderName) - - //nolint:staticcheck // as traceparent is not a canonical header - headerContent := gotHeader[W3CHeaderName][0] - assert.True(t, strings.HasPrefix(headerContent, W3CVersion+"-"+traceID+"-")) - }) - - t.Run("W3C propagator with sampled trace", func(t *testing.T) { - t.Parallel() - - sampler := mockSampler{decision: true} - propagator := NewW3CPropagator(sampler) - - gotHeader, gotErr := propagator.Propagate(traceID) - require.NoError(t, gotErr) - require.Contains(t, gotHeader, W3CHeaderName) - - //nolint:staticcheck // as traceparent is not a canonical header - assert.True(t, strings.HasSuffix(gotHeader[W3CHeaderName][0], "-01")) - }) - - t.Run("W3C propagator with unsampled trace", func(t *testing.T) { - t.Parallel() - - sampler := mockSampler{decision: false} - propagator := NewW3CPropagator(sampler) - - gotHeader, gotErr := propagator.Propagate(traceID) - require.NoError(t, gotErr) - require.Contains(t, gotHeader, W3CHeaderName) - - //nolint:staticcheck // as traceparent is not a canonical header - assert.True(t, strings.HasSuffix(gotHeader[W3CHeaderName][0], "-00")) - }) - - t.Run("Jaeger Propagator", func(t *testing.T) { - t.Parallel() - - sampler := mockSampler{decision: true} - propagator := NewJaegerPropagator(sampler) - - gotHeader, gotErr := propagator.Propagate(traceID) - - assert.NoError(t, gotErr) - assert.Contains(t, gotHeader, JaegerHeaderName) - - //nolint:staticcheck // as traceparent is not a canonical header - headerContent := gotHeader[JaegerHeaderName][0] - assert.True(t, strings.HasPrefix(headerContent, traceID+":")) - assert.True(t, strings.HasSuffix(headerContent, ":0:1")) - }) - - t.Run("Jaeger propagator with sampled trace", func(t *testing.T) { - t.Parallel() - - sampler := mockSampler{decision: true} - propagator := NewJaegerPropagator(sampler) - - gotHeader, gotErr := propagator.Propagate(traceID) - require.NoError(t, gotErr) - require.Contains(t, gotHeader, JaegerHeaderName) - - //nolint:staticcheck // as traceparent is not a canonical header - assert.True(t, strings.HasSuffix(gotHeader[JaegerHeaderName][0], ":1")) - }) - - t.Run("Jaeger propagator with unsampled trace", func(t *testing.T) { - t.Parallel() - - sampler := mockSampler{decision: false} - propagator := NewJaegerPropagator(sampler) - - gotHeader, gotErr := propagator.Propagate(traceID) - require.NoError(t, gotErr) - require.Contains(t, gotHeader, JaegerHeaderName) - - //nolint:staticcheck // as traceparent is not a canonical header - assert.True(t, strings.HasSuffix(gotHeader[JaegerHeaderName][0], ":0")) - }) -} - -type mockSampler struct { - decision bool -} - -func (m mockSampler) ShouldSample() bool { - return m.decision -} diff --git a/js/modules/k6/experimental/tracing/sampling.go b/js/modules/k6/experimental/tracing/sampling.go deleted file mode 100644 index e3dbe641410..00000000000 --- a/js/modules/k6/experimental/tracing/sampling.go +++ /dev/null @@ -1,70 +0,0 @@ -package tracing - -import ( - "math/rand" -) - -// Sampler is an interface defining a sampling strategy. -type Sampler interface { - // ShouldSample returns true if the trace should be sampled - // false otherwise. - ShouldSample() bool -} - -// ProbabilisticSampler implements the ProbabilisticSampler interface and allows -// to take probabilistic sampling decisions based on a sampling rate. -type ProbabilisticSampler struct { - // random is a random number generator used by the sampler. - random *rand.Rand - - // samplingRate is a chance value defined as a percentage - // value within 0.0 <= samplingRate <= 1.0 bounds. - samplingRate float64 -} - -// NewProbabilisticSampler returns a new ProbablisticSampler with the provided sampling rate. -// -// Note that the sampling rate is a percentage value within 0.0 <= samplingRate <= 1.0 bounds. -// If the provided sampling rate is outside of this range, it will be clamped to the closest -// bound. -func NewProbabilisticSampler(samplingRate float64) *ProbabilisticSampler { - // Ensure that the sampling rate is within the 0.0 <= samplingRate <= 1.0 bounds. - if samplingRate < 0.0 { - samplingRate = 0.0 - } else if samplingRate > 1.0 { - samplingRate = 1.0 - } - - return &ProbabilisticSampler{samplingRate: samplingRate} -} - -// ShouldSample returns true if the trace should be sampled. -// -// Its return value is probabilistic, based on the selected -// sampling rate S, there is S percent chance that the -// returned value is true. -func (ps ProbabilisticSampler) ShouldSample() bool { - return chance(ps.random, ps.samplingRate) -} - -// Ensure that ProbabilisticSampler implements the Sampler interface. -var _ Sampler = &ProbabilisticSampler{} - -// AlwaysOnSampler implements the Sampler interface and allows to bypass -// sampling decisions by returning true for all Sampled() calls. -// -// This is useful in cases where the user either does not provide -// the sampling option, or set it to 100% as it will avoid any -// call to the random number generator. -type AlwaysOnSampler struct{} - -// NewAlwaysOnSampler returns a new AlwaysSampledSampler. -func NewAlwaysOnSampler() *AlwaysOnSampler { - return &AlwaysOnSampler{} -} - -// ShouldSample always returns true. -func (AlwaysOnSampler) ShouldSample() bool { return true } - -// Ensure that AlwaysOnSampler implements the Sampler interface. -var _ Sampler = &AlwaysOnSampler{} diff --git a/js/modules/k6/experimental/tracing/trace_id.go b/js/modules/k6/experimental/tracing/trace_id.go deleted file mode 100644 index 937988cceff..00000000000 --- a/js/modules/k6/experimental/tracing/trace_id.go +++ /dev/null @@ -1,66 +0,0 @@ -package tracing - -import ( - "encoding/binary" - "encoding/hex" - "fmt" - "io" - "time" -) - -const ( - // Being 075 the ASCII code for 'K' :) - k6Prefix = 0o756 - - // To ingest and process the related spans in k6 Cloud. - k6CloudCode = 12 - - // To not ingest and process the related spans, b/c they are part of a non-cloud run. - k6LocalCode = 33 - - // metadataTraceIDKeyName is the key name of the traceID in the output metadata. - metadataTraceIDKeyName = "trace_id" - - // traceIDEncodedSize is the size of the encoded traceID. - traceIDEncodedSize = 16 -) - -// newTraceID generates a new hexadecimal-encoded trace ID as defined by the [W3C specification]. -// -// `prefix` is the first 2 bytes of the trace ID, and is used to identify the -// vendor of the trace ID. `code` is the third byte of the trace ID, and is -// used to identify the type of the trace ID. `t` is the time at which the trace -// ID was generated. `randSource` is the source of randomness used to fill the rest -// of bytes of the trace ID. -// -// [W3C specification]: https://www.w3.org/TR/trace-context/#trace-id -func newTraceID(prefix int16, code int8, t time.Time, randSource io.Reader) (string, error) { - if prefix != k6Prefix { - return "", fmt.Errorf("invalid prefix 0o%o, expected 0o%o", prefix, k6Prefix) - } - - if (code != k6CloudCode) && (code != k6LocalCode) { - return "", fmt.Errorf("invalid code 0o%d, accepted values are 0o%d and 0o%d", code, k6CloudCode, k6LocalCode) - } - - // Encode The trace ID into a binary buffer. - buf := make([]byte, traceIDEncodedSize) - n := binary.PutVarint(buf, int64(prefix)) - n += binary.PutVarint(buf[n:], int64(code)) - n += binary.PutVarint(buf[n:], t.UnixNano()) - - // Calculate the number of random bytes needed. - randomBytesSize := traceIDEncodedSize - n - - // Generate the random bytes. - randomness := make([]byte, randomBytesSize) - err := binary.Read(randSource, binary.BigEndian, randomness) - if err != nil { - return "", fmt.Errorf("failed to generate random bytes from os; reason: %w", err) - } - - // Combine the values and random bytes to form the encoded trace ID buffer. - buf = append(buf[:n], randomness...) - - return hex.EncodeToString(buf), nil -} diff --git a/js/modules/k6/experimental/tracing/trace_id_test.go b/js/modules/k6/experimental/tracing/trace_id_test.go deleted file mode 100644 index e52f91d4d1e..00000000000 --- a/js/modules/k6/experimental/tracing/trace_id_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package tracing - -import ( - "bytes" - "io" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestNewTraceID(t *testing.T) { - t.Parallel() - - testTime := time.Date(2022, time.January, 1, 0, 0, 0, 0, time.UTC) - testRandSourceFn := func() io.Reader { return bytes.NewReader([]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}) } - - // Precomputed hexadecimal representation of the binary values - // of the traceID components. - wantPrefixHexString := "dc07" - wantCloudCodeHexString := "18" - wantLocalCodeHexString := "42" - wantTimeHexString := "8080f8e1949cfec52d" - wantRandHexString := "01020304" - - testCases := []struct { - name string - prefix int16 - code int8 - t time.Time - randSource io.Reader - wantErr bool - }{ - { - name: "valid traceID with cloud code should succeed", - prefix: k6Prefix, - code: k6CloudCode, - t: testTime, - randSource: testRandSourceFn(), - wantErr: false, - }, - { - name: "valid traceID with local code should succeed", - prefix: k6Prefix, - code: k6LocalCode, - t: testTime, - randSource: testRandSourceFn(), - wantErr: false, - }, - { - name: "traceID with invalid prefix should fail", - prefix: 0o123, - code: k6CloudCode, - t: testTime, - randSource: testRandSourceFn(), - wantErr: true, - }, - { - name: "traceID with invalid code should fail", - prefix: k6Prefix, - code: 0o123, - t: testTime, - randSource: testRandSourceFn(), - wantErr: true, - }, - } - - for _, tc := range testCases { - tc := tc - - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - gotTraceID, gotErr := newTraceID(tc.prefix, tc.code, tc.t, tc.randSource) - - if tc.wantErr { - require.Error(t, gotErr) - return - } - - prefixEndOffset := len(wantPrefixHexString) - assert.Equal(t, wantPrefixHexString, gotTraceID[:prefixEndOffset]) - - codeEndOffset := prefixEndOffset + len(wantCloudCodeHexString) - if tc.code == k6CloudCode { - assert.Equal(t, wantCloudCodeHexString, gotTraceID[prefixEndOffset:codeEndOffset]) - } else { - assert.Equal(t, wantLocalCodeHexString, gotTraceID[prefixEndOffset:codeEndOffset]) - } - - timeEndOffset := codeEndOffset + len(wantTimeHexString) - assert.Equal(t, wantTimeHexString, gotTraceID[codeEndOffset:timeEndOffset]) - - assert.Equal(t, wantRandHexString, gotTraceID[timeEndOffset:]) - }) - } -}