Documentation Index
Fetch the complete documentation index at: https://mintlify.com/grafana/k6/llms.txt
Use this file to discover all available pages before exploring further.
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
xk6 build --with github.com/yourname/xk6-myextension@v0.1.0
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