Skip to main content

Running Tracetest with K6

Tracetest is a testing tool based on OpenTelemetry that allows you to test your distributed application. It allows you to use data from distributed traces generated by OpenTelemetry to validate and assert if your application has the desired behavior defined by your test definitions.

K6 is a powerful tool to run load tests against any type of services (REST, GRPC, GraphQL, etc). It is widely used by Developers, Site Reliability Engineers and Software Engineers in Test / QA teams to find potential issues when testing real life scenarios in both controlled environments and production.

Why is this important?

K6 is it's a great tool in its own right that allows you to replicate most of the production challenges you might encounter. But, as with all of the tools that only test the initial transaction between the client side and the server, you can only run validations against the immediate response from the service.

The K6 Tracetest Extension

With the K6 Tracetest extension, you will unlock the power of OpenTelemetry that allows you to run deeper testing based on the traces and spans generated by each of the checkpoints that you define within your services.

Language and Vendor agnostic, with this extension you can use your existing Tracing Data Store and Setup to leverage the work you have already done to instrument your services.

How It Works

The following is high level sequence diagram on how K6 and Tracetest interact with the different pieces of the system.

Requirements

Trecetest Server: You'll ned to have a running instance of Tracetest which is going to be executing your tests and assertions.

To do so, you can follow the instructions defined in the documentation.

XK6: To start using the Tracetest k6 binary you need to have the xk6 builder installed on your machine. You can follow the instructions defined in this page to do that.

OpenTelemetry Instrumented Service: In order to generate traces and spans, the service under test must support the basics for propagation through HTTP requests as well as store traces and spans into a Data Store Backend (Jaeger, Grafana Tempo, OpenSearch, etc).

Installing the K6 Tracetest Extension

Installing the K6 Tracetest extension is as easy as running the following command:

xk6 build v0.42.0 --with github.com/kubeshop/xk6-tracetest

The instructions can be also found in the main k6 docs in case you need to combine multiple extensions into one binary.

Using the Tracetest Extension For Load Tests

Once you have installed the k6 Tracetest binary, you can use the base k6 functionality to run load tests against instrumented services and Tracetest to run checks against the resulting telemetry data.

Creating your Tracetest Test

The first step is to create the test that will be used to run the telemetry data validations. In this step, we will create the assertions that will be executed for each of the runs.

To do so, you can either use the UI or the CLI.

UI:

  1. Click the Create button from the Tracetest homepage (https://demo.tracetest.io/).
  2. Select TraceID as the trigger mode.
  3. Enter the basic details.
  4. Choose the name of the variable you'll be using for the trace id (e.g. TRACE_ID).
  5. Create and run the test with a random value.

CLI:

If you have the CLI configured, you can save the following test definition to a local .yaml file.

type: Test
spec:
id: kc_MgKoVR
name: K6
description: K6
trigger:
type: traceid
traceid:
id: ${var:TRACE_ID}

And create the test using the following command:

tracetest run test -f <name-of-the-file>

Extension Features

The extension includes a new package which includes classes for HTTP requests and the main Tracetest instance.

This can be imported as follows:

import { Http, Tracetest } from "k6/x/tracetest";

The HTTP Class: The HTTP class is the primary entry point to execute requests. It extends the default http interface by generating parent trace ids and sending them as part of the propagation headers to the request.

To create an HTTP instance and use it, you can follow the next code sample.

// Tracetest test identifier
const testId = "<your-test-id>;

// creating the main http instance
const http = new Http();

// adding configuration whe creating the instance
const http = new Http({
// supported propagators tracecontext, baggage, b3, ot, jaeger & xray
propagators: ['b3'],

// tracetest configuration
tracetest: {
// define the triggered test id
testId,
// used variable name to inject the trace id to the test
variableName: 'TRACE_ID',
},
});

const url = "http://localhost:8081/pokemon/import";
const pokemonId = 6; // charizad
const payload = JSON.stringify({
id: pokemonId,
});

// using the http instance to trigger requests
const response = http.post(url, payload, const params = {
headers: {
"Content-Type": "application/json",
},
};);

// overwriting the instance level config per request
const response = http.post(url, payload, {
// same as the http instance config
tracetest: {
testId,
},
// rest of the regular http options
headers: {
"Content-Type": "application/json",
},
});


// direct access to the trace id from the response object
console.log(response.trace_id)

The Tracetest Class: The Tracetest class is the main breach between the k6 load tests and the Tracetest API, used to run the tests, validate the results and generate the summary response.

To create a Tracetest instance and use it you can follow the next code sample.

const tracetest = Tracetest();

// adding your tracetest server configuration
const tracetest = Tracetest({
serverUrl: "http://localhost:11633",
serverPath: "",
});

// generating the summary
export function handleSummary(data) {
// combine the default summary with the tracetest summary
const tracetestSummary = tracetest.summary();
const defaultSummary = textSummary(data);
const summary = `
${defaultSummary}
${tracetestSummary}
`;

return {
stdout: summary,
// JSON summary object for CI/CD proposes
"tracetest.json": tracetest.json(),
};
}

// enable this to return a non-zero status code if a tracetest test fails
export function teardown() {
// returns a non-zero code when any of the tracetest test fails
tracetest.validateResult();
}

const metadata = {
id: "123",
url,
method: "GET",
};

// manually running a test
tracetest.runTest(response.trace_id, { testId }, metadata);

Using the Custom Output

The Tracetest K6 extension includes an output you can use to automatically trigger the test using the predefined configuration. In this case, you can avoid manually using:

tracetest.runTest(response.trace_id, { testId }, metadata);

And just focus on writing the HTTP triggers. To use this feature, you can use the following command:

k6 run <your-script>.js -o xk6-tracetest

Result Summary

The Javascript extension includes a feature that you can use to display the results from the Tracetest test runs execution.

You can add it to your script using the following:

// generating the summary
export function handleSummary(data) {
// combine the default summary with the tracetest summary
const tracetestSummary = tracetest.summary();
const defaultSummary = textSummary(data);
const summary = `
${defaultSummary}
${tracetestSummary}
`;

return {
stdout: summary,
// JSON summary object for CI/CD proposes
"tracetest.json": tracetest.json(),
};
}

This will generate an output like the following:


/\ |‾‾| /‾‾/ /‾‾/
/\ / \ | |/ / / /
/ \/ \ | ( / ‾‾\
/ \ | |\ \ | () |
/ __________ \ |__| \__\ \_____/ .io

execution: local
script: tracetest/examples/tracetest-k6/import-pokemon.js
output: xk6-tracetest-output (TestRunID: 93008)

scenarios: (100.00%) 1 scenario, 1 max VUs, 36s max duration (incl. graceful stop):
* default: 1 looping VUs for 6s (gracefulStop: 30s)

ERRO[0017] panic: Tracetest: 5 jobs failed

Goja stack:
native

running (17.1s), 0/1 VUs, 6 complete and 0 interrupted iterations
default ✓ [======================================] 1 VUs 6s

✓ is status 200
✓ body matches de id

█ teardown

checks.........................: 100.00% ✓ 120
data_received..................: 1.1 kB 67 B/s
data_sent......................: 3.3 kB 190 B/s
http_req_blocked...............: avg=89µs min=3µs med=12.5µs max=476µs p(90)=249µs p(95)=362.49µs
http_req_connecting............: avg=37µs min=0s med=0s max=222µs p(90)=111µs p(95)=166.49µs
http_req_duration..............: avg=4.83ms min=1.86ms med=5.35ms max=7.61ms p(90)=6.77ms p(95)=7.19ms
{ expected_response:true }...: avg=4.83ms min=1.86ms med=5.35ms max=7.61ms p(90)=6.77ms p(95)=7.19ms
http_req_failed................: 0.00% ✓ 06
http_req_receiving.............: avg=51µs min=32µs med=52.5µs max=74µs p(90)=68µs p(95)=71µs
http_req_sending...............: avg=47.83µs min=17µs med=47µs max=88µs p(90)=71µs p(95)=79.49µs
http_req_tls_handshaking.......: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
http_req_waiting...............: avg=4.74ms min=1.75ms med=5.23ms max=7.56ms p(90)=6.69ms p(95)=7.12ms
http_reqs......................: 6 0.350387/s
iteration_duration.............: avg=2.44s min=1s med=1s max=11.08s p(90)=5.03s p(95)=8.06s
iterations.....................: 6 0.350387/s
vus............................: 0 min=0 max=1
vus_max........................: 1 min=1 max=1
[TotalRuns=6, SuccessfulRus=1, FailedRuns=5]
[FAILED]
[Request=POST - http://localhost:8081/pokemon/import, TraceID=dc071893eaaca9de301f2147e2be372e, RunState=FINISHED FailingSpecs=true, TracetestURL= http://localhost:3000/test/kc_MgKoVR/run/272]
[Request=POST - http://localhost:8081/pokemon/import, TraceID=dc0718fff1aca9de30b702c3a1bfad75, RunState=FINISHED FailingSpecs=true, TracetestURL= http://localhost:3000/test/kc_MgKoVR/run/275]
[Request=POST - http://localhost:8081/pokemon/import, TraceID=dc0718b8daaca9de301e39889afca15b, RunState=FINISHED FailingSpecs=true, TracetestURL= http://localhost:3000/test/kc_MgKoVR/run/276]
[Request=POST - http://localhost:8081/pokemon/import, TraceID=dc0718a7e2aca9de30955b5203b162a7, RunState=FINISHED FailingSpecs=true, TracetestURL= http://localhost:3000/test/kc_MgKoVR/run/273]
[Request=POST - http://localhost:8081/pokemon/import, TraceID=dc0718edf9aca9de305916d7b1e7814c, RunState=FINISHED FailingSpecs=true, TracetestURL= http://localhost:3000/test/kc_MgKoVR/run/274]
[SUCCESSFUL]
[Request=POST - http://localhost:8081/pokemon/import, TraceID=dc0718c9d2aca9de3044a794f7248eab, RunState=FINISHED FailingSpecs=false, TracetestURL= http://localhost:3000/test/kc_MgKoVR/run/271]

This identifies the test that failed and the status and links to look at the results directly from the UI.

Non-Zero Status

An important use case is to integrate the extension with your CI/CD tools. In this case, the Tracetest class allows you to validate the results during the k6 teardown lifecycle event which returns a failed status if any of the test run fails. A failed test can be defined as:

  • The trace could not be fetched from the data store.
  • Any of the test specs failed.
  • Could not connect to the Tracetest API.
  • An execution error from the Tracetest Server side.

To enable this, you can simply add the following to your test script:

// enable this to return a non-zero status code if a tracetest test fails
export function teardown() {
tracetest.validateResult();
}

Configuration and Environment Variables

If you want to reuse your test scripts for multiple environments and to avoid hardcoding values like the server URL and/or path, use the following configuration variables to achieve this:

XK6_TRACETEST_SERVER_URL: Allows you to set the server url for the entire test script.

Usage:

XK6_TRACETEST_SERVER_URL=<your-server-url> k6 run <your-script>.js -o xk6-tracetest

You can also define the global server url by passing it as an output parameter:

k6 run <your-script>.js -o xk6-tracetest=<your-server-url>

XK6_TRACETEST_SERVER_PATH: Allows you to set the server path for the entire test script.

Usage:

XK6_TRACETEST_SERVER_PATH=<your-server-path> k6 run <your-script>.js -o xk6-tracetest

Need more ways to configure it? Let us know!

Full Script Example

The following is a full script example using the k6 Tracetest extension:

import { check } from "k6";
import { textSummary } from "https://jslib.k6.io/k6-summary/0.0.2/index.js";
import { Http, Tracetest } from "k6/x/tracetest";
import { sleep } from "k6";

export const options = {
vus: 1,
duration: "6s",
};

const tracetest = Tracetest({
serverUrl: "http://localhost:11633",
});
const testId = "kc_MgKoVR";
let pokemonId = 6; // charizad
const http = new Http();
const url = "http://localhost:8081/pokemon/import";

export default function () {
const payload = JSON.stringify({
id: pokemonId,
});
const params = {
tracetest: {
testId,
},
headers: {
"Content-Type": "application/json",
},
};

const response = http.post(url, payload, params);

check(response, {
"is status 200": (r) => r.status === 200,
"body matches de id": (r) => JSON.parse(r.body).id === pokemonId,
});

pokemonId = pokemonId + 1;
sleep(1);
}

// enable this to return a non-zero status code if a tracetest test fails
export function teardown() {
tracetest.validateResult();
}

export function handleSummary(data) {
// combine the default summary with the tracetest summary
const tracetestSummary = tracetest.summary();
const defaultSummary = textSummary(data);
const summary = `
${defaultSummary}
${tracetestSummary}
`;

return {
stdout: summary,
"tracetest.json": tracetest.json(),
};
}

Get Started

If you want to learn more or try a full setup using k6 and Tracetest you can head out to this tracetest-k6 get-started example we have prepared. It is a step-by-step guide using Docker and our Pokemon demo app that showcases the power of combining k6 and Tracetest.