File size: 3,018 Bytes
bc4a7e8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 | /**
* Plugin Metrics DB module — per-plugin hook execution tracking.
*
* STATUS: The `plugin_metrics` table (migration 090) is a reserved aggregate table.
* `recordPluginMetric` is currently NOT called from any production code path — the
* per-execution row store is `plugin_analytics` (migration 091, read by the
* `plugin_executions` MCP tool). `plugin_metrics` is intended for future aggregate
* rollups (e.g. bumping per-(plugin, event) counters from `recordPluginExecution`),
* but that write path has not been wired yet. Do NOT remove the migration — it is
* harmless and reserves the schema for the planned rollup feature.
*
* @module db/pluginMetrics
*/
import { getDbInstance } from "./core";
export interface PluginMetricRow {
pluginName: string;
event: string;
calls: number;
errors: number;
totalDurationMs: number;
lastCalledAt: string | null;
}
function rowToMetric(row: Record<string, unknown>): PluginMetricRow {
return {
pluginName: row.plugin_name as string,
event: row.event as string,
calls: row.calls as number,
errors: row.errors as number,
totalDurationMs: row.total_duration_ms as number,
lastCalledAt: row.last_called_at as string | null,
};
}
/**
* Record a hook execution metric. Uses UPSERT to increment counters.
*/
export function recordPluginMetric(
pluginName: string,
event: string,
durationMs: number,
isError: boolean
): void {
try {
const db = getDbInstance();
const now = new Date().toISOString();
db.prepare(
`INSERT INTO plugin_metrics (plugin_name, event, calls, errors, total_duration_ms, last_called_at)
VALUES (?, ?, 1, ?, ?, ?)
ON CONFLICT(plugin_name, event) DO UPDATE SET
calls = calls + 1,
errors = errors + excluded.errors,
total_duration_ms = total_duration_ms + excluded.total_duration_ms,
last_called_at = excluded.last_called_at`
).run(pluginName, event, isError ? 1 : 0, durationMs, now);
} catch {
// Best-effort: DB hiccup should never break hook execution
}
}
/**
* Get plugin metrics, optionally filtered by plugin name.
*/
export function getPluginMetrics(pluginName?: string): PluginMetricRow[] {
try {
const db = getDbInstance();
const rows = pluginName
? db.prepare("SELECT * FROM plugin_metrics WHERE plugin_name = ? ORDER BY event").all(pluginName)
: db.prepare("SELECT * FROM plugin_metrics ORDER BY plugin_name, event").all();
return (rows as Record<string, unknown>[]).map(rowToMetric);
} catch {
return [];
}
}
/**
* Clear plugin metrics, optionally filtered by plugin name.
*/
export function clearPluginMetrics(pluginName?: string): number {
const db = getDbInstance();
const result = pluginName
? db.prepare("DELETE FROM plugin_metrics WHERE plugin_name = ?").run(pluginName)
: db.prepare("DELETE FROM plugin_metrics").run();
return result.changes;
}
|