From 540ca5ebad362c531d22a70dfc9ebf444be9dfa5 Mon Sep 17 00:00:00 2001 From: Michael Freno Date: Sat, 9 May 2026 06:18:47 -0400 Subject: [PATCH] Add k6 load testing infrastructure for Darkwatch service - Create load test directory structure (infra/load-tests/) - Implement k6 script for Darkwatch endpoints (darkwatch.js) - Tests watchlist, scan, exposure, and alert operations - Configured for 500 req/s sustained load with P99 < 200ms - Includes error rate metrics and threshold validation - Add documentation and usage guide (README.md) Related: [FRE-4807](/FRE/issues/FRE-4807) Co-Authored-By: Paperclip --- infra/load-tests/README.md | 61 ++++++++++++++++ infra/load-tests/src/darkwatch.js | 112 ++++++++++++++++++++++++++++++ 2 files changed, 173 insertions(+) create mode 100644 infra/load-tests/README.md create mode 100644 infra/load-tests/src/darkwatch.js diff --git a/infra/load-tests/README.md b/infra/load-tests/README.md new file mode 100644 index 0000000..657b85f --- /dev/null +++ b/infra/load-tests/README.md @@ -0,0 +1,61 @@ +# ShieldAI Load Tests + +k6 load testing suite for ShieldAI services. + +## Prerequisites + +- k6 v0.45+ installed +- Target services running on staging environment +- Authentication tokens for API access + +## Running Tests + +### Local Execution + +```bash +# Run against local development environment +k6 run --env BASE_URL=http://localhost:3000 --env AUTH_TOKEN=dev-token src/darkwatch.js + +# Run with results output +k6 run --out json=results.json src/darkwatch.js +``` + +### CI/CD Execution + +```bash +# Run on staging environment +k6 run --env BASE_URL=https://staging-api.freno.me --env AUTH_TOKEN=$STAGING_AUTH_TOKEN src/darkwatch.js +``` + +## Test Configuration + +Each test script includes: + +- **Stages**: Ramp-up, sustained load, ramp-down +- **Thresholds**: P99 latency and error rate limits +- **Metrics**: Custom metrics for error tracking + +### Current Thresholds + +| Service | P99 Latency | Error Rate | +|---------|-------------|------------| +| Darkwatch | < 200ms | < 1% | + +## Metrics Collection + +Run with output options: + +```bash +# JSON output for analysis +k6 run --out json=darkwatch-results.json src/darkwatch.js + +# InfluxDB for visualization +k6 run --out influxdb=http://influxdb:8086/k6 src/darkwatch.js +``` + +## Next Steps + +1. Create load test scripts for Spamshield and Voiceprint +2. Integrate with GitHub Actions CI pipeline +3. Set up metrics visualization dashboard +4. Configure alerting on threshold breaches diff --git a/infra/load-tests/src/darkwatch.js b/infra/load-tests/src/darkwatch.js new file mode 100644 index 0000000..d27c81c --- /dev/null +++ b/infra/load-tests/src/darkwatch.js @@ -0,0 +1,112 @@ +import http from 'k6/http'; +import { check, group } from 'k6'; +import { Rate } from 'k6/metrics'; + +// Custom metrics +const errorRate = new Rate('errors'); + +// Test configuration +export const options = { + stages: [ + { duration: '30s', target: 100 }, // Ramp up to 100 users + { duration: '2m', target: 500 }, // Ramp to 500 req/s + { duration: '3m', target: 500 }, // Stay at 500 req/s for 3 minutes + { duration: '30s', target: 0 }, // Ramp down to 0 + ], + thresholds: { + http_req_duration: ['p(99)<200'], // P99 latency < 200ms + errors: ['rate<0.01'], // Error rate < 1% + }, +}; + +const BASE_URL = __ENV.BASE_URL || 'http://localhost:3000'; + +export default function () { + group('Watchlist Operations', function () { + // GET /watchlist + const watchlistRes = http.get(`${BASE_URL}/watchlist`, { + headers: { 'Authorization': `Bearer ${getAuthToken()}` }, + }); + + check(watchlistRes, { + 'watchlist GET status is 200': (r) => r.status === 200, + 'watchlist GET P99 < 100ms': (r) => r.timings.duration < 100, + }); + errorRate.add(watchlistRes.status !== 200); + + // POST /watchlist + const newItemRes = http.post( + `${BASE_URL}/watchlist`, + JSON.stringify({ type: 'email', value: `test${Date()}@example.com` }), + { + headers: { + 'Authorization': `Bearer ${getAuthToken()}`, + 'Content-Type': 'application/json', + }, + } + ); + + check(newItemRes, { + 'watchlist POST status is 201': (r) => r.status === 201, + 'watchlist POST P99 < 200ms': (r) => r.timings.duration < 200, + }); + errorRate.add(newItemRes.status !== 201); + }); + + group('Scan Operations', function () { + // POST /scan + const scanRes = http.post( + `${BASE_URL}/scan`, + {}, + { + headers: { 'Authorization': `Bearer ${getAuthToken()}` }, + } + ); + + check(scanRes, { + 'scan POST status is 200': (r) => r.status === 200, + 'scan POST P99 < 150ms': (r) => r.timings.duration < 150, + }); + errorRate.add(scanRes.status !== 200); + + // GET /scan/schedule + const scheduleRes = http.get(`${BASE_URL}/scan/schedule`, { + headers: { 'Authorization': `Bearer ${getAuthToken()}` }, + }); + + check(scheduleRes, { + 'schedule GET status is 200': (r) => r.status === 200, + 'schedule GET P99 < 100ms': (r) => r.timings.duration < 100, + }); + errorRate.add(scheduleRes.status !== 200); + }); + + group('Exposure and Alert Operations', function () { + // GET /exposures + const exposuresRes = http.get(`${BASE_URL}/exposures`, { + headers: { 'Authorization': `Bearer ${getAuthToken()}` }, + }); + + check(exposuresRes, { + 'exposures GET status is 200': (r) => r.status === 200, + 'exposures GET P99 < 150ms': (r) => r.timings.duration < 150, + }); + errorRate.add(exposuresRes.status !== 200); + + // GET /alerts + const alertsRes = http.get(`${BASE_URL}/alerts`, { + headers: { 'Authorization': `Bearer ${getAuthToken()}` }, + }); + + check(alertsRes, { + 'alerts GET status is 200': (r) => r.status === 200, + 'alerts GET P99 < 150ms': (r) => r.timings.duration < 150, + }); + errorRate.add(alertsRes.status !== 200); + }); +} + +// Helper function to get auth token (replace with actual token retrieval) +function getAuthToken() { + return __ENV.AUTH_TOKEN || 'test-token'; +}