DatabaseInfrastructureSection.tsx268 lines · main
| 1 | import { useParams } from 'common' |
| 2 | import dayjs from 'dayjs' |
| 3 | import Link from 'next/link' |
| 4 | import { useMemo } from 'react' |
| 5 | import { |
| 6 | MetricCard, |
| 7 | MetricCardContent, |
| 8 | MetricCardHeader, |
| 9 | MetricCardLabel, |
| 10 | MetricCardValue, |
| 11 | } from 'ui-patterns/MetricCard' |
| 12 | |
| 13 | import { |
| 14 | parseConnectionsData, |
| 15 | parseInfrastructureMetrics, |
| 16 | } from './DatabaseInfrastructureSection.utils' |
| 17 | import { useInfraMonitoringAttributesQuery } from '@/data/analytics/infra-monitoring-query' |
| 18 | import { useMaxConnectionsQuery } from '@/data/database/max-connections-query' |
| 19 | import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' |
| 20 | |
| 21 | type DatabaseInfrastructureSectionProps = { |
| 22 | interval: '1hr' | '1day' | '7day' |
| 23 | refreshKey: number |
| 24 | dbErrorRate: number |
| 25 | isLoading: boolean |
| 26 | slowQueriesCount?: number |
| 27 | slowQueriesLoading?: boolean |
| 28 | } |
| 29 | |
| 30 | export const DatabaseInfrastructureSection = ({ |
| 31 | interval, |
| 32 | refreshKey, |
| 33 | dbErrorRate: _dbErrorRate, |
| 34 | isLoading: _dbLoading, |
| 35 | slowQueriesCount = 0, |
| 36 | slowQueriesLoading = false, |
| 37 | }: DatabaseInfrastructureSectionProps) => { |
| 38 | const { ref: projectRef } = useParams() |
| 39 | const { data: project } = useSelectedProjectQuery() |
| 40 | |
| 41 | // refreshKey forces date recalculation when user clicks refresh button |
| 42 | // eslint-disable-next-line react-hooks/exhaustive-deps |
| 43 | const { startDate, endDate, infraInterval } = useMemo(() => { |
| 44 | const now = dayjs() |
| 45 | const end = now.toISOString() |
| 46 | let start: string |
| 47 | let infraInterval: '1h' | '1d' |
| 48 | |
| 49 | switch (interval) { |
| 50 | case '1hr': |
| 51 | start = now.subtract(1, 'hour').toISOString() |
| 52 | infraInterval = '1h' |
| 53 | break |
| 54 | case '1day': |
| 55 | start = now.subtract(1, 'day').toISOString() |
| 56 | infraInterval = '1h' |
| 57 | break |
| 58 | case '7day': |
| 59 | start = now.subtract(7, 'day').toISOString() |
| 60 | infraInterval = '1d' |
| 61 | break |
| 62 | default: |
| 63 | start = now.subtract(1, 'hour').toISOString() |
| 64 | infraInterval = '1h' |
| 65 | } |
| 66 | |
| 67 | return { startDate: start, endDate: end, infraInterval } |
| 68 | }, [interval, refreshKey]) |
| 69 | |
| 70 | const { |
| 71 | data: infraData, |
| 72 | isLoading: infraLoading, |
| 73 | error: infraError, |
| 74 | } = useInfraMonitoringAttributesQuery({ |
| 75 | projectRef, |
| 76 | attributes: [ |
| 77 | 'avg_cpu_usage', |
| 78 | 'ram_usage', |
| 79 | 'disk_fs_used_system', |
| 80 | 'disk_fs_used_wal', |
| 81 | 'pg_database_size', |
| 82 | 'disk_fs_size', |
| 83 | 'disk_io_consumption', |
| 84 | 'pg_stat_database_num_backends', |
| 85 | ], |
| 86 | startDate, |
| 87 | endDate, |
| 88 | interval: infraInterval, |
| 89 | }) |
| 90 | |
| 91 | const { data: maxConnectionsData } = useMaxConnectionsQuery({ |
| 92 | projectRef, |
| 93 | connectionString: project?.connectionString, |
| 94 | }) |
| 95 | |
| 96 | const metrics = useMemo(() => parseInfrastructureMetrics(infraData), [infraData]) |
| 97 | |
| 98 | const connections = useMemo( |
| 99 | () => parseConnectionsData(infraData, maxConnectionsData), |
| 100 | [infraData, maxConnectionsData] |
| 101 | ) |
| 102 | |
| 103 | const errorMessage = |
| 104 | infraError && typeof infraError === 'object' && 'message' in infraError |
| 105 | ? String(infraError.message) |
| 106 | : 'Error loading data' |
| 107 | |
| 108 | // Generate database report URL with time range parameters |
| 109 | const getDatabaseReportUrl = () => { |
| 110 | const now = dayjs() |
| 111 | let its: string |
| 112 | let helperText: string |
| 113 | |
| 114 | switch (interval) { |
| 115 | case '1hr': |
| 116 | its = now.subtract(1, 'hour').toISOString() |
| 117 | helperText = 'Last 60 minutes' |
| 118 | break |
| 119 | case '1day': |
| 120 | its = now.subtract(24, 'hour').toISOString() |
| 121 | helperText = 'Last 24 hours' |
| 122 | break |
| 123 | case '7day': |
| 124 | its = now.subtract(7, 'day').toISOString() |
| 125 | helperText = 'Last 7 days' |
| 126 | break |
| 127 | default: |
| 128 | its = now.subtract(24, 'hour').toISOString() |
| 129 | helperText = 'Last 24 hours' |
| 130 | } |
| 131 | |
| 132 | const ite = now.toISOString() |
| 133 | const params = new URLSearchParams({ |
| 134 | its, |
| 135 | ite, |
| 136 | isHelper: 'true', |
| 137 | helperText, |
| 138 | }) |
| 139 | |
| 140 | return `/project/${projectRef}/observability/database?${params.toString()}` |
| 141 | } |
| 142 | |
| 143 | const databaseReportUrl = getDatabaseReportUrl() |
| 144 | |
| 145 | return ( |
| 146 | <div> |
| 147 | <h2 className="mb-4">Database</h2> |
| 148 | {/* First row: Metrics */} |
| 149 | <div className="grid grid-cols-3 gap-2"> |
| 150 | <Link |
| 151 | href={`/project/${projectRef}/observability/query-performance?totalTimeFilter=${encodeURIComponent(JSON.stringify({ operator: '>', value: 1000 }))}`} |
| 152 | className="block group" |
| 153 | > |
| 154 | <MetricCard isLoading={slowQueriesLoading}> |
| 155 | <MetricCardHeader |
| 156 | href={`/project/${projectRef}/observability/query-performance?totalTimeFilter=${encodeURIComponent(JSON.stringify({ operator: '>', value: 1000 }))}`} |
| 157 | linkTooltip="Go to query performance" |
| 158 | > |
| 159 | <MetricCardLabel tooltip="Queries with total execution time (execution time + planning time) greater than 1000ms. High values may indicate query optimization opportunities"> |
| 160 | Slow Queries |
| 161 | </MetricCardLabel> |
| 162 | </MetricCardHeader> |
| 163 | <MetricCardContent> |
| 164 | <MetricCardValue>{slowQueriesCount}</MetricCardValue> |
| 165 | </MetricCardContent> |
| 166 | </MetricCard> |
| 167 | </Link> |
| 168 | |
| 169 | <Link href={databaseReportUrl} className="block group"> |
| 170 | <MetricCard isLoading={infraLoading}> |
| 171 | <MetricCardHeader href={databaseReportUrl} linkTooltip="Go to database report"> |
| 172 | <MetricCardLabel tooltip="Active database connections (current/max). Monitor to avoid connection exhaustion"> |
| 173 | Connections |
| 174 | </MetricCardLabel> |
| 175 | </MetricCardHeader> |
| 176 | <MetricCardContent> |
| 177 | {infraError ? ( |
| 178 | <div className="text-xs text-destructive wrap-break-word">{errorMessage}</div> |
| 179 | ) : connections.max > 0 ? ( |
| 180 | <MetricCardValue> |
| 181 | {connections.current}/{connections.max} |
| 182 | </MetricCardValue> |
| 183 | ) : ( |
| 184 | <MetricCardValue>--</MetricCardValue> |
| 185 | )} |
| 186 | </MetricCardContent> |
| 187 | </MetricCard> |
| 188 | </Link> |
| 189 | |
| 190 | <Link href={databaseReportUrl} className="block group"> |
| 191 | <MetricCard isLoading={infraLoading}> |
| 192 | <MetricCardHeader href={databaseReportUrl} linkTooltip="Go to database report"> |
| 193 | <MetricCardLabel tooltip="Disk usage percentage of total disk space used"> |
| 194 | Disk Usage |
| 195 | </MetricCardLabel> |
| 196 | </MetricCardHeader> |
| 197 | <MetricCardContent> |
| 198 | {infraError ? ( |
| 199 | <div className="text-xs text-destructive wrap-break-word">{errorMessage}</div> |
| 200 | ) : metrics ? ( |
| 201 | <MetricCardValue>{metrics.disk.current.toFixed(0)}%</MetricCardValue> |
| 202 | ) : ( |
| 203 | <MetricCardValue>--</MetricCardValue> |
| 204 | )} |
| 205 | </MetricCardContent> |
| 206 | </MetricCard> |
| 207 | </Link> |
| 208 | |
| 209 | <Link href={databaseReportUrl} className="block group"> |
| 210 | <MetricCard isLoading={infraLoading}> |
| 211 | <MetricCardHeader href={databaseReportUrl} linkTooltip="Go to database report"> |
| 212 | <MetricCardLabel tooltip="Disk I/O consumption percentage. High values may indicate disk bottlenecks"> |
| 213 | Disk IO |
| 214 | </MetricCardLabel> |
| 215 | </MetricCardHeader> |
| 216 | <MetricCardContent> |
| 217 | {infraError ? ( |
| 218 | <div className="text-xs text-destructive wrap-break-word">{errorMessage}</div> |
| 219 | ) : metrics ? ( |
| 220 | <MetricCardValue>{metrics.diskIo.current.toFixed(0)}%</MetricCardValue> |
| 221 | ) : ( |
| 222 | <MetricCardValue>--</MetricCardValue> |
| 223 | )} |
| 224 | </MetricCardContent> |
| 225 | </MetricCard> |
| 226 | </Link> |
| 227 | |
| 228 | <Link href={databaseReportUrl} className="block group"> |
| 229 | <MetricCard isLoading={infraLoading}> |
| 230 | <MetricCardHeader href={databaseReportUrl} linkTooltip="Go to database report"> |
| 231 | <MetricCardLabel tooltip="RAM usage percentage. Sustained high usage may indicate memory pressure"> |
| 232 | Memory |
| 233 | </MetricCardLabel> |
| 234 | </MetricCardHeader> |
| 235 | <MetricCardContent> |
| 236 | {infraError ? ( |
| 237 | <div className="text-xs text-destructive wrap-break-word">{errorMessage}</div> |
| 238 | ) : metrics ? ( |
| 239 | <MetricCardValue>{metrics.ram.current.toFixed(0)}%</MetricCardValue> |
| 240 | ) : ( |
| 241 | <MetricCardValue>--</MetricCardValue> |
| 242 | )} |
| 243 | </MetricCardContent> |
| 244 | </MetricCard> |
| 245 | </Link> |
| 246 | |
| 247 | <Link href={databaseReportUrl} className="block group"> |
| 248 | <MetricCard isLoading={infraLoading}> |
| 249 | <MetricCardHeader href={databaseReportUrl} linkTooltip="Go to database report"> |
| 250 | <MetricCardLabel tooltip="CPU usage percentage. High values may suggest CPU-intensive queries or workloads"> |
| 251 | CPU |
| 252 | </MetricCardLabel> |
| 253 | </MetricCardHeader> |
| 254 | <MetricCardContent> |
| 255 | {infraError ? ( |
| 256 | <div className="text-xs text-destructive wrap-break-word">{errorMessage}</div> |
| 257 | ) : metrics ? ( |
| 258 | <MetricCardValue>{metrics.cpu.current.toFixed(0)}%</MetricCardValue> |
| 259 | ) : ( |
| 260 | <MetricCardValue>--</MetricCardValue> |
| 261 | )} |
| 262 | </MetricCardContent> |
| 263 | </MetricCard> |
| 264 | </Link> |
| 265 | </div> |
| 266 | </div> |
| 267 | ) |
| 268 | } |