Skip to content

Commit

Permalink
Add self-contained setup for load test (#275)
Browse files Browse the repository at this point in the history
* 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 <agnes.toulet@grafana.com>
  • Loading branch information
pianohacker and AgnesToulet authored Sep 9, 2021
1 parent af78b2d commit 4f3a10f
Show file tree
Hide file tree
Showing 6 changed files with 240 additions and 37 deletions.
5 changes: 5 additions & 0 deletions devenv/loadtest/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ Run load test for custom duration:
$ ./run.sh -d 10s
```

Run only 1 iteration of the load test (useful for testing):

```bash
$ ./run.sh -i 1

Run load test for custom target url:

```bash
Expand Down
File renamed without changes.
121 changes: 109 additions & 12 deletions devenv/loadtest/modules/client.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,66 @@
import http from "k6/http";

export const DatasourcesEndpoint = class DatasourcesEndpoint {
constructor(httpClient) {
this.httpClient = httpClient;
}

getAll() {
return this.httpClient.get('/datasources');
}

getById(id) {
return this.httpClient.get(`/datasources/${id}`);
}

getByName(name) {
return this.httpClient.get(`/datasources/name/${name}`);
}

create(payload) {
return this.httpClient.post(`/datasources`, JSON.stringify(payload));
}

update(id, payload) {
return this.httpClient.put(`/datasources/${id}`, JSON.stringify(payload));
}
};

export const DashboardsEndpoint = class DashboardsEndpoint {
constructor(httpClient) {
this.httpClient = httpClient;
}

getAll() {
return this.httpClient.get('/dashboards');
}

upsert(payload) {
return this.httpClient.post(`/dashboards/db`, JSON.stringify(payload));
}
};

export const OrganizationsEndpoint = class OrganizationsEndpoint {
constructor(httpClient) {
this.httpClient = httpClient;
}

getById(id) {
return this.httpClient.get(`/orgs/${id}`);
}

getByName(name) {
return this.httpClient.get(`/orgs/name/${name}`);
}

create(name) {
let payload = {
name: name,
};
return this.httpClient.post(`/orgs`, JSON.stringify(payload));
}
};

export const UIEndpoint = class UIEndpoint {
constructor(httpClient) {
this.httpClient = httpClient;
Expand All @@ -10,32 +71,53 @@ export const UIEndpoint = class UIEndpoint {
return this.httpClient.formPost('/login', payload);
}

render() {
return this.httpClient.get('/render/d-solo/_CPokraWz/graph-panel?orgId=1&panelId=1&width=1000&height=500&tz=Europe%2FStockholm')
renderPanel(orgId, dashboardUid, panelId) {
return this.httpClient.get(
`/render/d-solo/${dashboardUid}/graph-panel`,
{
orgId,
panelId,
width: 1000,
height: 500,
tz: 'Europe/Stockholm',
}
);
}
}

export const GrafanaClient = class GrafanaClient {
constructor(httpClient) {
httpClient.onBeforeRequest = this.onBeforeRequest;
httpClient.onBeforeRequest = (params) => {
if (this.orgId && this.orgId > 0) {
params.headers = params.headers || {};
params.headers["X-Grafana-Org-Id"] = this.orgId;
}
}

this.raw = httpClient;
this.dashboards = new DashboardsEndpoint(httpClient.withUrl('/api'));
this.datasources = new DatasourcesEndpoint(httpClient.withUrl('/api'));
this.orgs = new OrganizationsEndpoint(httpClient.withUrl('/api'));
this.ui = new UIEndpoint(httpClient);
}

loadCookies(cookies) {
for (let [name, value] of Object.entries(cookies)) {
http.cookieJar().set(this.raw.url, name, value);
}
}

saveCookies() {
return http.cookieJar().cookiesForURL(this.raw.url + '/');
}

batch(requests) {
return this.raw.batch(requests);
}

withOrgId(orgId) {
this.orgId = orgId;
}

onBeforeRequest(params) {
if (this.orgId && this.orgId > 0) {
params = params.headers || {};
params.headers["X-Grafana-Org-Id"] = this.orgId;
}
}
}

export const BaseClient = class BaseClient {
Expand All @@ -62,10 +144,16 @@ export const BaseClient = class BaseClient {

}

get(url, params) {
get(url, queryParams, params) {
params = params || {};
this.beforeRequest(params);
this.onBeforeRequest(params);

if (queryParams) {
url += '?' + Array.from(Object.entries(queryParams)).map(([key, value]) =>
`${key}=${encodeURIComponent(value)}`
).join('&');
}

return http.get(this.url + url, params);
}

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

put(url, body, params) {
params = params || {};
params.headers = params.headers || {};
params.headers['Content-Type'] = 'application/json';

this.onBeforeRequest(params);
return http.put(this.url + url, body, params);
}

delete(url, params) {
params = params || {};
this.beforeRequest(params);
Expand Down
64 changes: 64 additions & 0 deletions devenv/loadtest/modules/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export const createTestOrgIfNotExists = (client) => {
let orgId = 0;

let res = client.orgs.getByName('k6-image-renderer');
if (res.status === 404) {
res = client.orgs.create('k6-image-renderer');
if (res.status !== 200) {
throw new Error('Expected 200 response status when creating org');
}
return res.json().orgId;
}

// This can happen e.g. in Hosted Grafana instances, where even admins
// cannot see organisations
if (res.status !== 200) {
console.info(`unable to get orgs from instance, continuing with default orgId ${orgId}`);
return orgId;
}

return res.json().id;
};

export const upsertTestdataDatasource = (client, name) => {
const payload = {
access: 'proxy',
isDefault: false,
name,
type: 'testdata',
};

let res = client.datasources.getByName(payload.name);
let id;
if (res.status === 404) {
res = client.datasources.create(payload);

if (res.status == 200) {
id = res.json().id;
}
} else if (res.status == 200) {
id = res.json().id;
res = client.datasources.update(res.json().id, payload);
}

if (res.status !== 200) {
throw new Error(`expected 200 response status when creating datasource, got ${res.status}`);
}

return id;
};

export const upsertDashboard = (client, dashboard) => {
const payload = {
dashboard,
overwrite: true,
};

let res = client.dashboards.upsert(payload);

if (res.status !== 200) {
throw new Error(`expected 200 response status when creating dashboards, got ${res.status}`);
}

return res.json().id;
};
61 changes: 42 additions & 19 deletions devenv/loadtest/render_test.js
Original file line number Diff line number Diff line change
@@ -1,33 +1,56 @@
import { check, group } from 'k6';
import { createClient } from './modules/client.js';
import {
createTestOrgIfNotExists,
upsertTestdataDatasource,
upsertDashboard,
} from './modules/util.js';

export let options = {
noCookiesReset: true
noCookiesReset: true,
thresholds: { checks: [ { threshold: 'rate=1', abortOnFail: true } ] },
};

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

export default () => {
group("render test", () => {
if (__ITER === 0) {
group("user authenticates thru ui with username and password", () => {
let res = client.ui.login('admin', 'admin');
export const setup = () => {
group("user authenticates thru ui with username and password", () => {
let res = client.ui.login('admin', 'admin');

check(res, {
'response status is 200': (r) => r.status === 200,
});
});
}

if (__ITER !== 0) {
group("render graph panel", () => {
const response = client.ui.render();
check(response, {
'response status is 200': (r) => r.status === 200,
});
check(res, {
'response status is 200': (r) => r.status === 200,
});
});

const orgId = createTestOrgIfNotExists(client);
client.withOrgId(orgId);
upsertTestdataDatasource(client, dashboard.panels[0].datasource);
upsertDashboard(client, dashboard);

return {
orgId,
cookies: client.saveCookies(),
};
};

export default (data) => {
client.loadCookies(data.cookies);
client.withOrgId(data.orgId);

group("render test", () => {
group("render graph panel", () => {
const response = client.ui.renderPanel(
data.orgId,
dashboard.uid,
dashboard.panels[0].id,
);
check(response, {
'response status is 200': (r) => r.status === 200,
'response is a PNG': (r) => r.headers['Content-Type'] == 'image/png',
});
}
});
});
}

Expand Down
26 changes: 20 additions & 6 deletions devenv/loadtest/run.sh
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
#/bin/bash

PWD=$(pwd)
cd "$(dirname $0)"

run() {
duration='15m'
url='http://localhost:3000'
vus='2'
local duration='15m'
local url='http://localhost:3000'
local vus='2'
local iterationsOption=''

while getopts ":d:u:v:" o; do
while getopts ":d:i:u:v:" o; do
case "${o}" in
d)
duration=${OPTARG}
;;
i)
iterationsOption="--iterations ${OPTARG}"
;;
u)
url=${OPTARG}
;;
Expand All @@ -22,7 +26,17 @@ run() {
done
shift $((OPTIND-1))

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
docker run \
-it \
--network=host \
--mount type=bind,source=$PWD,destination=/src \
-e URL=$url \
--rm \
loadimpact/k6:master run \
--vus $vus \
--duration $duration \
$iterationsOption \
//src/render_test.js
}

run "$@"

0 comments on commit 4f3a10f

Please sign in to comment.