Skip to content

Commit

Permalink
WIP Add JS and Output sections
Browse files Browse the repository at this point in the history
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.
312 changes: 254 additions & 58 deletions src/data/markdown/translated-guides/en/07 Misc/05 k6 Extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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

- ...

0 comments on commit d740d18

Please sign in to comment.