No results for

Powered byAlgolia

Create an extension

If you want to create your own k6 extension, you will need to be familiar with both Go and JavaScript, understand how the k6 Go<->JS bridge works, and maintain a public repository for the extension that keeps up to date with any breaking API changes while xk6 is being stabilized.

The first thing you should do before starting work on a new extension is to confirm that a similar extension doesn't already exist for your use case. Take a look at the Extensions page and the xk6 topic on GitHub. For example, if a system you need support for can be tested with a generic protocol like MQTT, prefer using xk6-mqtt instead of creating an extension that uses some custom protocol. Also, prefer to write a pure JavaScript library that can be used in k6 if you can avoid writing an extension in Go, since a JS library will be better supported and likely easier to write and reuse than an extension.

Next, you should decide the type of extension you need. A JavaScript extension is a good fit if you want to extend the JS functionality of your script, or add support for a new network protocol to test with. An Output extension would be more suitable if you need to process the metrics emitted by k6 in some way, submit them to a specific storage backend that was previously unsupported, etc. The k6 APIs you'll need to use and things to consider while developing will be different in each case.

Writing a new JavaScript extension

A simple JavaScript extension consists of a main module struct that exposes methods that can be called from a k6 test script. For example:

package compare
type Compare struct{}
func (*Compare) IsGreater(a, b int) bool {
return a > b
}

In order to use this from k6 test scripts we need to register the module by adding the following:

import "go.k6.io/k6/js/modules"
func init() {
modules.Register("k6/x/compare", new(Compare))
}

Note that all k6 extensions should have the k6/x/ prefix and the short name must be unique among all extensions built in the same k6 binary.

The final extension code will look like so:

compare.go
1package compare
2
3import "go.k6.io/k6/js/modules"
4
5func init() {
6 modules.Register("k6/x/compare", new(Compare))
7}
8
9type Compare struct{}
10
11func (*Compare) IsGreater(a, b int) bool {
12 return a > b
13}

We can then build a k6 binary with this extension by running xk6 build --with xk6-compare=.. In this case xk6-compare is the Go module name passed to go mod init, but in a real-world scenario this would be a URL.

Finally we can use the extension in a test script:

test.js
1import compare from 'k6/x/compare';
2
3export default function () {
4 console.log(compare.isGreater(2, 1));
5}

And run the test with ./k6 run test.js, which should output INFO[0000] true.

Notable features

The k6 Go-JS bridge has a few features we should highlight:

  • Go method names will be converted from Pascal case to Camel case when accessed in JS, as in the example above: IsGreater becomes isGreater.

  • Similarly, Go field names will be converted from Pascal case to Snake case. For example, the struct field SomeField string will be accessible in JS as the some_field object property. This behavior is configurable with the js struct tag, so this can be changed with SomeField string `js:"someField"` or the field can be hidden with `js:"-"`.

  • Methods with a name prefixed with X will be transformed to JS constructors, and will support the new operator. For example, defining the following method on the above struct:

type Comparator struct{}
func (*Compare) XComparator() *Comparator {
return &Comparator{}
}

Would allow creating a Comparator instance in JS with new compare.Comparator(), which is a bit more idiomatic to JS.

Advanced JavaScript extension

ℹ️ Note

The internal JavaScript module API is currently (October 2021) in a state of flux. The traditional approach of initializing JS modules involves calling common.Bind() on any objects that need to be exposed to JS. This method has a few technical issues we want to improve, and also isn't flexible enough to implement new features like giving extensions access to internal k6 objects. Starting from v0.32.0 we've introduced a new approach for writing JS modules and is the method we'll be describing below. While this new API is recommended for new modules and extensions, note that it's still in development and might change while it's being stabilized.

If your extension requires access to internal k6 objects to, for example, inspect the state of the test during execution, we will need to make some slightly more complicated changes to the above example.

Our main Compare struct should implement the modules.Instance interface and get access to modules.VU in order to access internal k6 objects such as:

  • lib.State: the VU state with values like the VU ID and iteration number.
  • goja.Runtime: the JavaScript runtime used by the VU.
  • a global context.Context which contains other interesting objects like lib.ExecutionState.

Additionally there should be a root module implementation of the modules.Module interface that will serve as a factory of Compare instances for each VU. Note that this can have memory implications depending on the size of your module.

Here's how that would look like:

compare-advanced.go
1package compare
2
3import "go.k6.io/k6/js/modules"
4
5func init() {
6 modules.Register("k6/x/compare", New())
7}
8
9type (
10 // RootModule is the global module instance that will create Compare
11 // instances for each VU.
12 RootModule struct{}
13
14 // Compare represents an instance of the JS module.
15 Compare struct {
16 // modules.VU provides some useful methods for accessing internal k6
17 // objects like the global context, VU state and goja runtime.
18 vu modules.VU
19 // Comparator is the exported module instance.
20 *Comparator
21 }
22)
23
24// Ensure the interfaces are implemented correctly.
25var (
26 _ modules.Instance = &Compare{}
27 _ modules.Module = &RootModule{}
28)
29
30// New returns a pointer to a new RootModule instance.
31func New() *RootModule {
32 return &RootModule{}
33}
34
35// NewModuleInstance implements the modules.Module interface and returns
36// a new instance for each VU.
37func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
38 return &Compare{vu: vu, Comparator: &Comparator{vu: vu}}
39}
40
41// Comparator is the exported module instance.
42type Comparator struct{
43 vu modules.VU
44}
45
46// IsGreater returns true if a is greater than b, or false otherwise.
47func (*Comparator) IsGreater(a, b int) bool {
48 return a > b
49}
50
51// Exports implements the modules.Instance interface and returns the exports
52// of the JS module.
53func (c *Compare) Exports() modules.Exports {
54 return modules.Exports{Default: c.Comparator}
55}

Currently this module isn't taking advantage of the methods provided by modules.VU because our simple example extension doesn't require it, but here is a contrived example of how that could be done:

type InternalState struct {
ActiveVUs int64 `js:"activeVUs"`
Iteration int64
VUID uint64 `js:"vuID"`
VUIDFromRuntime goja.Value `js:"vuIDFromRuntime"`
}
func (c *Comparator) GetInternalState() *InternalState {
state := c.vu.State()
ctx := c.vu.Context()
es := lib.GetExecutionState(ctx)
rt := c.vu.Runtime()
return &InternalState{
VUID: state.VUID,
VUIDFromRuntime: rt.Get("__VU"),
Iteration: state.Iteration,
ActiveVUs: es.GetCurrentlyActiveVUsCount(),
}
}

Running a script like:

test.js
1import compare from 'k6/x/compare';
2
3export default function () {
4 const state = compare.getInternalState();
5 console.log(`Active VUs: ${state.activeVUs}
6Iteration: ${state.iteration}
7VU ID: ${state.vuID}
8VU ID from runtime: ${state.vuIDFromRuntime}`);
9}

Should output:

INFO[0000] Active VUs: 1
Iteration: 0
VU ID: 1
VU ID from runtime: 1 source=console

ℹ️ Note

For a more extensive usage example of this API, take a look at the k6/execution module.

Notice that the JavaScript runtime will transparently convert Go types like int64 to their JS equivalent. For complex types where this is not possible your script might fail with a TypeError and you will need to convert your object to a goja.Object or goja.Value.

For example:

type Comparator struct{}
func (*Compare) XComparator(call goja.ConstructorCall, rt *goja.Runtime) *goja.Object {
return rt.ToValue(&Comparator{}).ToObject(rt)
}

This also demonstrates the native constructors feature from goja, where methods with this signature will be transformed to JS constructors, and also have the benefit of receiving the goja.Runtime, which is an alternative way to access it in addition to the GetRuntime() method shown above.

Things to keep in mind

  • The code in the default function (or another function specified by exec) will be executed many times during a test run and possibly in parallel by thousands of VUs. As such any operation of your extension meant to run in that context needs to be performant and thread-safe.
  • Any heavy initialization should be done in the init context if possible, and not as part of the default function execution.
  • Custom metric emission can be done by creating new metrics using stats.New() and emitting them using stats.PushIfNotDone(). For an example of this see the xk6-remote-write extension.

Writing a new Output extension

Output extensions are written similarly to JavaScript extensions, but have a different API and performance considerations.

The core of an Output extension is a struct that implements the output.Output interface. For example:

log.go
package log
import (
"fmt"
"io"
"go.k6.io/k6/output"
"go.k6.io/k6/stats"
)
// Register the extension on module initialization.
func init() {
output.RegisterExtension("logger", New)
}
// Logger writes k6 metric samples to stdout.
type Logger struct {
out io.Writer
}
// New returns a new instance of Logger.
func New(params output.Params) (output.Output, error) {
return &Logger{params.StdOut}, nil
}
// Description returns a short human-readable description of the output.
func (*Logger) Description() string {
return "logger"
}
// Start initializes any state needed for the output, establishes network
// connections, etc.
func (o *Logger) Start() error {
return nil
}
// AddMetricSamples receives metric samples from the k6 Engine as they're
// emitted and prints them to stdout.
func (l *Logger) AddMetricSamples(samples []stats.SampleContainer) {
for i := range samples {
all := samples[i].GetSamples()
for j := range all {
fmt.Fprintf(l.out, "%d %s: %f\n", all[j].Time.UnixNano(), all[j].Metric.Name, all[j].Value)
}
}
}
// Stop finalizes any tasks in progress, closes network connections, etc.
func (*Logger) Stop() error {
return nil
}

Notice a couple of things:

  • The module initializer New() receives an instance of output.Params. With this object the extension can access the output-specific configuration, interfaces to the filesystem, synchronized stdout and stderr, and more.
  • AddMetricSamples in this example simply writes to stdout. In a real-world scenario this output might have to be buffered and flushed periodically to avoid memory leaks. Below we'll discuss some helpers you can use for that.

Additional features

  • Output structs can optionally implement additional interfaces that allows them to receive thresholds, test run status updates or interrupt a test run.
  • Because output implementations typically need to process large amounts of data that k6 produces and dispatch it to another system, we've provided a couple of helper structs you can use in your extensions: output.SampleBuffer is a thread-safe buffer for metric samples to help with memory management and output.PeriodicFlusher will periodically run a function which is useful for flushing or dispatching the buffered samples. For usage examples see the statsd output.