| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import http from "k6/http"; |
| import { check, sleep } from "k6"; |
| import { Rate, Trend } from "k6/metrics"; |
|
|
| |
| const errorRate = new Rate("errors"); |
| const chatLatency = new Trend("chat_latency", true); |
| const healthLatency = new Trend("health_latency", true); |
|
|
| |
| const BASE_URL = __ENV.BASE_URL || "http://localhost:3000"; |
| const API_KEY = __ENV.API_KEY || __ENV.OMNIROUTE_API_KEY || "test-key"; |
| const VUS = parseInt(__ENV.VUS || "10", 10); |
| const DURATION = __ENV.DURATION || "60s"; |
|
|
| export const options = { |
| scenarios: { |
| |
| chat_stress: { |
| executor: "ramping-vus", |
| startVUs: 1, |
| stages: [ |
| { duration: "10s", target: VUS }, |
| { duration: DURATION, target: VUS }, |
| { duration: "10s", target: 0 }, |
| ], |
| exec: "chatCompletions", |
| }, |
| |
| health_check: { |
| executor: "constant-vus", |
| vus: 2, |
| duration: DURATION, |
| exec: "healthCheck", |
| }, |
| }, |
| thresholds: { |
| http_req_duration: ["p(95)<5000"], |
| errors: ["rate<0.1"], |
| chat_latency: ["p(50)<3000", "p(95)<8000"], |
| health_latency: ["p(95)<500"], |
| }, |
| }; |
|
|
| |
| const headers = { |
| "Content-Type": "application/json", |
| Authorization: `Bearer ${API_KEY}`, |
| }; |
|
|
| |
|
|
| |
| |
| |
| |
| export function chatCompletions() { |
| const payload = JSON.stringify({ |
| model: "gpt-4o-mini", |
| messages: [{ role: "user", content: "Say hello in one word." }], |
| temperature: 0, |
| max_tokens: 10, |
| stream: false, |
| }); |
|
|
| const res = http.post(`${BASE_URL}/v1/chat/completions`, payload, { |
| headers, |
| timeout: "15s", |
| }); |
|
|
| chatLatency.add(res.timings.duration); |
|
|
| const passed = check(res, { |
| "status is 200": (r) => r.status === 200, |
| "has choices": (r) => { |
| try { |
| const body = JSON.parse(r.body); |
| return body.choices && body.choices.length > 0; |
| } catch { |
| return false; |
| } |
| }, |
| "response time < 10s": (r) => r.timings.duration < 10000, |
| }); |
|
|
| errorRate.add(!passed); |
| sleep(0.5); |
| } |
|
|
| |
| |
| |
| export function healthCheck() { |
| const res = http.get(`${BASE_URL}/api/health`, { |
| headers: { Authorization: `Bearer ${API_KEY}` }, |
| timeout: "5s", |
| }); |
|
|
| healthLatency.add(res.timings.duration); |
|
|
| const passed = check(res, { |
| "health status 200": (r) => r.status === 200, |
| "response time < 1s": (r) => r.timings.duration < 1000, |
| }); |
|
|
| errorRate.add(!passed); |
| sleep(2); |
| } |
|
|
| |
| |
| |
| export function handleSummary(data) { |
| const summary = { |
| timestamp: new Date().toISOString(), |
| scenarios: Object.keys(options.scenarios), |
| metrics: { |
| http_reqs: data.metrics.http_reqs?.values?.count || 0, |
| avg_duration_ms: Math.round(data.metrics.http_req_duration?.values?.avg || 0), |
| p95_duration_ms: Math.round(data.metrics.http_req_duration?.values?.["p(95)"] || 0), |
| p99_duration_ms: Math.round(data.metrics.http_req_duration?.values?.["p(99)"] || 0), |
| error_rate: (data.metrics.errors?.values?.rate || 0).toFixed(4), |
| }, |
| }; |
|
|
| return { |
| stdout: `\nπ Load Test Summary\n${JSON.stringify(summary, null, 2)}\n`, |
| "tests/load/results.json": JSON.stringify(summary, null, 2), |
| }; |
| } |
|
|