/** * WalkGuide — Custom Metric Collectors * Menambah metric k6 custom di atas default http_req_* metrics. * * METRIC YANG DIKUMPULKAN (sesuai ketentuan pillar 3): * 1. throughput — req/s (via http_reqs counter bawaan k6 + custom trend) * 2. p95 latency — custom trend per endpoint * 3. error rate — custom rate * 4. db query time — diperkirakan via response time karena tidak ada akses langsung DB * 5. jvm heap — via Spring Actuator /actuator/metrics/jvm.memory.used */ import { Counter, Gauge, Rate, Trend } from "k6/metrics"; import http from "k6/http"; // ── Custom Metrics ──────────────────────────────────────────────────────────── /** Total request error rate (non-2xx) */ export const errorRate = new Rate("walkguide_error_rate"); /** Request latency per endpoint */ export const endpointLatency = new Trend("walkguide_endpoint_latency_ms", true); /** Location update latency khusus (endpoint paling kritis) */ export const locationLatency = new Trend("walkguide_location_latency_ms", true); /** Obstacle log latency khusus */ export const obstacleLatency = new Trend("walkguide_obstacle_latency_ms", true); /** SOS trigger latency khusus (harus paling cepat) */ export const sosLatency = new Trend("walkguide_sos_latency_ms", true); /** Notification send latency */ export const notifLatency = new Trend("walkguide_notif_latency_ms", true); /** Auth latency */ export const authLatency = new Trend("walkguide_auth_latency_ms", true); /** Timeline query latency */ export const timelineLatency = new Trend("walkguide_timeline_latency_ms", true); /** Pairing flow latency */ export const pairingLatency = new Trend("walkguide_pairing_latency_ms", true); /** Successful requests counter */ export const successfulRequests = new Counter("walkguide_successful_requests"); /** Failed requests counter */ export const failedRequests = new Counter("walkguide_failed_requests"); /** JVM heap usage (MB) — populated via actuator polling */ export const jvmHeapUsed = new Gauge("walkguide_jvm_heap_used_mb"); /** Active DB connections estimate (via actuator) */ export const dbConnections = new Gauge("walkguide_db_connections"); // ── Endpoint → metric mapping ───────────────────────────────────────────────── const ENDPOINT_METRIC_MAP = { location_update: locationLatency, obstacle_log: obstacleLatency, sos_trigger: sosLatency, send_notification: notifLatency, login: authLatency, register: authLatency, refresh_token: authLatency, location_history: timelineLatency, activity_logs: timelineLatency, activity_logs_guardian: timelineLatency, pairing_invite: pairingLatency, pairing_respond: pairingLatency, }; // ── metricsHelper object ────────────────────────────────────────────────────── export const metricsHelper = { /** * Catat metric untuk setiap response. * @param {string} endpointTag — string yang di-pass sebagai tags.endpoint * @param {Response} res — k6 http response object */ recordEndpoint(endpointTag, res) { const isSuccess = res.status >= 200 && res.status < 300; const durationMs = res.timings.duration; // Error rate errorRate.add(!isSuccess); // Generic endpoint latency endpointLatency.add(durationMs, { endpoint: endpointTag }); // Specific metric kalau ada di map if (ENDPOINT_METRIC_MAP[endpointTag]) { ENDPOINT_METRIC_MAP[endpointTag].add(durationMs); } // Success / fail counter if (isSuccess) { successfulRequests.add(1); } else { failedRequests.add(1); } }, /** * Poll Spring Actuator untuk JVM heap. * Call ini dari setup() atau secara periodik dari satu VU khusus. * @param {string} baseUrl * @param {string} token — optional, actuator mungkin butuh auth */ pollJvmHeap(baseUrl, token = null) { const headers = token ? { Authorization: `Bearer ${token}`, Accept: "application/json" } : { Accept: "application/json" }; try { const res = http.get(`${baseUrl}/actuator/metrics/jvm.memory.used`, { headers, timeout: "5s", tags: { endpoint: "actuator_jvm" }, }); if (res.status === 200) { const body = JSON.parse(res.body); // measurement[0].value adalah bytes const heapMb = body.measurements[0].value / (1024 * 1024); jvmHeapUsed.add(heapMb); } } catch (e) { // Actuator mungkin tidak public — skip } }, /** * Poll Spring Actuator untuk DB connection pool (HikariCP). * @param {string} baseUrl * @param {string} token */ pollDbConnections(baseUrl, token = null) { const headers = token ? { Authorization: `Bearer ${token}`, Accept: "application/json" } : { Accept: "application/json" }; try { const res = http.get( `${baseUrl}/actuator/metrics/hikaricp.connections.active`, { headers, timeout: "5s", tags: { endpoint: "actuator_db" } }, ); if (res.status === 200) { const body = JSON.parse(res.body); const active = body.measurements[0].value; dbConnections.add(active); } } catch (e) { // Skip } }, /** * Generate ringkasan teks dari custom metrics (untuk print di teardown). */ summary() { return { errorRate: errorRate, locationP95: locationLatency, obstacleP95: obstacleLatency, sosP95: sosLatency, notifP95: notifLatency, authP95: authLatency, timelineP95: timelineLatency, pairingP95: pairingLatency, }; }, }; /** * Default thresholds yang bisa di-reuse di semua scenario options. * Sesuai ketentuan pillar 3: p95 < 500ms, error rate < 1%. */ export const DEFAULT_THRESHOLDS = { // ── HTTP default k6 http_req_duration: ["p(95)<500", "p(99)<1000"], http_req_failed: ["rate<0.01"], // ── WalkGuide custom walkguide_error_rate: ["rate<0.01"], walkguide_endpoint_latency_ms: ["p(95)<500"], walkguide_location_latency_ms: ["p(95)<300"], // location harus cepat walkguide_obstacle_latency_ms: ["p(95)<400"], walkguide_sos_latency_ms: ["p(95)<200"], // SOS PALING KRITIS walkguide_notif_latency_ms: ["p(95)<500"], walkguide_auth_latency_ms: ["p(95)<800"], walkguide_timeline_latency_ms: ["p(95)<1000"], // query analytics boleh lebih lambat walkguide_pairing_latency_ms: ["p(95)<600"], }; /** * Relaxed thresholds untuk stress test (ekspektasi lebih longgar). */ export const STRESS_THRESHOLDS = { http_req_duration: ["p(95)<1500", "p(99)<3000"], http_req_failed: ["rate<0.05"], walkguide_error_rate: ["rate<0.05"], walkguide_sos_latency_ms: ["p(95)<500"], }; /** * Threshold untuk smoke test lokal. * Dipakai saat backend jalan di laptop tetapi database masih remote, sehingga * angka latency lebih cocok dipakai sebagai bukti run, bukan production SLA. */ export const LOCAL_SMOKE_THRESHOLDS = { http_req_duration: ["p(95)<35000", "p(99)<40000"], http_req_failed: ["rate<0.10"], walkguide_error_rate: ["rate<0.10"], walkguide_endpoint_latency_ms: ["p(95)<35000"], walkguide_location_latency_ms: ["p(95)<3000"], walkguide_obstacle_latency_ms: ["p(95)<5000"], walkguide_sos_latency_ms: ["p(95)<3000"], walkguide_notif_latency_ms: ["p(95)<5000"], walkguide_auth_latency_ms: ["p(95)<35000"], walkguide_timeline_latency_ms: ["p(95)<5000"], walkguide_pairing_latency_ms: ["p(95)<5000"], }; export function pickThresholds(overrides = {}) { const base = __ENV.BENCH_PROFILE === "local" ? LOCAL_SMOKE_THRESHOLDS : DEFAULT_THRESHOLDS; return { ...base, ...overrides }; } /** * handleSummary helper — generate output JSON + text. * Import di tiap scenario: export { handleSummary } from '../modules/metrics-helper.js' */ export function handleSummary(data) { const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); const fileName = `k6-results/summary_${stamp}.json`; return { stdout: textSummary(data), [fileName]: JSON.stringify(data, null, 2), }; } function textSummary(data) { const { metrics } = data; const lines = [ "═══════════════════════════════════════════════════════════════", " WalkGuide Load Test — Result Summary", "═══════════════════════════════════════════════════════════════", ]; const pick = (name, stat) => { if (metrics[name]) { const val = metrics[name].values[stat]; return val !== undefined ? val.toFixed(2) : "N/A"; } return "N/A"; }; lines.push( ` http_req_duration p95=${pick("http_req_duration", "p(95)")}ms p99=${pick("http_req_duration", "p(99)")}ms`, ); lines.push(` http_req_failed rate=${pick("http_req_failed", "rate")}`); lines.push( ` sos_latency p95=${pick("walkguide_sos_latency_ms", "p(95)")}ms`, ); lines.push( ` location_latency p95=${pick("walkguide_location_latency_ms", "p(95)")}ms`, ); lines.push( ` obstacle_latency p95=${pick("walkguide_obstacle_latency_ms", "p(95)")}ms`, ); lines.push( ` notif_latency p95=${pick("walkguide_notif_latency_ms", "p(95)")}ms`, ); lines.push( ` auth_latency p95=${pick("walkguide_auth_latency_ms", "p(95)")}ms`, ); lines.push( ` timeline_latency p95=${pick("walkguide_timeline_latency_ms", "p(95)")}ms`, ); lines.push("═══════════════════════════════════════════════════════════════"); return lines.join("\n") + "\n"; }