127 lines
3.5 KiB
JavaScript
127 lines
3.5 KiB
JavaScript
import { performance } from 'node:perf_hooks';
|
|
|
|
export class MetricsCollector {
|
|
constructor() {
|
|
this.entries = new Map();
|
|
}
|
|
|
|
record(name, durationMs, error = null) {
|
|
let entry = this.entries.get(name);
|
|
if (!entry) {
|
|
entry = {
|
|
name,
|
|
durations: [],
|
|
count: 0,
|
|
errors: 0,
|
|
totalMs: 0,
|
|
};
|
|
this.entries.set(name, entry);
|
|
}
|
|
entry.count += 1;
|
|
entry.totalMs += durationMs;
|
|
entry.durations.push(durationMs);
|
|
if (error) {
|
|
entry.errors += 1;
|
|
}
|
|
}
|
|
|
|
getStats(name) {
|
|
const entry = this.entries.get(name);
|
|
if (!entry || entry.count === 0) return null;
|
|
|
|
const sorted = [...entry.durations].sort((a, b) => a - b);
|
|
const count = entry.count;
|
|
const totalMs = entry.totalMs;
|
|
const avgMs = totalMs / count;
|
|
const minMs = sorted[0];
|
|
const maxMs = sorted[count - 1];
|
|
|
|
const p50Index = Math.min(Math.floor(count * 0.5), count - 1);
|
|
const p95Index = Math.min(Math.floor(count * 0.95), count - 1);
|
|
const p99Index = Math.min(Math.floor(count * 0.99), count - 1);
|
|
|
|
const p50Ms = sorted[p50Index];
|
|
const p95Ms = sorted[p95Index];
|
|
const p99Ms = sorted[p99Index];
|
|
|
|
return {
|
|
name,
|
|
count,
|
|
errors: entry.errors,
|
|
totalMs,
|
|
avgMs,
|
|
minMs,
|
|
maxMs,
|
|
p50Ms,
|
|
p95Ms,
|
|
p99Ms,
|
|
};
|
|
}
|
|
|
|
getAllStats() {
|
|
const results = [];
|
|
for (const name of this.entries.keys()) {
|
|
results.push(this.getStats(name));
|
|
}
|
|
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wraps a LibDashStore instance so all method calls across all namespaces
|
|
* are intercepted and measured with high-resolution performance timers.
|
|
*
|
|
* @param {import('../src/index.js').default} store
|
|
* @param {MetricsCollector} metrics
|
|
* @returns {import('../src/index.js').default} Instrumented store
|
|
*/
|
|
export function instrumentStore(store, metrics = new MetricsCollector()) {
|
|
const ignoredProperties = new Set(['provider', 'pool', 'constructor']);
|
|
|
|
for (const [namespaceKey, namespaceVal] of Object.entries(store)) {
|
|
if (ignoredProperties.has(namespaceKey)) continue;
|
|
|
|
// Direct root function like store.purge or store.initialize
|
|
if (typeof namespaceVal === 'function') {
|
|
const originalFn = namespaceVal.bind(store);
|
|
store[namespaceKey] = async (...args) => {
|
|
const start = performance.now();
|
|
let error = null;
|
|
try {
|
|
return await originalFn(...args);
|
|
} catch (err) {
|
|
error = err;
|
|
throw err;
|
|
} finally {
|
|
const duration = performance.now() - start;
|
|
metrics.record(`store.${namespaceKey}`, duration, error);
|
|
}
|
|
};
|
|
continue;
|
|
}
|
|
|
|
if (typeof namespaceVal === 'object' && namespaceVal !== null) {
|
|
for (const [methodKey, methodVal] of Object.entries(namespaceVal)) {
|
|
if (typeof methodVal === 'function') {
|
|
const originalMethod = methodVal.bind(namespaceVal);
|
|
namespaceVal[methodKey] = async (...args) => {
|
|
const start = performance.now();
|
|
let error = null;
|
|
try {
|
|
return await originalMethod(...args);
|
|
} catch (err) {
|
|
error = err;
|
|
throw err;
|
|
} finally {
|
|
const duration = performance.now() - start;
|
|
metrics.record(`${namespaceKey}.${methodKey}`, duration, error);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { store, metrics };
|
|
}
|