Skip to content

Commit 4f3a10f

Browse files
Add self-contained setup for load test (#275)
* Load test now provisions its own org, datasource, and dashboard * Load test run script now takes option for number of iterations Co-authored-by: Agnès Toulet <[email protected]>
1 parent af78b2d commit 4f3a10f

File tree

6 files changed

+240
-37
lines changed

6 files changed

+240
-37
lines changed

devenv/loadtest/README.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ Run load test for custom duration:
2020
$ ./run.sh -d 10s
2121
```
2222

23+
Run only 1 iteration of the load test (useful for testing):
24+
25+
```bash
26+
$ ./run.sh -i 1
27+
2328
Run load test for custom target url:
2429

2530
```bash

devenv/docker/ha/grafana/provisioning/dashboards/general/graph_panel.json renamed to devenv/loadtest/fixtures/graph_panel.json

File renamed without changes.

devenv/loadtest/modules/client.js

Lines changed: 109 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,66 @@
11
import http from "k6/http";
22

3+
export const DatasourcesEndpoint = class DatasourcesEndpoint {
4+
constructor(httpClient) {
5+
this.httpClient = httpClient;
6+
}
7+
8+
getAll() {
9+
return this.httpClient.get('/datasources');
10+
}
11+
12+
getById(id) {
13+
return this.httpClient.get(`/datasources/${id}`);
14+
}
15+
16+
getByName(name) {
17+
return this.httpClient.get(`/datasources/name/${name}`);
18+
}
19+
20+
create(payload) {
21+
return this.httpClient.post(`/datasources`, JSON.stringify(payload));
22+
}
23+
24+
update(id, payload) {
25+
return this.httpClient.put(`/datasources/${id}`, JSON.stringify(payload));
26+
}
27+
};
28+
29+
export const DashboardsEndpoint = class DashboardsEndpoint {
30+
constructor(httpClient) {
31+
this.httpClient = httpClient;
32+
}
33+
34+
getAll() {
35+
return this.httpClient.get('/dashboards');
36+
}
37+
38+
upsert(payload) {
39+
return this.httpClient.post(`/dashboards/db`, JSON.stringify(payload));
40+
}
41+
};
42+
43+
export const OrganizationsEndpoint = class OrganizationsEndpoint {
44+
constructor(httpClient) {
45+
this.httpClient = httpClient;
46+
}
47+
48+
getById(id) {
49+
return this.httpClient.get(`/orgs/${id}`);
50+
}
51+
52+
getByName(name) {
53+
return this.httpClient.get(`/orgs/name/${name}`);
54+
}
55+
56+
create(name) {
57+
let payload = {
58+
name: name,
59+
};
60+
return this.httpClient.post(`/orgs`, JSON.stringify(payload));
61+
}
62+
};
63+
364
export const UIEndpoint = class UIEndpoint {
465
constructor(httpClient) {
566
this.httpClient = httpClient;
@@ -10,32 +71,53 @@ export const UIEndpoint = class UIEndpoint {
1071
return this.httpClient.formPost('/login', payload);
1172
}
1273

13-
render() {
14-
return this.httpClient.get('/render/d-solo/_CPokraWz/graph-panel?orgId=1&panelId=1&width=1000&height=500&tz=Europe%2FStockholm')
74+
renderPanel(orgId, dashboardUid, panelId) {
75+
return this.httpClient.get(
76+
`/render/d-solo/${dashboardUid}/graph-panel`,
77+
{
78+
orgId,
79+
panelId,
80+
width: 1000,
81+
height: 500,
82+
tz: 'Europe/Stockholm',
83+
}
84+
);
1585
}
1686
}
1787

1888
export const GrafanaClient = class GrafanaClient {
1989
constructor(httpClient) {
20-
httpClient.onBeforeRequest = this.onBeforeRequest;
90+
httpClient.onBeforeRequest = (params) => {
91+
if (this.orgId && this.orgId > 0) {
92+
params.headers = params.headers || {};
93+
params.headers["X-Grafana-Org-Id"] = this.orgId;
94+
}
95+
}
96+
2197
this.raw = httpClient;
98+
this.dashboards = new DashboardsEndpoint(httpClient.withUrl('/api'));
99+
this.datasources = new DatasourcesEndpoint(httpClient.withUrl('/api'));
100+
this.orgs = new OrganizationsEndpoint(httpClient.withUrl('/api'));
22101
this.ui = new UIEndpoint(httpClient);
23102
}
24103

104+
loadCookies(cookies) {
105+
for (let [name, value] of Object.entries(cookies)) {
106+
http.cookieJar().set(this.raw.url, name, value);
107+
}
108+
}
109+
110+
saveCookies() {
111+
return http.cookieJar().cookiesForURL(this.raw.url + '/');
112+
}
113+
25114
batch(requests) {
26115
return this.raw.batch(requests);
27116
}
28117

29118
withOrgId(orgId) {
30119
this.orgId = orgId;
31120
}
32-
33-
onBeforeRequest(params) {
34-
if (this.orgId && this.orgId > 0) {
35-
params = params.headers || {};
36-
params.headers["X-Grafana-Org-Id"] = this.orgId;
37-
}
38-
}
39121
}
40122

41123
export const BaseClient = class BaseClient {
@@ -62,10 +144,16 @@ export const BaseClient = class BaseClient {
62144

63145
}
64146

65-
get(url, params) {
147+
get(url, queryParams, params) {
66148
params = params || {};
67-
this.beforeRequest(params);
68149
this.onBeforeRequest(params);
150+
151+
if (queryParams) {
152+
url += '?' + Array.from(Object.entries(queryParams)).map(([key, value]) =>
153+
`${key}=${encodeURIComponent(value)}`
154+
).join('&');
155+
}
156+
69157
return http.get(this.url + url, params);
70158
}
71159

@@ -86,6 +174,15 @@ export const BaseClient = class BaseClient {
86174
return http.post(this.url + url, body, params);
87175
}
88176

177+
put(url, body, params) {
178+
params = params || {};
179+
params.headers = params.headers || {};
180+
params.headers['Content-Type'] = 'application/json';
181+
182+
this.onBeforeRequest(params);
183+
return http.put(this.url + url, body, params);
184+
}
185+
89186
delete(url, params) {
90187
params = params || {};
91188
this.beforeRequest(params);

devenv/loadtest/modules/util.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
export const createTestOrgIfNotExists = (client) => {
2+
let orgId = 0;
3+
4+
let res = client.orgs.getByName('k6-image-renderer');
5+
if (res.status === 404) {
6+
res = client.orgs.create('k6-image-renderer');
7+
if (res.status !== 200) {
8+
throw new Error('Expected 200 response status when creating org');
9+
}
10+
return res.json().orgId;
11+
}
12+
13+
// This can happen e.g. in Hosted Grafana instances, where even admins
14+
// cannot see organisations
15+
if (res.status !== 200) {
16+
console.info(`unable to get orgs from instance, continuing with default orgId ${orgId}`);
17+
return orgId;
18+
}
19+
20+
return res.json().id;
21+
};
22+
23+
export const upsertTestdataDatasource = (client, name) => {
24+
const payload = {
25+
access: 'proxy',
26+
isDefault: false,
27+
name,
28+
type: 'testdata',
29+
};
30+
31+
let res = client.datasources.getByName(payload.name);
32+
let id;
33+
if (res.status === 404) {
34+
res = client.datasources.create(payload);
35+
36+
if (res.status == 200) {
37+
id = res.json().id;
38+
}
39+
} else if (res.status == 200) {
40+
id = res.json().id;
41+
res = client.datasources.update(res.json().id, payload);
42+
}
43+
44+
if (res.status !== 200) {
45+
throw new Error(`expected 200 response status when creating datasource, got ${res.status}`);
46+
}
47+
48+
return id;
49+
};
50+
51+
export const upsertDashboard = (client, dashboard) => {
52+
const payload = {
53+
dashboard,
54+
overwrite: true,
55+
};
56+
57+
let res = client.dashboards.upsert(payload);
58+
59+
if (res.status !== 200) {
60+
throw new Error(`expected 200 response status when creating dashboards, got ${res.status}`);
61+
}
62+
63+
return res.json().id;
64+
};

devenv/loadtest/render_test.js

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,56 @@
11
import { check, group } from 'k6';
22
import { createClient } from './modules/client.js';
3+
import {
4+
createTestOrgIfNotExists,
5+
upsertTestdataDatasource,
6+
upsertDashboard,
7+
} from './modules/util.js';
38

49
export let options = {
5-
noCookiesReset: true
10+
noCookiesReset: true,
11+
thresholds: { checks: [ { threshold: 'rate=1', abortOnFail: true } ] },
612
};
713

814
let endpoint = __ENV.URL || 'http://localhost:3000';
915
const client = createClient(endpoint);
16+
const dashboard = JSON.parse(open('fixtures/graph_panel.json'));
1017

11-
export default () => {
12-
group("render test", () => {
13-
if (__ITER === 0) {
14-
group("user authenticates thru ui with username and password", () => {
15-
let res = client.ui.login('admin', 'admin');
18+
export const setup = () => {
19+
group("user authenticates thru ui with username and password", () => {
20+
let res = client.ui.login('admin', 'admin');
1621

17-
check(res, {
18-
'response status is 200': (r) => r.status === 200,
19-
});
20-
});
21-
}
22-
23-
if (__ITER !== 0) {
24-
group("render graph panel", () => {
25-
const response = client.ui.render();
26-
check(response, {
27-
'response status is 200': (r) => r.status === 200,
28-
});
22+
check(res, {
23+
'response status is 200': (r) => r.status === 200,
24+
});
25+
});
26+
27+
const orgId = createTestOrgIfNotExists(client);
28+
client.withOrgId(orgId);
29+
upsertTestdataDatasource(client, dashboard.panels[0].datasource);
30+
upsertDashboard(client, dashboard);
31+
32+
return {
33+
orgId,
34+
cookies: client.saveCookies(),
35+
};
36+
};
37+
38+
export default (data) => {
39+
client.loadCookies(data.cookies);
40+
client.withOrgId(data.orgId);
41+
42+
group("render test", () => {
43+
group("render graph panel", () => {
44+
const response = client.ui.renderPanel(
45+
data.orgId,
46+
dashboard.uid,
47+
dashboard.panels[0].id,
48+
);
49+
check(response, {
50+
'response status is 200': (r) => r.status === 200,
51+
'response is a PNG': (r) => r.headers['Content-Type'] == 'image/png',
2952
});
30-
}
53+
});
3154
});
3255
}
3356

devenv/loadtest/run.sh

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
#/bin/bash
22

3-
PWD=$(pwd)
3+
cd "$(dirname $0)"
44

55
run() {
6-
duration='15m'
7-
url='http://localhost:3000'
8-
vus='2'
6+
local duration='15m'
7+
local url='http://localhost:3000'
8+
local vus='2'
9+
local iterationsOption=''
910

10-
while getopts ":d:u:v:" o; do
11+
while getopts ":d:i:u:v:" o; do
1112
case "${o}" in
1213
d)
1314
duration=${OPTARG}
1415
;;
16+
i)
17+
iterationsOption="--iterations ${OPTARG}"
18+
;;
1519
u)
1620
url=${OPTARG}
1721
;;
@@ -22,7 +26,17 @@ run() {
2226
done
2327
shift $((OPTIND-1))
2428

25-
docker run -t --network=host -v $PWD:/src -e URL=$url --rm -i loadimpact/k6:master run --vus $vus --duration $duration src/render_test.js
29+
docker run \
30+
-it \
31+
--network=host \
32+
--mount type=bind,source=$PWD,destination=/src \
33+
-e URL=$url \
34+
--rm \
35+
loadimpact/k6:master run \
36+
--vus $vus \
37+
--duration $duration \
38+
$iterationsOption \
39+
//src/render_test.js
2640
}
2741

2842
run "$@"

0 commit comments

Comments
 (0)