Add load testing job to GitHub Actions CI pipeline (FRE-4931)
- Add load-test job to ci.yml that runs after docker-build on push to main - Create combined load test runner (scripts/load-test/run-all.sh) for all services - Create k6 load test scripts for api, darkwatch, spamshield, and voiceprint - Add shared k6 utilities (lib/common.js) - Update load-test.yml to support all services and report artifacts - Configure k6 cloud output and P99 threshold validation - Generate load test report as CI artifact Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
48
scripts/load-test/services/api.js
Normal file
48
scripts/load-test/services/api.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import http from 'k6/http';
|
||||
import { check, group } from 'k6';
|
||||
import { Rate, Trend } from 'k6/metrics';
|
||||
import { getBaseUrl, getTargetRps, getDuration, defaultThresholds, checkResponse, randomString } from '../lib/common.js';
|
||||
|
||||
const errorRate = new Rate('errors');
|
||||
const notificationLatency = new Trend('notification_p99');
|
||||
const correlationLatency = new Trend('correlation_p99');
|
||||
|
||||
export const options = {
|
||||
thresholds: {
|
||||
...defaultThresholds(250).thresholds,
|
||||
notification_p99: ['p(99)<500'],
|
||||
correlation_p99: ['p(99)<300'],
|
||||
},
|
||||
};
|
||||
|
||||
const BASE_URL = getBaseUrl();
|
||||
const AUTH_TOKEN = __ENV.API_TOKEN || 'test-token';
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${AUTH_TOKEN}`,
|
||||
};
|
||||
|
||||
export default function () {
|
||||
group('API Health', function () {
|
||||
const res = http.get(`${BASE_URL}/health`, { headers });
|
||||
checkResponse(res, 200);
|
||||
});
|
||||
|
||||
group('Notifications', function () {
|
||||
const payload = JSON.stringify({
|
||||
userId: `user-${randomString()}`,
|
||||
channel: 'email',
|
||||
message: 'Load test notification',
|
||||
});
|
||||
const res = http.post(`${BASE_URL}/notifications`, payload, { headers });
|
||||
checkResponse(res, 200);
|
||||
notificationLatency.add(res.timings.duration);
|
||||
});
|
||||
|
||||
group('Correlation', function () {
|
||||
const res = http.get(`${BASE_URL}/correlation/events?limit=10`, { headers });
|
||||
checkResponse(res, 200);
|
||||
correlationLatency.add(res.timings.duration);
|
||||
});
|
||||
}
|
||||
55
scripts/load-test/services/darkwatch.js
Normal file
55
scripts/load-test/services/darkwatch.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import http from 'k6/http';
|
||||
import { check, group } from 'k6';
|
||||
import { Rate, Trend } from 'k6/metrics';
|
||||
import { getBaseUrl, getTargetRps, getDuration, defaultThresholds, checkResponse, randomString } from '../lib/common.js';
|
||||
|
||||
const errorRate = new Rate('errors');
|
||||
const scanLatency = new Trend('scan_p99');
|
||||
const watchlistLatency = new Trend('watchlist_p99');
|
||||
const alertLatency = new Trend('alert_p99');
|
||||
|
||||
export const options = {
|
||||
thresholds: {
|
||||
...defaultThresholds(200).thresholds,
|
||||
scan_p99: ['p(99)<300'],
|
||||
watchlist_p99: ['p(99)<200'],
|
||||
alert_p99: ['p(99)<250'],
|
||||
},
|
||||
};
|
||||
|
||||
const BASE_URL = getBaseUrl();
|
||||
const AUTH_TOKEN = __ENV.API_TOKEN || 'test-token';
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${AUTH_TOKEN}`,
|
||||
};
|
||||
|
||||
export default function () {
|
||||
group('Darkwatch Scan', function () {
|
||||
const payload = JSON.stringify({
|
||||
type: 'email',
|
||||
value: `loadtest-${randomString()}@example.com`,
|
||||
});
|
||||
const res = http.post(`${BASE_URL}/darkwatch/scan`, payload, { headers });
|
||||
checkResponse(res, 200);
|
||||
scanLatency.add(res.timings.duration);
|
||||
});
|
||||
|
||||
group('Watchlist', function () {
|
||||
const res = http.get(`${BASE_URL}/watchlist?page=1&limit=20`, { headers });
|
||||
checkResponse(res, 200);
|
||||
watchlistLatency.add(res.timings.duration);
|
||||
});
|
||||
|
||||
group('Alerts', function () {
|
||||
const res = http.get(`${BASE_URL}/alerts?status=open&limit=10`, { headers });
|
||||
checkResponse(res, 200);
|
||||
alertLatency.add(res.timings.duration);
|
||||
});
|
||||
|
||||
group('Exposure Check', function () {
|
||||
const res = http.get(`${BASE_URL}/exposure/summary`, { headers });
|
||||
checkResponse(res, 200);
|
||||
});
|
||||
}
|
||||
48
scripts/load-test/services/spamshield.js
Normal file
48
scripts/load-test/services/spamshield.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import http from 'k6/http';
|
||||
import { check, group } from 'k6';
|
||||
import { Rate, Trend } from 'k6/metrics';
|
||||
import { getBaseUrl, getTargetRps, getDuration, defaultThresholds, checkResponse, randomString } from '../lib/common.js';
|
||||
|
||||
const errorRate = new Rate('errors');
|
||||
const classificationLatency = new Trend('classification_p99');
|
||||
const engineLatency = new Trend('engine_p99');
|
||||
|
||||
export const options = {
|
||||
thresholds: {
|
||||
...defaultThresholds(200).thresholds,
|
||||
classification_p99: ['p(99)<300'],
|
||||
engine_p99: ['p(99)<250'],
|
||||
},
|
||||
};
|
||||
|
||||
const BASE_URL = getBaseUrl();
|
||||
const AUTH_TOKEN = __ENV.API_TOKEN || 'test-token';
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${AUTH_TOKEN}`,
|
||||
};
|
||||
|
||||
export default function () {
|
||||
group('SMS Classification', function () {
|
||||
const payload = JSON.stringify({
|
||||
message: `Load test message ${randomString(8)} - please check if this is spam`,
|
||||
from: `+1555${String(Math.floor(1000000 + Math.random() * 9000000))}`,
|
||||
to: `+1555${String(Math.floor(1000000 + Math.random() * 9000000))}`,
|
||||
});
|
||||
const res = http.post(`${BASE_URL}/spamshield/classify`, payload, { headers });
|
||||
checkResponse(res, 200);
|
||||
classificationLatency.add(res.timings.duration);
|
||||
});
|
||||
|
||||
group('Spam Engine Health', function () {
|
||||
const res = http.get(`${BASE_URL}/spamshield/health`, { headers });
|
||||
checkResponse(res, 200);
|
||||
engineLatency.add(res.timings.duration);
|
||||
});
|
||||
|
||||
group('Blocklist Check', function () {
|
||||
const res = http.get(`${BASE_URL}/spamshield/blocklist/check?phone=+1555${String(Math.floor(1000000 + Math.random() * 9000000))}`, { headers });
|
||||
checkResponse(res, 200);
|
||||
});
|
||||
}
|
||||
56
scripts/load-test/services/voiceprint.js
Normal file
56
scripts/load-test/services/voiceprint.js
Normal file
@@ -0,0 +1,56 @@
|
||||
import http from 'k6/http';
|
||||
import { check, group } from 'k6';
|
||||
import { Rate, Trend } from 'k6/metrics';
|
||||
import { getBaseUrl, getTargetRps, getDuration, defaultThresholds, checkResponse, randomString } from '../lib/common.js';
|
||||
|
||||
const errorRate = new Rate('errors');
|
||||
const enrollmentLatency = new Trend('enrollment_p99');
|
||||
const verificationLatency = new Trend('verification_p99');
|
||||
const modelLatency = new Trend('model_retrieval_p99');
|
||||
|
||||
export const options = {
|
||||
thresholds: {
|
||||
...defaultThresholds(250).thresholds,
|
||||
enrollment_p99: ['p(99)<500'],
|
||||
verification_p99: ['p(99)<250'],
|
||||
model_retrieval_p99: ['p(99)<100'],
|
||||
},
|
||||
};
|
||||
|
||||
const BASE_URL = getBaseUrl();
|
||||
const AUTH_TOKEN = __ENV.API_TOKEN || 'test-token';
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${AUTH_TOKEN}`,
|
||||
};
|
||||
|
||||
export default function () {
|
||||
group('Voiceprint Enrollment', function () {
|
||||
const payload = JSON.stringify({
|
||||
userId: `loadtest-${randomString()}`,
|
||||
audioSample: 'base64-encoded-audio-data-placeholder',
|
||||
sampleRate: 16000,
|
||||
});
|
||||
const res = http.post(`${BASE_URL}/voiceprint/enroll`, payload, { headers });
|
||||
checkResponse(res, 200);
|
||||
enrollmentLatency.add(res.timings.duration);
|
||||
});
|
||||
|
||||
group('Voiceprint Verification', function () {
|
||||
const payload = JSON.stringify({
|
||||
userId: `loadtest-${randomString()}`,
|
||||
audioSample: 'base64-encoded-audio-data-placeholder',
|
||||
sampleRate: 16000,
|
||||
});
|
||||
const res = http.post(`${BASE_URL}/voiceprint/verify`, payload, { headers });
|
||||
checkResponse(res, 200);
|
||||
verificationLatency.add(res.timings.duration);
|
||||
});
|
||||
|
||||
group('Model Retrieval', function () {
|
||||
const res = http.get(`${BASE_URL}/voiceprint/model/${randomString()}`, { headers });
|
||||
checkResponse(res, 200);
|
||||
modelLatency.add(res.timings.duration);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user