Skip to content
This repository was archived by the owner on Jan 17, 2024. It is now read-only.

Commit 6ad7b5e

Browse files
dgzlopesna--
andauthored
Complete refactor of the extension (#25)
* Refactor Signed-off-by: Daniel González Lopes <danielgonzalezlopes@gmail.com> * Add k6 version Signed-off-by: Daniel González Lopes <danielgonzalezlopes@gmail.com> * wip * wip2 * wip * crocospans->cloud Signed-off-by: Daniel González Lopes <danielgonzalezlopes@gmail.com> * Fix examples Signed-off-by: Daniel González Lopes <danielgonzalezlopes@gmail.com> * Refactor README Signed-off-by: Daniel González Lopes <danielgonzalezlopes@gmail.com> * Add new line Signed-off-by: Daniel González Lopes <danielgonzalezlopes@gmail.com> Signed-off-by: Daniel González Lopes <danielgonzalezlopes@gmail.com> Co-authored-by: Nedyalko Andreev <n@andreev.sh>
1 parent e1e3c6c commit 6ad7b5e

File tree

15 files changed

+1025
-665
lines changed

15 files changed

+1025
-665
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@
22
examples/k6
33
k6
44
k6-distributed-tracing
5-
vendor
5+
vendor
6+
gen

Makefile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
DOCKER_BUILD=docker build
2+
3+
DOCKER_RUN=docker run
4+
5+
.PHONY: build
6+
build:
7+
xk6 build master --with github.com/grafana/xk6-distributed-tracing="${PWD}/../xk6-distributed-tracing"
8+
9+
.PHONY: proto
10+
proto:
11+
$(DOCKER_RUN) -v ${PWD}/crocospans:/defs namely/protoc-all -f *.proto -l go
12+
cp -r ${PWD}/crocospans/gen/pb-go/*.pb.go ${PWD}/crocospans

README.md

Lines changed: 23 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,13 @@
11
# xk6-distributed-tracing
22

3-
> ⚠️ **This is a proof of concept** ⚠️
4-
> It won't be supported by the k6 team.
5-
> It may also break in the future as xk6 evolves.
3+
This extension adds distributed tracing support to [k6](https://github.com/grafana/k6)!
64

7-
</div>
5+
That means that if you're testing an instrumented system, you can use this extension to start the traces on k6.
86

9-
This extension adds distributed tracing support to [k6](https://github.com/grafana/k6)! That means, that if you're testing a system that is instrumented, you can use this extension to start the traces on k6.
7+
Currently, it supports HTTP requests and the following propagation formats: `w3c`, `b3`, and `jaeger`.
108

119
It is implemented using the [xk6](https://github.com/grafana/xk6) extension system.
1210

13-
<div align="center">
14-
15-
![Example trace](/media/trace.png)
16-
*Trace started on k6*
17-
18-
</div>
19-
20-
## Features
21-
22-
The extension is built on top of [OpenTelemetry](https://opentelemetry.io/), and gives k6:
23-
- An instrumented HTTP client.
24-
- The hability to:
25-
- Export spans (your tracing data).
26-
- Supported exporters: `jaeger`, `zipkin`, `otlp`, `noop`, `stdout`
27-
- Propagate context.
28-
- Supported protocols: `w3c`, `b3`, `jaeger`, `ot`
29-
3011
## Build
3112

3213
To build a `k6` binary with this extension, first ensure you have the prerequisites:
@@ -56,7 +37,7 @@ import { sleep } from 'k6';
5637

5738
export let options = {
5839
vus: 1,
59-
duration: '10s',
40+
iterations: 10,
6041
};
6142

6243
export function setup() {
@@ -65,30 +46,23 @@ export function setup() {
6546

6647
export default function() {
6748
const http = new Http({
68-
exporter: "jaeger",
6949
propagator: "w3c",
70-
endpoint: "http://localhost:14268/api/traces"
7150
});
7251
const r = http.get('https://test-api.k6.io');
7352
console.log(`trace_id=${r.trace_id}`);
7453
sleep(1);
7554
}
76-
77-
export function teardown(){
78-
// Cleanly shutdown and flush telemetry when k6 exits.
79-
tracing.shutdown();
80-
}
8155
```
8256

8357
Result output:
8458

8559
```bash
8660
$ ./k6 run script.js
8761

88-
/\ |‾‾| /‾‾/ /‾‾/
89-
/\ / \ | |/ / / /
90-
/ \/ \ | ( / ‾‾\
91-
/ \ | |\ \ | (‾) |
62+
/\ |‾‾| /‾‾/ /‾‾/
63+
/\ / \ | |/ / / /
64+
/ \/ \ | ( / ‾‾\
65+
/ \ | |\ \ | (‾) |
9266
/ __________ \ |__| \__\ \_____/ .io
9367

9468
execution: local
@@ -98,17 +72,17 @@ $ ./k6 run script.js
9872
scenarios: (100.00%) 1 scenario, 1 max VUs, 40s max duration (incl. graceful stop):
9973
* default: 1 looping VUs for 10s (gracefulStop: 30s)
10074

101-
INFO[0000] Running xk6-distributed-tracing v0.0.2 source=console
102-
INFO[0000] trace-id=743fff0b96778539acb7139e72ea1e33
103-
INFO[0001] trace-id=365f4637a52526db1de2d30a5568ca3a
104-
INFO[0002] trace-id=c49e1df945049c5c3c8b59acc84d7d3b
105-
INFO[0003] trace-id=53e1937d56aa172b46d2310e3380dfe9
106-
INFO[0004] trace-id=d61e8757d35c9ca1780b88977ac56d72
107-
INFO[0005] trace-id=358e794ed636d268a918dcd2f3f9db0a
108-
INFO[0006] trace-id=992a959e09ee84f3905a215bec8b53a0
109-
INFO[0007] trace-id=aee11c64de11744ab5b66d5dd8ed361b
110-
INFO[0008] trace-id=c4dc45d857e99ede2bb902666457239d
111-
INFO[0009] trace-id=7623d10293d9f03c15deb8055935664e
75+
INFO[0000] Running xk6-distributed-tracing v0.2.0 source=console
76+
INFO[0000] trace_id=743fff0b96778539acb7139e72ea1e33
77+
INFO[0001] trace_id=365f4637a52526db1de2d30a5568ca3a
78+
INFO[0002] trace_id=c49e1df945049c5c3c8b59acc84d7d3b
79+
INFO[0003] trace_id=53e1937d56aa172b46d2310e3380dfe9
80+
INFO[0004] trace_id=d61e8757d35c9ca1780b88977ac56d72
81+
INFO[0005] trace_id=358e794ed636d268a918dcd2f3f9db0a
82+
INFO[0006] trace_id=992a959e09ee84f3905a215bec8b53a0
83+
INFO[0007] trace_id=aee11c64de11744ab5b66d5dd8ed361b
84+
INFO[0008] trace_id=c4dc45d857e99ede2bb902666457239d
85+
INFO[0009] trace_id=7623d10293d9f03c15deb8055935664e
11286

11387
running (10.1s), 0/1 VUs, 10 complete and 0 interrupted iterations
11488
default ✓ [======================================] 1 VUs 10s
@@ -119,13 +93,13 @@ default ✓ [======================================] 1 VUs 10s
11993
data_sent..................: 1.7 kB 165 B/s
12094
http_req_blocked...........: avg=223.43µs min=146.53µs med=217.39µs max=314.54µs p(90)=276.68µs p(95)=295.61µs
12195
http_req_connecting........: avg=137.18µs min=87.22µs med=130.17µs max=196.38µs p(90)=184.38µs p(95)=190.38µs
122-
http_req_duration..........: avg=6.58ms min=5.07ms med=6.45ms max=7.91ms p(90)=7.83ms p(95)=7.87ms
96+
http_req_duration..........: avg=6.58ms min=5.07ms med=6.45ms max=7.91ms p(90)=7.83ms p(95)=7.87ms
12397
http_req_receiving.........: avg=187.27µs min=94.29µs med=171.7µs max=295.67µs p(90)=293.28µs p(95)=294.48µs
12498
http_req_sending...........: avg=128.07µs min=94.64µs med=121.77µs max=175.65µs p(90)=160.41µs p(95)=168.03µs
125-
http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
126-
http_req_waiting...........: avg=6.27ms min=4.83ms med=6.13ms max=7.64ms p(90)=7.56ms p(95)=7.6ms
99+
http_req_tls_handshaking...: avg=0s min=0s med=0s max=0s p(90)=0s p(95)=0s
100+
http_req_waiting...........: avg=6.27ms min=4.83ms med=6.13ms max=7.64ms p(90)=7.56ms p(95)=7.6ms
127101
http_reqs..................: 10 0.991797/s
128-
iteration_duration.........: avg=916.48ms min=65.67µs med=1s max=1s p(90)=1s p(95)=1s
102+
iteration_duration.........: avg=916.48ms min=65.67µs med=1s max=1s p(90)=1s p(95)=1s
129103
iterations.................: 10 0.991797/s
130104
vus........................: 1 min=1 max=1
131105
vus_max....................: 1 min=1 max=1

client/client.go

Lines changed: 76 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,42 @@ package client
22

33
import (
44
"context"
5+
"fmt"
56
"net/http"
6-
"net/http/httptrace"
7+
"time"
78

89
"github.com/dop251/goja"
910
"go.k6.io/k6/js/modules"
1011
k6HTTP "go.k6.io/k6/js/modules/k6/http"
11-
"go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace"
12-
"go.opentelemetry.io/otel"
13-
"go.opentelemetry.io/otel/attribute"
14-
"go.opentelemetry.io/otel/propagation"
15-
"go.opentelemetry.io/otel/trace"
12+
"go.k6.io/k6/metrics"
1613
)
1714

15+
type Options struct {
16+
Propagator string
17+
}
18+
1819
type TracingClient struct {
1920
vu modules.VU
2021
httpRequest HttpRequestFunc
22+
23+
options Options
2124
}
2225

2326
type HTTPResponse struct {
24-
*k6HTTP.Response
25-
TraceID string
27+
*k6HTTP.Response `js:"-"`
28+
TraceID string
2629
}
2730

2831
type (
2932
HttpRequestFunc func(method string, url goja.Value, args ...goja.Value) (*k6HTTP.Response, error)
3033
HttpFunc func(ctx context.Context, url goja.Value, args ...goja.Value) (*k6HTTP.Response, error)
3134
)
3235

33-
func New(vu modules.VU, requestFunc HttpRequestFunc) *TracingClient {
36+
func New(vu modules.VU, requestFunc HttpRequestFunc, options Options) *TracingClient {
3437
return &TracingClient{
3538
httpRequest: requestFunc,
3639
vu: vu,
40+
options: options,
3741
}
3842
}
3943

@@ -72,44 +76,76 @@ func (c *TracingClient) Options(url goja.Value, args ...goja.Value) (*HTTPRespon
7276
return c.WithTrace(requestToHttpFunc(http.MethodOptions, c.httpRequest), "HTTP OPTIONS", url, args...)
7377
}
7478

75-
func (c *TracingClient) WithTrace(fn HttpFunc, spanName string, url goja.Value, args ...goja.Value) (*HTTPResponse, error) {
76-
ctx, _, span := startTraceAndSpan(c.vu.Context(), spanName)
77-
defer span.End()
78-
79-
id := span.SpanContext().TraceID().String()
80-
81-
ctx, val := getTraceHeadersArg(ctx)
82-
83-
args = append(args, val)
84-
res, err := fn(ctx, url, args...)
85-
span.SetAttributes(attribute.String("http.method", res.Request.Method), attribute.Int("http.status_code", res.Response.Status), attribute.String("http.url", res.Request.URL))
86-
// TODO: extract the textmap from the response
87-
return &HTTPResponse{Response: res, TraceID: id}, err
88-
}
89-
90-
func startTraceAndSpan(ctx context.Context, name string) (context.Context, trace.Tracer, trace.Span) {
91-
trace := otel.Tracer("xk6/http")
92-
ctx, span := trace.Start(ctx, name)
93-
return ctx, trace, span
79+
func isNilly(val goja.Value) bool {
80+
return val == nil || goja.IsNull(val) || goja.IsUndefined(val)
9481
}
9582

96-
func getTraceHeadersArg(ctx context.Context) (context.Context, goja.Value) {
97-
vm := goja.New()
98-
99-
ctx = httptrace.WithClientTrace(ctx, otelhttptrace.NewClientTrace(ctx))
83+
func (c *TracingClient) WithTrace(fn HttpFunc, spanName string, url goja.Value, args ...goja.Value) (*HTTPResponse, error) {
84+
state := c.vu.State()
85+
if state == nil {
86+
return nil, fmt.Errorf("HTTP requests can only be made in the VU context")
87+
}
10088

101-
h := http.Header{}
89+
traceID, _, err := EncodeTraceID(TraceID{
90+
Prefix: K6Prefix,
91+
Code: K6Code_Cloud,
92+
UnixTimestampNano: uint64(time.Now().UnixNano()) / uint64(time.Millisecond),
93+
})
94+
if err != nil {
95+
return nil, err
96+
}
10297

103-
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(h))
98+
tracingHeaders, err := GenerateHeaderBasedOnPropagator(c.options.Propagator, traceID)
99+
if err != nil {
100+
return nil, err
101+
}
104102

105-
headers := map[string][]string{}
106-
for key, header := range h {
107-
headers[key] = header
103+
// This makes sure that the tracing header will always be added correctly to
104+
// the HTTP request headers, whether they were explicitly specified by the
105+
// user in the script or not.
106+
//
107+
// First we make sure to either get the existing request params, or create
108+
// them from scratch if they were not specified:
109+
rt := c.vu.Runtime()
110+
var params *goja.Object
111+
if len(args) < 2 {
112+
params = rt.NewObject()
113+
if len(args) == 0 {
114+
args = []goja.Value{goja.Null(), params}
115+
} else {
116+
args = append(args, params)
117+
}
118+
} else {
119+
jsParams := args[1]
120+
if isNilly(jsParams) {
121+
params = rt.NewObject()
122+
args[1] = params
123+
} else {
124+
params = jsParams.ToObject(rt)
125+
}
126+
}
127+
// Then we either augment the existing params.headers or create them:
128+
var headers *goja.Object
129+
if jsHeaders := params.Get("headers"); isNilly(jsHeaders) {
130+
headers = rt.NewObject()
131+
params.Set("headers", headers)
132+
} else {
133+
headers = jsHeaders.ToObject(rt)
134+
}
135+
for key, val := range tracingHeaders {
136+
headers.Set(key, val)
108137
}
109138

110-
val := vm.ToValue(map[string]map[string][]string{
111-
"headers": headers,
139+
// TODO: set span_id as well as some other metadata?
140+
state.Tags.Modify(func(tagsAndMeta *metrics.TagsAndMeta) {
141+
tagsAndMeta.SetMetadata("trace_id", traceID)
112142
})
143+
defer state.Tags.Modify(func(tagsAndMeta *metrics.TagsAndMeta) {
144+
tagsAndMeta.DeleteMetadata("trace_id")
145+
})
146+
147+
// This calls the actual request() function from k6/http with our augmented arguments
148+
res, e := fn(c.vu.Context(), url, args...)
113149

114-
return ctx, val
150+
return &HTTPResponse{Response: res, TraceID: traceID}, e
115151
}

0 commit comments

Comments
 (0)