feat(xugu): add read-only server monitoring dashboard

This commit is contained in:
Elias authored and t8y2 committed 2026-09-06 10:57:08 +08:00
1 parent d6c4fa1aff
commit 3e82d6d650
9 files changed
+1153 -1

No files matched your search

@@ -0,0 +1,329 @@
import type { ConnectionConfig, QueryResult } from "@/types/database";
import { effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
export interface XuguClusterNode {
nodeId: string;
rackNo: string;
host: string;
port: string;
nodeType: string;
state: string;
cpuLoad: string;
bootTime: string;
storeCount: string;
majorCount: string;
}
export interface XuguRunInfo {
nodeId: string;
activeTransactions: string;
diskReadCount: string;
diskReadBytes: string;
diskWriteCount: string;
diskWriteBytes: string;
maxTransactionId: string;
minTransactionId: string;
xlogWritePosition: string;
xlogCheckpointPosition: string;
freeStores: string;
}
export interface XuguSessionSummary {
nodeId: string;
sessions: string;
activeSessions: string;
memoryBytes: string;
oldestStatement: string;
}
export interface XuguTransactionSummary {
nodeId: string;
activeTransactions: string;
oldestTransaction: string;
}
export interface XuguMemoryStatus {
nodeId: string;
bufferTotalBytes: string;
bufferFreeBytes: string;
bufferDirtyBytes: string;
bufferLruBytes: string;
sgaTotalBytes: string;
sgaFreeBytes: string;
sgaPeakBytes: string;
swapTotalBytes: string;
swapFreeBytes: string;
}
export interface XuguSessionStatusSummary {
nodeId: string;
status: string;
sessions: string;
}
export interface XuguLockModeSummary {
nodeId: string;
lockMode: string;
locks: string;
}
export interface XuguTablespaceSummary {
nodeId: string;
spaceName: string;
spaceType: string;
datafiles: string;
mediaError: string;
totalBytes: string;
freeBytes: string;
}
export const XUGU_VERSION_SQL = "SELECT VERSION() AS VERSION FROM DUAL;";
export const XUGU_CLUSTER_NODES_SQL = `
SELECT
NODE_ID,
RACK_NO,
NODE_IP,
NODE_PORT,
NODE_TYPE,
NODE_STATE,
CPU_LOAD,
BOOT_TIME,
STORE_NUM,
MAJOR_NUM
FROM SYS_CLUSTERS
ORDER BY NODE_ID;`.trim();
export const XUGU_RUN_INFO_SQL = `
SELECT
NODEID,
ACT_TRANS_NUM,
DISK_R_N,
DISK_R_BYTES,
DISK_W_N,
DISK_W_BYTES,
MAX_TRANS_ID,
MIN_TRANS_ID,
XLOG_WPOS,
XLOG_CKPT,
FREE_STO_N
FROM SYS_ALL_RUN_INFO
ORDER BY NODEID;`.trim();
/** Per-node session totals. This is optional because restricted users may not see SYS_ALL_SESSIONS. */
export const XUGU_SESSION_SUMMARY_SQL = `
SELECT NODEID AS NODE_ID,
COUNT(*) AS SESSIONS,
SUM(MEM_SIZE) AS MEMORY_BYTES
FROM SYS_ALL_SESSIONS
GROUP BY NODEID
ORDER BY NODEID;`.trim();
/** The thread-session view is the Xugu-specific definition of an active statement. */
export const XUGU_ACTIVE_SESSION_SQL = `
SELECT T.NODEID AS NODE_ID,
COUNT(*) AS ACTIVE_SESSIONS,
MIN(S.CMD_START_T) AS OLDEST_STATEMENT
FROM SYS_ALL_THD_SESSION T
JOIN SYS_ALL_SESSIONS S
ON T.NODEID = S.NODEID AND T.SESSION_ID = S.SESSION_ID
GROUP BY T.NODEID
ORDER BY T.NODEID;`.trim();
export const XUGU_TRANSACTION_SUMMARY_SQL = `
SELECT NODEID AS NODE_ID,
COUNT(*) AS ACTIVE_TRANSACTIONS,
MIN(START_T) AS OLDEST_TRANSACTION
FROM SYS_ALL_TRANS
GROUP BY NODEID
ORDER BY NODEID;`.trim();
/** Only table-level lock waiters are exposed by SYS_ALL_LWAITERS. */
export const XUGU_LOCK_WAITS_SQL = `
SELECT
(SELECT COUNT(*) FROM SYS_ALL_LWAITERS) AS LOCK_WAITS,
(SELECT COUNT(*) FROM SYS_ALL_LOWNERS) AS LOCK_OWNERS
FROM DUAL;`.trim();
export const XUGU_LOCK_MODE_SUMMARY_SQL = `
SELECT NODEID AS NODE_ID,
LOCK_LEVEL AS LOCK_MODE,
COUNT(*) AS LOCKS
FROM SYS_ALL_LOWNERS
GROUP BY NODEID, LOCK_LEVEL
ORDER BY NODEID, LOCK_LEVEL;`.trim();
export const XUGU_SESSION_STATUS_SUMMARY_SQL = `
SELECT NODEID AS NODE_ID,
STATUS,
COUNT(*) AS SESSIONS
FROM SYS_ALL_SESSIONS
GROUP BY NODEID, STATUS
ORDER BY NODEID, STATUS;`.trim();
export const XUGU_MEMORY_STATUS_SQL = `
SELECT
NODEID,
BUFF_SIZE * TOTAL_BUFF_NUM AS BUFFER_TOTAL_BYTES,
BUFF_SIZE * FREE_BUFF_NUM AS BUFFER_FREE_BYTES,
BUFF_SIZE * DIRTY_BUFF_NUM AS BUFFER_DIRTY_BYTES,
BUFF_SIZE * LRU_BUFF_NUM AS BUFFER_LRU_BYTES,
SGA_BLK_SIZE * TOTAL_SGA_MEM AS SGA_TOTAL_BYTES,
SGA_BLK_SIZE * FREE_SGA_MEM AS SGA_FREE_BYTES,
SGA_BLK_SIZE * PEAK_SGA_MEM AS SGA_PEAK_BYTES,
SWAP_BLK_SIZE * TOTAL_SWAP_MEM AS SWAP_TOTAL_BYTES,
SWAP_BLK_SIZE * FREE_SWAP_MEM AS SWAP_FREE_BYTES
FROM SYS_ALL_MEM_STATUS
ORDER BY NODEID;`.trim();
export const XUGU_TABLESPACE_SUMMARY_SQL = `
SELECT
T.NODEID AS NODE_ID,
T.SPACE_NAME,
T.SPACE_TYPE,
T.DATAFILE_NUM AS DATAFILES,
T.MEDIA_ERROR,
T.TOTAL_CHUNK_NUM * C.CHUNK_SIZE AS TOTAL_BYTES,
T.FREE_CHUNK_NUM * C.CHUNK_SIZE AS FREE_BYTES
FROM SYS_ALL_TABLESPACES T, SYS_CTL_VARS C
ORDER BY T.NODEID, T.SPACE_NAME;`.trim();
export function connectionSupportsXuguServerDashboard(connection: ConnectionConfig | undefined): boolean {
return !!connection && effectiveDatabaseTypeForConnection(connection) === "xugu";
}
export function xuguClusterNodeStateLabel(state: string): "joining" | "running" | "error" | "offline" | "unknown" {
switch (Number(state)) {
case 1:
return "joining";
case 2:
return "running";
case 3:
return "error";
case 4:
return "offline";
default:
return "unknown";
}
}
export type XuguClusterNodeRole = "master" | "standby" | "storage" | "query" | "worker" | "change";
export function xuguClusterNodeTypeLabels(type: string): XuguClusterNodeRole[] {
const value = Number(type);
if (!Number.isFinite(value) || value <= 0) return [];
const roles: Array<[number, XuguClusterNodeRole]> = [
[1, "master"],
[2, "standby"],
[4, "storage"],
[8, "query"],
[16, "worker"],
[32, "change"],
];
return roles.filter(([bit]) => (value & bit) === bit).map(([, role]) => role);
}
export function xuguClusterNodesFromResult(result: QueryResult): XuguClusterNode[] {
return result.rows.map((row) => ({
nodeId: valueAt(result, row, "NODE_ID"),
rackNo: valueAt(result, row, "RACK_NO"),
host: valueAt(result, row, "NODE_IP"),
port: valueAt(result, row, "NODE_PORT"),
nodeType: valueAt(result, row, "NODE_TYPE"),
state: valueAt(result, row, "NODE_STATE"),
cpuLoad: valueAt(result, row, "CPU_LOAD"),
bootTime: valueAt(result, row, "BOOT_TIME"),
storeCount: valueAt(result, row, "STORE_NUM"),
majorCount: valueAt(result, row, "MAJOR_NUM"),
}));
}
export function xuguRunInfoFromResult(result: QueryResult): XuguRunInfo[] {
return result.rows.map((row) => ({
nodeId: valueAt(result, row, "NODEID"),
activeTransactions: valueAt(result, row, "ACT_TRANS_NUM"),
diskReadCount: valueAt(result, row, "DISK_R_N"),
diskReadBytes: valueAt(result, row, "DISK_R_BYTES"),
diskWriteCount: valueAt(result, row, "DISK_W_N"),
diskWriteBytes: valueAt(result, row, "DISK_W_BYTES"),
maxTransactionId: valueAt(result, row, "MAX_TRANS_ID"),
minTransactionId: valueAt(result, row, "MIN_TRANS_ID"),
xlogWritePosition: valueAt(result, row, "XLOG_WPOS"),
xlogCheckpointPosition: valueAt(result, row, "XLOG_CKPT"),
freeStores: valueAt(result, row, "FREE_STO_N"),
}));
}
export function xuguSessionSummaryFromResult(result: QueryResult): XuguSessionSummary[] {
return result.rows.map((row) => ({
nodeId: valueAt(result, row, "NODE_ID"),
sessions: valueAt(result, row, "SESSIONS"),
activeSessions: valueAt(result, row, "ACTIVE_SESSIONS"),
memoryBytes: valueAt(result, row, "MEMORY_BYTES"),
oldestStatement: valueAt(result, row, "OLDEST_STATEMENT"),
}));
}
export function xuguTransactionSummaryFromResult(result: QueryResult): XuguTransactionSummary[] {
return result.rows.map((row) => ({
nodeId: valueAt(result, row, "NODE_ID"),
activeTransactions: valueAt(result, row, "ACTIVE_TRANSACTIONS"),
oldestTransaction: valueAt(result, row, "OLDEST_TRANSACTION"),
}));
}
export function xuguMemoryStatusFromResult(result: QueryResult): XuguMemoryStatus[] {
return result.rows.map((row) => ({
nodeId: valueAt(result, row, "NODEID"),
bufferTotalBytes: valueAt(result, row, "BUFFER_TOTAL_BYTES"),
bufferFreeBytes: valueAt(result, row, "BUFFER_FREE_BYTES"),
bufferDirtyBytes: valueAt(result, row, "BUFFER_DIRTY_BYTES"),
bufferLruBytes: valueAt(result, row, "BUFFER_LRU_BYTES"),
sgaTotalBytes: valueAt(result, row, "SGA_TOTAL_BYTES"),
sgaFreeBytes: valueAt(result, row, "SGA_FREE_BYTES"),
sgaPeakBytes: valueAt(result, row, "SGA_PEAK_BYTES"),
swapTotalBytes: valueAt(result, row, "SWAP_TOTAL_BYTES"),
swapFreeBytes: valueAt(result, row, "SWAP_FREE_BYTES"),
}));
}
export function xuguSessionStatusSummaryFromResult(result: QueryResult): XuguSessionStatusSummary[] {
return result.rows.map((row) => ({
nodeId: valueAt(result, row, "NODE_ID"),
status: valueAt(result, row, "STATUS"),
sessions: valueAt(result, row, "SESSIONS"),
}));
}
export function xuguLockModeSummaryFromResult(result: QueryResult): XuguLockModeSummary[] {
return result.rows.map((row) => ({
nodeId: valueAt(result, row, "NODE_ID"),
lockMode: valueAt(result, row, "LOCK_MODE"),
locks: valueAt(result, row, "LOCKS"),
}));
}
export function xuguTablespaceSummaryFromResult(result: QueryResult): XuguTablespaceSummary[] {
return result.rows.map((row) => ({
nodeId: valueAt(result, row, "NODE_ID"),
spaceName: valueAt(result, row, "SPACE_NAME"),
spaceType: valueAt(result, row, "SPACE_TYPE"),
datafiles: valueAt(result, row, "DATAFILES"),
mediaError: valueAt(result, row, "MEDIA_ERROR"),
totalBytes: valueAt(result, row, "TOTAL_BYTES"),
freeBytes: valueAt(result, row, "FREE_BYTES"),
}));
}
export function xuguScalarFromResult(result: QueryResult, column: string): string {
return result.rows.length > 0 ? valueAt(result, result.rows[0], column) : "";
}
export function xuguVersionFromResult(result: QueryResult): string {
return xuguScalarFromResult(result, "VERSION");
}
function valueAt(result: QueryResult, row: unknown[], column: string): string {
const index = result.columns.findIndex((candidate) => candidate.toUpperCase() === column.toUpperCase());
return index < 0 || row[index] == null ? "" : String(row[index]);
}