2026-05-17 19:36:46 +07:00

287 lines
10 KiB
JavaScript

/**
* 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";
}