feat:
--ADDED NODE WAL GENERATION PER SEC --ADDED NODE WAL SIZE
This commit is contained in:
1 parent
b48038adca
commit
14e6981853
3 files changed
+176
No files matched your search
+12
@@ -5,6 +5,8 @@ import NodeConnections from './NodeConnections'
|
|||||||
import NodeActiveConnections from './NodeActiveConnections'
|
import NodeActiveConnections from './NodeActiveConnections'
|
||||||
import NodePerformance from './NodePerformance'
|
import NodePerformance from './NodePerformance'
|
||||||
import NodeTupleActivity from './NodeTupleActivity'
|
import NodeTupleActivity from './NodeTupleActivity'
|
||||||
|
import NodeWalGenerated from './NodeWalGenerated'
|
||||||
|
import NodeWalSize from './NodeWalSize'
|
||||||
import NodeDeadTuples from './NodeDeadTuples'
|
import NodeDeadTuples from './NodeDeadTuples'
|
||||||
import NodeLocksDeadlocks from './NodeLocksDeadlocks'
|
import NodeLocksDeadlocks from './NodeLocksDeadlocks'
|
||||||
import NodeDatabaseSize from './NodeDatabaseSize'
|
import NodeDatabaseSize from './NodeDatabaseSize'
|
||||||
@@ -645,6 +647,16 @@ function Workspace({ page: activePage }: { page: PageId | 'node-details' | 'larg
|
|||||||
nodeId={nodeId}
|
nodeId={nodeId}
|
||||||
nodeName={clusterDetails.nodes.find((node) => node.uuid === nodeId)!.node_name}
|
nodeName={clusterDetails.nodes.find((node) => node.uuid === nodeId)!.node_name}
|
||||||
/>
|
/>
|
||||||
|
<NodeWalGenerated
|
||||||
|
key={`wal-generated/${clusterId}/${nodeId}`}
|
||||||
|
nodeId={nodeId}
|
||||||
|
nodeName={clusterDetails.nodes.find((node) => node.uuid === nodeId)!.node_name}
|
||||||
|
/>
|
||||||
|
<NodeWalSize
|
||||||
|
key={`wal-size/${clusterId}/${nodeId}`}
|
||||||
|
nodeId={nodeId}
|
||||||
|
nodeName={clusterDetails.nodes.find((node) => node.uuid === nodeId)!.node_name}
|
||||||
|
/>
|
||||||
<NodeDeadTuples
|
<NodeDeadTuples
|
||||||
key={`dead-tuples/${clusterId}/${nodeId}`}
|
key={`dead-tuples/${clusterId}/${nodeId}`}
|
||||||
clusterId={clusterId!}
|
clusterId={clusterId!}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import useNodeDatabaseMetric from './useNodeDatabaseMetric'
|
||||||
|
|
||||||
|
const metrics = [
|
||||||
|
{ field: 'wal_generated_bytes_per_second', label: 'WAL generated (bytes/s)' },
|
||||||
|
{ field: 'wal_generated_mb_per_second', label: 'WAL generated (MB/s)' },
|
||||||
|
] as const
|
||||||
|
|
||||||
|
type WalGenerated = {
|
||||||
|
database_name: string
|
||||||
|
} & Record<(typeof metrics)[number]['field'], number>
|
||||||
|
|
||||||
|
function isWalGenerated(value: unknown): value is WalGenerated {
|
||||||
|
if (!value || typeof value !== 'object') return false
|
||||||
|
const data = value as Record<string, unknown>
|
||||||
|
return typeof data.database_name === 'string'
|
||||||
|
&& metrics.every(({ field }) => {
|
||||||
|
const rate = data[field]
|
||||||
|
return typeof rate === 'number' && Number.isFinite(rate) && rate >= 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NodeWalGenerated({ nodeId, nodeName }: { nodeId: string; nodeName: string }) {
|
||||||
|
const { data, connection } = useNodeDatabaseMetric(nodeId, nodeName, 'wal-generated', isWalGenerated)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="database-connections" aria-labelledby="database-wal-generated-heading">
|
||||||
|
<div className="page-header">
|
||||||
|
<div>
|
||||||
|
<h2 id="database-wal-generated-heading">WAL generated per second</h2>
|
||||||
|
{data && <p>Database: {data.database_name}</p>}
|
||||||
|
</div>
|
||||||
|
<p role="status" className={data ? 'connection-status connected' : ''}>{connection}</p>
|
||||||
|
</div>
|
||||||
|
<div className="database-connection-metrics">
|
||||||
|
{metrics.map(({ field, label }) => (
|
||||||
|
<div className="database-connection-metric" key={field}>
|
||||||
|
<h3>{label}</h3>
|
||||||
|
<strong>{data ? data[field].toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '—'}</strong>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
import { useEffect, useState } from 'react'
|
||||||
|
|
||||||
|
type WalSize = {
|
||||||
|
database_name: string
|
||||||
|
wal_size_bytes: number
|
||||||
|
wal_size_mb: number
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWalSize(value: unknown): value is WalSize {
|
||||||
|
if (!value || typeof value !== 'object') return false
|
||||||
|
const data = value as Record<string, unknown>
|
||||||
|
return typeof data.database_name === 'string'
|
||||||
|
&& typeof data.wal_size_bytes === 'number' && Number.isSafeInteger(data.wal_size_bytes) && data.wal_size_bytes >= 0
|
||||||
|
&& typeof data.wal_size_mb === 'number' && Number.isFinite(data.wal_size_mb) && data.wal_size_mb >= 0
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function NodeWalSize({ nodeId, nodeName }: { nodeId: string; nodeName: string }) {
|
||||||
|
const [data, setData] = useState<WalSize | null>(null)
|
||||||
|
const [status, setStatus] = useState('Loading WAL size…')
|
||||||
|
const [error, setError] = useState('')
|
||||||
|
const [attempt, setAttempt] = useState(0)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let active = true
|
||||||
|
const controller = new AbortController()
|
||||||
|
|
||||||
|
const load = async () => {
|
||||||
|
try {
|
||||||
|
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL
|
||||||
|
if (!apiBaseUrl) throw new Error('API URL is not configured.')
|
||||||
|
const baseUrl = apiBaseUrl.replace(/\/$/, '')
|
||||||
|
const databaseApiBaseUrl = import.meta.env.VITE_DATABASE_API_BASE_URL
|
||||||
|
if (!databaseApiBaseUrl) throw new Error('Database API URL is not configured.')
|
||||||
|
const databaseBaseUrl = import.meta.env.DEV
|
||||||
|
? '/database-api'
|
||||||
|
: databaseApiBaseUrl.replace(/\/$/, '')
|
||||||
|
const token = localStorage.getItem('access_token')
|
||||||
|
if (!token) throw new Error('Authentication required. Get a token from the Nodes page, then retry.')
|
||||||
|
const headers: Record<string, string> = { Authorization: `Bearer ${token}` }
|
||||||
|
const nodeResponse = await fetch(`${baseUrl}/spock/node-interfaces/${encodeURIComponent(nodeId)}`, {
|
||||||
|
headers,
|
||||||
|
signal: controller.signal,
|
||||||
|
})
|
||||||
|
if (!nodeResponse.ok) throw new Error('Failed to load node details.')
|
||||||
|
const result = await nodeResponse.json()
|
||||||
|
const node = result?.node
|
||||||
|
if (result?.status !== 'success' || !node || node.uuid !== nodeId
|
||||||
|
|| typeof node.host !== 'string' || typeof node.port !== 'number'
|
||||||
|
|| typeof node.dbname !== 'string' || typeof node.user !== 'string') {
|
||||||
|
throw new Error('Invalid node details.')
|
||||||
|
}
|
||||||
|
if (!active) return
|
||||||
|
const response = await fetch(`${databaseBaseUrl}/database/wal-size`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
signal: controller.signal,
|
||||||
|
body: JSON.stringify({
|
||||||
|
node_name: nodeName,
|
||||||
|
host: node.host,
|
||||||
|
port: node.port,
|
||||||
|
dbname: node.dbname,
|
||||||
|
user: node.user,
|
||||||
|
password: 'postgres',
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
if (response.status === 401) throw new Error('Authentication failed. Get a new token from the Nodes page, then retry.')
|
||||||
|
if (!response.ok) throw new Error(`Failed to load WAL size (${response.status}).`)
|
||||||
|
const size: unknown = await response.json()
|
||||||
|
if (!isWalSize(size)) throw new Error('Invalid WAL size response.')
|
||||||
|
if (!active) return
|
||||||
|
setData(size)
|
||||||
|
setStatus('Loaded')
|
||||||
|
} catch (cause) {
|
||||||
|
if (!active) return
|
||||||
|
setStatus('')
|
||||||
|
setError(cause instanceof Error ? cause.message : 'Unable to load WAL size.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void load()
|
||||||
|
return () => {
|
||||||
|
active = false
|
||||||
|
controller.abort()
|
||||||
|
}
|
||||||
|
}, [nodeId, nodeName, attempt])
|
||||||
|
|
||||||
|
const loading = status === 'Loading WAL size…'
|
||||||
|
|
||||||
|
return (
|
||||||
|
<section className="database-connections" aria-labelledby="wal-size-heading">
|
||||||
|
<div className="page-header">
|
||||||
|
<div>
|
||||||
|
<h2 id="wal-size-heading">WAL size</h2>
|
||||||
|
{data && <p>Database: {data.database_name}</p>}
|
||||||
|
</div>
|
||||||
|
<div className="page-actions">
|
||||||
|
{status && <p role="status">{status}</p>}
|
||||||
|
<button className="secondary-button" type="button" disabled={loading} onClick={() => {
|
||||||
|
setData(null)
|
||||||
|
setError('')
|
||||||
|
setStatus('Loading WAL size…')
|
||||||
|
setAttempt((current) => current + 1)
|
||||||
|
}}>
|
||||||
|
{error ? 'Retry' : 'Refresh'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{error && <p className="status-message error" role="alert">{error}</p>}
|
||||||
|
<div className="database-connection-metrics">
|
||||||
|
<div className="database-connection-metric">
|
||||||
|
<h3>Size (MB)</h3>
|
||||||
|
<strong>{data ? data.wal_size_mb.toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) : '—'}</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in new issue
Block a user