260 lines
9.3 KiB
JavaScript
260 lines
9.3 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"],
|
|
};
|
|
|
|
/**
|
|
* 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";
|
|
}
|