Create custom k6 extensions to add JavaScript modules, output formats, secret sources, or CLI subcommands. Extensions are written in Go and compiled into k6 using xk6.
Prerequisites
- Go 1.22 or later
- Understanding of Go programming
- Familiarity with k6 concepts
- Git for version control
Extension Types
You can create four types of extensions:
- JavaScript Extensions: Add custom JavaScript APIs
- Output Extensions: Create custom result outputs
- Secret Source Extensions: Implement secret providers
- Subcommand Extensions: Add CLI subcommands
See Extensions Overview for details on each type.
Project Setup
Create a new Go module for your extension:
mkdir xk6-myextension
cd xk6-myextension
go mod init github.com/yourname/xk6-myextension
go get go.k6.io/k6@latest
Create your extension implementation:
Follow the xk6- naming convention for discoverability. Name your extension repository xk6-extensionname.
Creating a JavaScript Extension
JavaScript extensions add custom modules that can be imported in test scripts.
Basic Structure
package myextension
import (
"go.k6.io/k6/js/modules"
)
func init() {
modules.Register("k6/x/myextension", New())
}
type RootModule struct{}
type ModuleInstance struct {
vu modules.VU
}
func New() *RootModule {
return &RootModule{}
}
func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
return &ModuleInstance{vu: vu}
}
func (mi *ModuleInstance) Exports() modules.Exports {
return modules.Exports{Default: mi}
}
Adding Functionality
Add methods that will be available in JavaScript:
func (mi *ModuleInstance) Greet(name string) string {
return "Hello, " + name + "!"
}
func (mi *ModuleInstance) Add(a, b int) int {
return a + b
}
Use in JavaScript:
import myext from 'k6/x/myextension';
export default function () {
console.log(myext.greet('World'));
console.log(myext.add(2, 3));
}
Working with Metrics
Create custom metrics in your extension:
import (
"go.k6.io/k6/js/modules"
"go.k6.io/k6/metrics"
"time"
)
type ModuleInstance struct {
vu modules.VU
custom *metrics.Metric
}
func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
return &ModuleInstance{
vu: vu,
custom: vu.InitEnv().Registry.MustNewMetric("custom_metric", metrics.Counter),
}
}
func (mi *ModuleInstance) RecordValue(value float64) {
state := mi.vu.State()
ctx := mi.vu.Context()
tags := state.Tags.GetCurrentValues().Tags
metrics.PushIfNotDone(ctx, state.Samples, metrics.Sample{
Time: time.Now(),
TimeSeries: metrics.TimeSeries{Metric: mi.custom, Tags: tags},
Value: value,
})
}
Always check if vu.State() is nil. It will be nil in the init context where metric emission is not allowed.
Real Example
Here’s a real JavaScript extension from k6’s test suite (xk6-js-test/jstest.go:1):
package jstest
import (
"fmt"
"time"
"go.k6.io/k6/js/modules"
"go.k6.io/k6/metrics"
)
func init() {
modules.Register("k6/x/jsexttest", New())
}
type (
RootModule struct{}
JSTest struct {
vu modules.VU
foos *metrics.Metric
}
)
func New() *RootModule {
return &RootModule{}
}
func (*RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
return &JSTest{
vu: vu,
foos: vu.InitEnv().Registry.MustNewMetric("foos", metrics.Counter),
}
}
func (j *JSTest) Exports() modules.Exports {
return modules.Exports{Default: j}
}
func (j *JSTest) Foo(arg float64) (bool, error) {
state := j.vu.State()
if state == nil {
return false, fmt.Errorf("the VU State is not available in the init context")
}
ctx := j.vu.Context()
tags := state.Tags.GetCurrentValues().Tags.With("foo", "bar")
metrics.PushIfNotDone(ctx, state.Samples, metrics.Sample{
Time: time.Now(),
TimeSeries: metrics.TimeSeries{Metric: j.foos, Tags: tags},
Value: arg,
})
return true, nil
}
Creating an Output Extension
Output extensions send test results to custom backends.
Basic Implementation
See Creating Custom Outputs for a comprehensive guide.
Quick example:
package myoutput
import (
"go.k6.io/k6/output"
"go.k6.io/k6/metrics"
)
func init() {
output.RegisterExtension("myoutput", New)
}
type Output struct {
output.SampleBuffer
params output.Params
}
func New(params output.Params) (output.Output, error) {
return &Output{params: params}, nil
}
func (o *Output) Description() string {
return "my-custom-output"
}
func (o *Output) Start() error {
return nil
}
func (o *Output) Stop() error {
samples := o.GetBufferedSamples()
// Process samples
return nil
}
Creating a Secret Source Extension
Secret source extensions provide secure configuration values.
Implementation
package mysecrets
import (
"go.k6.io/k6/ext"
)
func init() {
ext.Register("mysecrets", ext.SecretSourceExtension, &SecretSource{})
}
type SecretSource struct{}
func (s *SecretSource) GetSecret(key string) (string, error) {
// Fetch secret from your backend
return fetchSecretFromVault(key)
}
Creating a Subcommand Extension
Subcommand extensions add new CLI commands to k6.
Implementation
package mycommand
import (
"github.com/spf13/cobra"
"go.k6.io/k6/ext"
)
func init() {
ext.Register("mycommand", ext.SubcommandExtension, NewCommand)
}
func NewCommand() *cobra.Command {
return &cobra.Command{
Use: "mycommand",
Short: "My custom command",
RunE: func(cmd *cobra.Command, args []string) error {
// Command implementation
return nil
},
}
}
Use it:
Extension Registration
All extensions must register in an init() function using ext.Register() (ext/ext.go:62):
import "go.k6.io/k6/ext"
func init() {
ext.Register(
"extensionname", // Name
ext.JSExtension, // Type
New(), // Module/implementation
)
}
Registration happens automatically when the extension package is imported during build.
Testing Your Extension
Unit Tests
package myextension
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestGreet(t *testing.T) {
mi := &ModuleInstance{}
result := mi.Greet("World")
assert.Equal(t, "Hello, World!", result)
}
Integration Tests
Create a test script:
// test.js
import myext from 'k6/x/myextension';
import { check } from 'k6';
export default function () {
const result = myext.greet('k6');
check(result, {
'greeting works': (r) => r === 'Hello, k6!',
});
}
Build and test:
xk6 build --with github.com/yourname/xk6-myextension=.
./k6 run test.js
Building and Using
Local Development
Build k6 with your local extension:
xk6 build --with github.com/yourname/xk6-myextension=.
Publishing
git add .
git commit -m "Initial extension implementation"
git tag v0.1.0
git push origin main --tags
Best Practices
Use standard Go project layout and naming conventions.
Provide clear documentation for all exported functions:
// Greet returns a greeting message for the given name.
func (mi *ModuleInstance) Greet(name string) string {
return "Hello, " + name + "!"
}
Return errors to JavaScript instead of panicking:
func (mi *ModuleInstance) DoSomething() error {
if err := operation(); err != nil {
return fmt.Errorf("operation failed: %w", err)
}
return nil
}
Check all inputs from JavaScript:
func (mi *ModuleInstance) SetValue(val int) error {
if val < 0 {
return errors.New("value must be non-negative")
}
// ...
}
Use Context Appropriately
func (mi *ModuleInstance) LongOperation() error {
ctx := mi.vu.Context()
select {
case <-ctx.Done():
return ctx.Err()
case result := <-doWork():
return nil
}
}
Don’t block VU execution with long operations:
// Bad: blocks VU
func (mi *ModuleInstance) SlowSync() {
time.Sleep(5 * time.Second)
}
// Good: async or fast
func (mi *ModuleInstance) FastAsync() chan Result {
ch := make(chan Result, 1)
go func() {
// Long operation
ch <- result
}()
return ch
}
Common Patterns
Connection Pooling
type ModuleInstance struct {
vu modules.VU
pool *ConnectionPool
}
func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
return &ModuleInstance{
vu: vu,
pool: getOrCreatePool(vu),
}
}
Configuration
type Config struct {
Host string `json:"host"`
Port int `json:"port"`
}
func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance {
var config Config
if err := json.Unmarshal(vu.InitEnv().Options, &config); err != nil {
// Handle error
}
return &ModuleInstance{config: config}
}
Resource Cleanup
func (mi *ModuleInstance) Close() error {
if mi.connection != nil {
return mi.connection.Close()
}
return nil
}
Debugging
Enable Verbose Logging
import "github.com/sirupsen/logrus"
func (mi *ModuleInstance) Debug(msg string) {
logger := mi.vu.InitEnv().Logger
logger.WithField("extension", "myext").Debug(msg)
}
Use k6 Verbose Mode
./k6 run --verbose test.js
Example Extensions
Study these official examples:
Resources
Next Steps