import http from 'k6/http'; import { check } from 'k6'; import { Counter } from 'k6/metrics'; import { Trend } from 'k6/metrics'; import exec from 'k6/execution'; const API_URL = __ENV.API_URL || 'http://localhost:8000/api/v1/cdr/raw'; const PROFILE = (__ENV.PROFILE || 'assessment').toLowerCase(); const P95_MS = Number(__ENV.P95_MS || 50); // Assessment profile directly models the required scenario: // 500 records in <5s. 125 iterations/s for 4 seconds schedules exactly 500 iterations. const assessmentScenario = { executor: 'constant-arrival-rate', rate: 125, timeUnit: '1s', duration: '4s', preAllocatedVUs: 50, maxVUs: 250, gracefulStop: '5s', }; // Optional heavier capacity test. Run with PROFILE=stress. const stressScenario = { executor: 'ramping-arrival-rate', startRate: 250, timeUnit: '1s', preAllocatedVUs: 250, maxVUs: 1000, stages: [ { duration: '10s', target: 500 }, { duration: '10s', target: 1000 }, { duration: '30s', target: 1000 }, { duration: '5s', target: 0 }, ], }; export const options = { discardResponseBodies: true, scenarios: { ingestion: PROFILE === 'stress' ? stressScenario : assessmentScenario, }, thresholds: { // 429 is a failure for the assessment burst. k6 also treats 4xx as failed HTTP requests. http_req_failed: ['rate==0'], checks: ['rate==1'], dropped_iterations: ['count==0'], ...(PROFILE === 'assessment' ? { accepted_202: ['count==500'], assessment_completion_ms: ['max<5000'], } : {}), // This is a project performance guardrail, not an assessment requirement. // Override it with P95_MS=100 (etc.) if you want a different SLA. http_req_duration: [`p(95)<${P95_MS}`], }, }; const accepted202 = new Counter('accepted_202'); const rejected429 = new Counter('rejected_429'); const unexpectedStatus = new Counter('unexpected_status'); const completionTime = new Trend('assessment_completion_ms'); export default function () { const now = Date.now(); const rawId = `k6-${PROFILE}-${__VU}-${__ITER}-${now}`; const payload = JSON.stringify({ raw_id: rawId, src_ip: '10.240.1.1', device_id: '310410998877665', caller_num: '+19875551234', svc: '4G_DATA', up_bytes: 1048576, down_bytes: 4194304, rat: 6, lac: '4021', ci: '8812', epoch_start: Math.floor(now / 1000) - 300, epoch_end: Math.floor(now / 1000), }); const res = http.post(API_URL, payload, { headers: { 'Content-Type': 'application/json' }, tags: { test_profile: PROFILE }, }); completionTime.add(Date.now() - exec.scenario.startTime); if (res.status === 202) { accepted202.add(1); } else if (res.status === 429) { rejected429.add(1); } else { unexpectedStatus.add(1); } check(res, { 'accepted with 202': (r) => r.status === 202, 'not backpressured with 429': (r) => r.status !== 429, }); } export function handleSummary(data) { const reqs = data.metrics.http_reqs?.values?.count || 0; const failedRate = data.metrics.http_req_failed?.values?.rate || 0; const p95 = data.metrics.http_req_duration?.values?.['p(95)']; const dropped = data.metrics.dropped_iterations?.values?.count || 0; const accepted = data.metrics.accepted_202?.values?.count || 0; const rejected = data.metrics.rejected_429?.values?.count || 0; const unexpected = data.metrics.unexpected_status?.values?.count || 0; const completionMs = data.metrics.assessment_completion_ms?.values?.max; const lines = [ '', '=== CDR Ingestion Load-Test Summary ===', `Profile: ${PROFILE}`, `Requests attempted: ${reqs}`, `202 Accepted: ${accepted}`, `429 Backpressure: ${rejected}`, `Unexpected statuses: ${unexpected}`, `Dropped iterations: ${dropped}`, `HTTP failure rate: ${(failedRate * 100).toFixed(2)}%`, `p95 latency: ${p95 !== undefined ? p95.toFixed(2) + ' ms' : 'n/a'}`, ]; if (PROFILE === 'assessment') { const assessmentPass = reqs === 500 && accepted === 500 && rejected === 0 && unexpected === 0 && dropped === 0 && completionMs < 5000; lines.push( 'Assessment target: 500 acknowledged records in <5s, no 429, no dropped iterations', `Last completion: ${completionMs !== undefined ? completionMs.toFixed(2) + ' ms' : 'unavailable'}`, `Assessment result: ${assessmentPass ? 'PASS' : 'FAIL'}`, 'Note: k6 proves API-side burst handling; run generate_traffic.py', ' to verify end-to-end ClickHouse/DLQ/deduplication behavior.' ); } else { lines.push( 'Stress target: sustain the configured arrival rate without errors/drops.' ); } lines.push('=======================================', ''); return { stdout: `${lines.join('\n')}\n`, }; }