-
Notifications
You must be signed in to change notification settings - Fork 223
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Maybe resolves #346 (comment) ? Still needs a bit of work.
- Loading branch information
Ivan Mirić
committed
Jul 23, 2021
1 parent
17ab3e9
commit d740d18
Showing
1 changed file
with
254 additions
and
58 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -74,18 +74,19 @@ Some examples include: [xk6-sql](https://github.com/imiric/xk6-sql), | |
|
||
#### Output extension | ||
|
||
While k6 has built-in support for many popular | ||
[output backends](/docs/getting-started/results-output/), this list will undoubtedly | ||
not be exhaustive. Output extensions receive metric samples from k6, and are able to | ||
do any processing or further dispatching. | ||
While k6 has built-in support for many popular [output | ||
backends](/docs/getting-started/results-output/), this list will undoubtedly not be | ||
exhaustive. Support for new systems and novel ways of handling the metric data | ||
generated by k6 can be easily added with Output extensions. These receive metric | ||
samples from k6, and are able to do any processing or further dispatching. | ||
|
||
Some examples include: [xk6-prometheus](https://github.com/szkiba/xk6-prometheus) | ||
and [xk6-influxdbv2](https://github.com/li-zhixin/xk6-influxdbv2). | ||
|
||
|
||
## How to get started | ||
## Getting started | ||
|
||
There are two ways that xk6 can be used: | ||
There are two ways in which xk6 can be used: | ||
|
||
- By k6 users that wish to enhance their tests with existing extensions. A | ||
familiarity with the command line and Go is preferred, but not required. | ||
|
@@ -95,8 +96,10 @@ There are two ways that xk6 can be used: | |
and maintain a public repository for the extension that keeps up to date with any | ||
breaking API changes while xk6 is being stabilized. | ||
|
||
|
||
### Using xk6 to build a k6 binary | ||
<!-- This used to be an h3 (nested under "Getting Started"), but h5s aren't styled --> | ||
<!-- properly and the right side index doesn't show sections above h2, so this is --> | ||
<!-- at the same level now. :-/ --> | ||
## Using xk6 to build a k6 binary | ||
|
||
You might have found a neat k6 extension on the [Ecosystem page](/ecosystem) or on | ||
[GitHub](https://github.com/topics/xk6) and wish to use it in your tests. Great! The | ||
|
@@ -117,7 +120,7 @@ $ go install github.com/k6io/xk6/cmd/xk6@latest | |
|
||
</CodeGroup> | ||
|
||
And confirm that `which xk6` on Linux/macOS or `where xk6.exe` on Windows returns a | ||
And confirm that `which xk6` on Linux/macOS or `where xk6` on Windows returns a | ||
valid path. Otherwise ensure that `$GOPATH` is correctly defined and that | ||
`$GOPATH/bin` is added to your `$PATH` environment variable. See the | ||
[Go documentation](https://golang.org/cmd/go/#hdr-GOPATH_environment_variable) for details. | ||
|
@@ -144,54 +147,247 @@ Note that you'll need to specify the binary in the current directory (i.e. `./k6 | |
`.\k6` on Windows) to avoid using any other `k6` binary in your `PATH`. | ||
|
||
|
||
### Writing a new extension | ||
|
||
A good starting point for using xk6 and writing your own extension is the [xk6 | ||
introductory article](https://k6.io/blog/extending-k6-with-xk6). | ||
<!-- TODO: How much of this article do we want to reproduce here? --> | ||
|
||
You'll first need to decide the type of extension you wish to create. Choose a | ||
JavaScript extension if you want to extend the JS functionality of your script, or an | ||
Output extension if you want to process the metrics emitted by k6 in some way. | ||
|
||
A few things to keep in mind when writing a new extension: | ||
<!-- TODO: Better structure this list? --> | ||
|
||
- JS extensions are registered using | ||
[`modules.Register()`](https://pkg.go.dev/go.k6.io/[email protected]/js/modules#Register) | ||
and Output extensions using | ||
[`output.RegisterExtension()`](https://pkg.go.dev/go.k6.io/[email protected]/output#RegisterExtension), | ||
which should be called in the `init()` function of the module. | ||
In either case you'll have to specify a unique name that preferably doesn't clash | ||
with any existing extension. JS extensions also must begin with `k6/x/` so as to | ||
differentiate them from built-in JS modules. | ||
|
||
- JS extensions expose Go methods which can optionally receive a | ||
[`context.Context`](https://golang.org/pkg/context/#Context) instance as the first argument. | ||
This context is used throughout the k6 codebase and contains embedded objects | ||
which can be extracted to access the | ||
[`goja.Runtime`](https://pkg.go.dev/github.com/dop251/goja#Runtime) for a | ||
particular VU, and to get more information about the test in progress. | ||
Take a look at | ||
[`common.GetRuntime()`](https://pkg.go.dev/go.k6.io/[email protected]/js/common#GetRuntime), | ||
[`lib.GetState()`](https://pkg.go.dev/go.k6.io/[email protected]/lib#GetState) and | ||
[`lib.GetExecutionState()`](https://pkg.go.dev/go.k6.io/[email protected]/lib#GetExecutionState). | ||
|
||
- Output extensions must implement the | ||
[`output.Output`](https://pkg.go.dev/go.k6.io/[email protected]/output#Output) interface. | ||
They should be particularly weary of performance, since they can receive a large | ||
amount of metrics to process, which can easily degrade the performance of the test | ||
if the extension is inefficient. | ||
As such you should ensure that `AddMetricSamples()` doesn't block for a long time, | ||
and that metrics are flushed periodically to avoid memory leaks. | ||
|
||
Since this is common functionality that most outputs should have, we've provided | ||
a couple of helper structs: [`output.SampleBuffer`](https://pkg.go.dev/go.k6.io/[email protected]/output#SampleBuffer) | ||
and [`output.PeriodicFlusher`](https://pkg.go.dev/go.k6.io/[email protected]/output#PeriodicFlusher) | ||
that output implementations can use which handles bufferring and periodic | ||
flushing in a thread-safe way. | ||
For usage examples see the [`statsd` output](https://github.com/k6io/k6/blob/v0.33.0/output/statsd/output.go#L55). | ||
## Writing a new extension | ||
|
||
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 [Ecosystem page](/ecosystem) and the [`xk6` topic on GitHub](https://github.com/topics/xk6). | ||
For example, if a system you need support for can be tested with a generic protocol | ||
like MQTT, prefer using [xk6-mqtt](https://github.com/pmalhaire/xk6-mqtt) | ||
instead of creating an extension that uses some custom protocol. | ||
|
||
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 good starting point for using xk6 and writing a JS extension is the [xk6 | ||
introductory article](https://k6.io/blog/extending-k6-with-xk6), but we'll cover | ||
some of the details here. | ||
|
||
JavaScript extensions consist of a main module struct that exposes methods that | ||
can be called from a k6 test script. For example: | ||
|
||
<!-- TODO: A better trivial example? --> | ||
|
||
<CodeGroup labels={["compare.go"]} lineNumbers={[false]}> | ||
|
||
```go | ||
package compare | ||
|
||
type Compare struct{} | ||
|
||
func (*Compare) IsGreater(int a, b) bool { | ||
return a > b | ||
} | ||
``` | ||
|
||
</CodeGroup> | ||
|
||
In order to use this from k6 test scripts we need to register the module | ||
by adding the following: | ||
|
||
<CodeGroup labels={["compare.go"]} lineNumbers={[false]}> | ||
|
||
```go | ||
import "go.k6.io/k6/js/modules" | ||
|
||
func init() { | ||
modules.Register("k6/x/compare", new(Compare)) | ||
} | ||
``` | ||
|
||
</CodeGroup> | ||
|
||
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. | ||
|
||
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: | ||
|
||
<CodeGroup labels={["test.js"]} lineNumbers={[true]}> | ||
|
||
```go | ||
import compare from 'k6/x/compare'; | ||
|
||
- Custom metric emission can be done by creating new metrics using [`stats.New()`](https://pkg.go.dev/go.k6.io/[email protected]/stats#New) | ||
and emitting them using [`stats.PushIfNotDone()`](https://pkg.go.dev/go.k6.io/[email protected]/stats#PushIfNotDone). | ||
export default function () { | ||
console.log(compare.isGreater(2, 1)); | ||
} | ||
``` | ||
|
||
</CodeGroup> | ||
|
||
And run the test with `./k6 run test.js`, which should output `INFO[0000] true`. | ||
|
||
Note that we have to specify the binary we just built in the current directory | ||
(`./k6`), as otherwise some other `k6` binary found on the system could be executed | ||
which might not have the extension built-in. This is only the case on Linux and | ||
macOS, as Windows shells will execute the binary in the current directory first. | ||
|
||
|
||
#### Additional 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`. | ||
|
||
<!-- FIXME: Markdown magic to escape backticks in Go struct tags? <pre> doesn't work... --> | ||
- Similarly, Go field names will be converted from Pascal case to Snake case. | ||
For example, the struct field `SomeField string` is 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:"-"`. | ||
|
||
- Method names prefixed with `X` are interpreted as constructors in JS, | ||
and will support the `new` operator. | ||
For example, defining the following method on the above struct: | ||
|
||
<CodeGroup labels={["compare.go"]} lineNumbers={[false]}> | ||
|
||
```go | ||
func (*Compare) XComparator() *Comparator { | ||
return &Comparator{} | ||
} | ||
``` | ||
|
||
</CodeGroup> | ||
|
||
Would allow creating a `Comparator` instance in JS with `new compare.Comparator()`, | ||
which is a bit more idiomatic to JS. | ||
|
||
- Methods that specify `context.Context` or `*context.Context` as the first | ||
argument will be passed the `Context` instance used internally in k6, | ||
which has attached some useful objects for inspecting the internal execution | ||
state, such as | ||
[`lib.State`](https://github.com/grafana/k6/blob/v0.33.0/lib/state.go#L43) | ||
or VU state, [`lib.ExecutionState`](https://github.com/grafana/k6/blob/v0.33.0/lib/execution.go#L142), | ||
and the [`goja.Runtime`](https://github.com/dop251/goja/blob/705acef95ba3654f89c969d9e792ac5f49215350/runtime.go#L162) instance | ||
the VU is using to execute the script. | ||
This feature is used extensively in the | ||
[`xk6-execution`](https://github.com/grafana/xk6-execution) extension. | ||
|
||
<!-- TODO: Mention common.Bind() here? HasModuleInstancePerVU? --> | ||
|
||
|
||
#### Things to keep in mind | ||
|
||
- The code in the `default` function (or another function specified by | ||
[`exec`](/using-k6/scenarios/#common-options)) 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](https://en.wikipedia.org/wiki/Thread_safety). | ||
- Any heavy initialization should be done in the [`init` | ||
context](/javascript-api/init-context/)... | ||
- Custom metric emission can be done by creating new metrics using | ||
[`stats.New()`](https://github.com/grafana/k6/blob/v0.33.0/stats/stats.go#L449) | ||
and emitting them using [`stats.PushIfNotDone()`](https://github.com/grafana/k6/blob/v0.33.0/stats/stats.go#L429). | ||
For an example of this see the [`xk6-remote-write` extension](https://github.com/dgzlopes/xk6-remote-write). | ||
|
||
|
||
### Writing a new Output extension | ||
|
||
Output extensions are similarly written, but have a different API and performance | ||
considerations. | ||
|
||
The core of an Output extension is a struct that implements the [`output.Output` | ||
interface](https://github.com/grafana/k6/blob/v0.33.0/output/types.go#L57). For example: | ||
|
||
<CodeGroup labels={["log.go"]} lineNumbers={[false]}> | ||
|
||
```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 | ||
} | ||
``` | ||
|
||
</CodeGroup> | ||
|
||
Notice a couple of things: | ||
|
||
- The module initializer `New()` receives an instance of | ||
[`output.Params`](https://github.com/grafana/k6/blob/v0.33.0/output/types.go#L36). | ||
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](https://github.com/grafana/k6/blob/v0.33.0/output/types.go#L79), | ||
[test run status updates](https://github.com/grafana/k6/blob/v0.33.0/output/types.go#L94) | ||
or [interrupt a test run](https://github.com/grafana/k6/blob/v0.33.0/output/types.go#L88). | ||
- 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`](https://github.com/grafana/k6/blob/v0.33.0/output/helpers.go#L35) | ||
is a thread-safe buffer for metric samples to help with memory management and | ||
[`output.PeriodicFlusher`](https://github.com/grafana/k6/blob/v0.33.0/output/helpers.go#L75) | ||
will periodically run a function which is useful for flushing or dispatching the | ||
buffered samples. | ||
For usage examples see the [`statsd` output](https://github.com/k6io/k6/blob/v0.33.0/output/statsd/output.go#L55). | ||
|
||
|
||
#### Things to keep in mind | ||
|
||
- ... |