mirror of
https://github.com/supabase/supabase.git
synced 2026-06-13 01:39:53 +08:00
## Problem Users on the Infrastructure activity tab see a Disk IO Bandwidth chart that plots burst budget percentage rather than real disk activity. The more granular IOPS and throughput charts live in Observability, but there is no path between the two pages — users either dismiss real warnings or file support tickets. Reported in [Linear DEBUG-64](https://linear.app/supabase/issue/DEBUG-64). ## Fix Two cross-links into Observability/Database: 1. **Below the Disk IO Bandwidth chart** ([InfrastructureActivity.tsx](apps/studio/components/interfaces/Settings/Infrastructure/InfrastructureActivity.tsx)) an Admonition with a "View detailed IOPS and throughput" button. Only shown for the disk IO section, not CPU/RAM. 2. **In the disk IO exhaustion banner's Troubleshoot dropdown** ([ResourceExhaustionWarningBanner.tsx](apps/studio/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner.tsx)) a "View metrics" item alongside Documentation and Ask AI Assistant. The banner part is wired via a new optional `metricsHref` on `ResourceWarningMessage`, so CPU/RAM warnings can opt in later by adding their own observability path. ## Test plan - [ ] Visit `/project/{ref}/settings/infrastructure` on a project that does not have dedicated I/O resources. Confirm the new Admonition appears under the Disk IO chart and the button navigates to `/project/{ref}/observability/database`. - [ ] On a project with dedicated I/O resources, confirm only the existing "dedicated I/O" Admonition shows (no double-Admonition). - [ ] Trigger (or mock) a `disk_io` resource warning; confirm the banner's Troubleshoot dropdown shows "View metrics" first and links to Observability. - [ ] Trigger a `cpu` or `ram` warning; confirm the dropdown does NOT show the new item. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Clarifies that the disk IO consumption chart shows remaining burst budget, not real throughput. * Adds a quick-access button to jump to the Database Observability page for detailed read/write IOPS and throughput. * Adds a "View metrics" link in resource exhaustion warnings for disk IO issues (shown when a single warning is active). <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
458 lines
20 KiB
TypeScript
458 lines
20 KiB
TypeScript
import { useParams } from 'common'
|
|
import dayjs from 'dayjs'
|
|
import { capitalize } from 'lodash'
|
|
import { BarChart2, ChartLine, ExternalLink } from 'lucide-react'
|
|
import Link from 'next/link'
|
|
import { Fragment, useMemo, useState } from 'react'
|
|
import { Button } from 'ui'
|
|
import { Admonition } from 'ui-patterns/admonition'
|
|
import { ShimmeringLoader } from 'ui-patterns/ShimmeringLoader'
|
|
|
|
import { INFRA_ACTIVITY_METRICS } from './Infrastructure.constants'
|
|
import { getAddons } from '@/components/interfaces/Billing/Subscription/Subscription.utils'
|
|
import { CPUWarnings } from '@/components/interfaces/Billing/Usage/UsageWarningAlerts/CPUWarnings'
|
|
import { DiskIOBandwidthWarnings } from '@/components/interfaces/Billing/Usage/UsageWarningAlerts/DiskIOBandwidthWarnings'
|
|
import { RAMWarnings } from '@/components/interfaces/Billing/Usage/UsageWarningAlerts/RAMWarnings'
|
|
import UsageBarChart from '@/components/interfaces/Organization/Usage/UsageBarChart'
|
|
import {
|
|
ScaffoldContainer,
|
|
ScaffoldDivider,
|
|
ScaffoldSection,
|
|
ScaffoldSectionContent,
|
|
ScaffoldSectionDetail,
|
|
} from '@/components/layouts/Scaffold'
|
|
import { DatabaseSelector } from '@/components/ui/DatabaseSelector'
|
|
import { DateRangePicker } from '@/components/ui/DateRangePicker'
|
|
import { DocsButton } from '@/components/ui/DocsButton'
|
|
import Panel from '@/components/ui/Panel'
|
|
import { DataPoint } from '@/data/analytics/constants'
|
|
import { mapMultiResponseToAnalyticsData } from '@/data/analytics/infra-monitoring-queries'
|
|
import {
|
|
InfraMonitoringAttribute,
|
|
useInfraMonitoringAttributesQuery,
|
|
} from '@/data/analytics/infra-monitoring-query'
|
|
import { useOrgSubscriptionQuery } from '@/data/subscriptions/org-subscription-query'
|
|
import { useProjectAddonsQuery } from '@/data/subscriptions/project-addons-query'
|
|
import { useResourceWarningsQuery } from '@/data/usage/resource-warnings-query'
|
|
import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements'
|
|
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
|
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
|
import { DOCS_URL, INSTANCE_MICRO_SPECS, INSTANCE_NANO_SPECS, InstanceSpecs } from '@/lib/constants'
|
|
import { TIME_PERIODS_BILLING, TIME_PERIODS_REPORTS } from '@/lib/constants/metrics'
|
|
import { useDatabaseSelectorStateSnapshot } from '@/state/database-selector'
|
|
|
|
const NON_DEDICATED_IO_RESOURCES = [
|
|
'ci_micro',
|
|
'ci_small',
|
|
'ci_medium',
|
|
'ci_large',
|
|
'ci_xlarge',
|
|
'ci_2xlarge',
|
|
]
|
|
|
|
const INFRA_ATTRIBUTES: InfraMonitoringAttribute[] = [
|
|
'max_cpu_usage',
|
|
'ram_usage',
|
|
'disk_io_consumption',
|
|
]
|
|
|
|
export const InfrastructureActivity = () => {
|
|
const { ref: projectRef } = useParams()
|
|
const { data: project } = useSelectedProjectQuery()
|
|
const { data: organization } = useSelectedOrganizationQuery()
|
|
const state = useDatabaseSelectorStateSnapshot()
|
|
const [dateRange, setDateRange] = useState<any>()
|
|
|
|
const { data: subscription, isPending: isLoadingSubscription } = useOrgSubscriptionQuery({
|
|
orgSlug: organization?.slug,
|
|
})
|
|
const { hasAccess: hasAccessToComputeSizes } = useCheckEntitlements(
|
|
'instances.compute_update_available_sizes'
|
|
)
|
|
|
|
const { data: resourceWarnings } = useResourceWarningsQuery({ ref: projectRef })
|
|
// [Joshen Cleanup] JFYI this client side filtering can be cleaned up once BE changes are live which will only return the warnings based on the provided ref
|
|
const projectResourceWarnings = resourceWarnings?.find((x) => x.project === projectRef)
|
|
|
|
const { data: addons } = useProjectAddonsQuery({ projectRef })
|
|
const selectedAddons = addons?.selected_addons ?? []
|
|
|
|
const { computeInstance } = getAddons(selectedAddons)
|
|
const hasDedicatedIOResources =
|
|
computeInstance !== undefined &&
|
|
!NON_DEDICATED_IO_RESOURCES.includes(computeInstance.variant.identifier)
|
|
|
|
function getCurrentComputeInstanceSpecs() {
|
|
if (computeInstance?.variant.meta) {
|
|
// If user has a compute instance (called addons) return that
|
|
return computeInstance?.variant.meta as InstanceSpecs
|
|
} else {
|
|
// Otherwise, return the default specs
|
|
return project?.infra_compute_size === 'nano' ? INSTANCE_NANO_SPECS : INSTANCE_MICRO_SPECS
|
|
}
|
|
}
|
|
|
|
const currentComputeInstanceSpecs = getCurrentComputeInstanceSpecs()
|
|
|
|
const currentBillingCycleSelected = useMemo(() => {
|
|
// Selected by default
|
|
if (!dateRange?.period_start || !dateRange?.period_end || !subscription) return true
|
|
|
|
const { current_period_start, current_period_end } = subscription
|
|
|
|
return (
|
|
dayjs(dateRange.period_start.date).isSame(new Date(current_period_start * 1000)) &&
|
|
dayjs(dateRange.period_end.date).isSame(new Date(current_period_end * 1000))
|
|
)
|
|
}, [dateRange, subscription])
|
|
|
|
const upgradeUrl =
|
|
organization === undefined
|
|
? `/`
|
|
: hasAccessToComputeSizes
|
|
? `/org/${organization?.slug ?? '[slug]'}/billing#subscription`
|
|
: `/project/${projectRef}/settings/addons`
|
|
|
|
const categoryMeta = INFRA_ACTIVITY_METRICS.find((category) => category.key === 'infra')
|
|
|
|
const startDate = useMemo(() => {
|
|
if (dateRange?.period_start?.date === 'Invalid Date') return undefined
|
|
|
|
// If end date is in future, set end date to now
|
|
if (!dateRange?.period_start?.date) {
|
|
return undefined
|
|
} else {
|
|
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
|
return new Date(dateRange?.period_start?.date ?? 0).toISOString().slice(0, -5) + 'Z'
|
|
}
|
|
}, [dateRange])
|
|
|
|
const endDate = useMemo(() => {
|
|
if (dateRange?.period_end?.date === 'Invalid Date') return undefined
|
|
|
|
// If end date is in future, set end date to end of current day
|
|
if (dateRange?.period_end?.date && dayjs(dateRange.period_end.date).isAfter(dayjs())) {
|
|
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
|
// In order to have full days from Prometheus metrics when using 1d interval,
|
|
// the time needs to be greater or equal than the time of the start date
|
|
return dayjs().endOf('day').toISOString().slice(0, -5) + 'Z'
|
|
} else if (dateRange?.period_end?.date) {
|
|
// LF seems to have an issue with the milliseconds, causes infinite loading sometimes
|
|
return new Date(dateRange.period_end.date ?? 0).toISOString().slice(0, -5) + 'Z'
|
|
}
|
|
}, [dateRange])
|
|
|
|
// Switch to hourly interval, if the timeframe is <48 hours
|
|
let interval: '1d' | '1h' = '1d'
|
|
let dateFormat = 'DD MMM'
|
|
if (startDate && endDate) {
|
|
const diffInHours = dayjs(endDate).diff(startDate, 'hours')
|
|
|
|
if (diffInHours <= 48) {
|
|
interval = '1h'
|
|
dateFormat = 'h a'
|
|
}
|
|
}
|
|
|
|
const { data: infraMonitoringData, isPending: isLoadingInfraData } =
|
|
useInfraMonitoringAttributesQuery({
|
|
projectRef,
|
|
attributes: INFRA_ATTRIBUTES,
|
|
interval,
|
|
startDate,
|
|
endDate,
|
|
databaseIdentifier: state.selectedDatabaseId,
|
|
})
|
|
|
|
const transformedData = useMemo(() => {
|
|
if (!infraMonitoringData) return undefined
|
|
return mapMultiResponseToAnalyticsData(infraMonitoringData, INFRA_ATTRIBUTES, dateFormat)
|
|
}, [infraMonitoringData, dateFormat])
|
|
|
|
const cpuUsageData = transformedData?.max_cpu_usage
|
|
const memoryUsageData = transformedData?.ram_usage
|
|
const ioBudgetData = transformedData?.disk_io_consumption
|
|
|
|
const hasLatest = dayjs(endDate!).isAfter(dayjs().startOf('day'))
|
|
|
|
const latestIoBudgetConsumption =
|
|
hasLatest && ioBudgetData?.data?.slice(-1)?.[0]
|
|
? Number(ioBudgetData.data.slice(-1)[0].disk_io_consumption)
|
|
: 0
|
|
|
|
const highestIoBudgetConsumption = (ioBudgetData?.data || [])
|
|
.map((x) => Number(x.disk_io_consumption) ?? 0)
|
|
.reduce((a, b) => Math.max(a, b), 0)
|
|
|
|
const chartMeta: { [key: string]: { data: DataPoint[]; isLoading: boolean } } = {
|
|
max_cpu_usage: {
|
|
isLoading: isLoadingInfraData,
|
|
data: cpuUsageData?.data ?? [],
|
|
},
|
|
ram_usage: {
|
|
isLoading: isLoadingInfraData,
|
|
data: memoryUsageData?.data ?? [],
|
|
},
|
|
disk_io_consumption: {
|
|
isLoading: isLoadingInfraData,
|
|
data: ioBudgetData?.data ?? [],
|
|
},
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<ScaffoldContainer id="infrastructure-activity">
|
|
<div className="mx-auto flex flex-col gap-10 pt-6">
|
|
<div>
|
|
<p className="text-xl">Infrastructure Activity</p>
|
|
<p className="text-sm text-foreground-light">
|
|
Activity statistics related to your server instance
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</ScaffoldContainer>
|
|
<ScaffoldContainer className="sticky top-0 py-6 border-b bg-studio z-10">
|
|
<div className="flex items-center gap-x-4">
|
|
<DatabaseSelector />
|
|
{!isLoadingSubscription && (
|
|
<>
|
|
<DateRangePicker
|
|
onChange={setDateRange}
|
|
value={TIME_PERIODS_REPORTS[0].key}
|
|
options={[...TIME_PERIODS_BILLING, ...TIME_PERIODS_REPORTS]}
|
|
loading={isLoadingSubscription}
|
|
currentBillingPeriodStart={subscription?.current_period_start}
|
|
currentBillingPeriodEnd={subscription?.current_period_end}
|
|
/>
|
|
<p className="text-sm text-foreground-light">
|
|
{dayjs(startDate).format('DD MMM YYYY')} - {dayjs(endDate).format('DD MMM YYYY')}
|
|
</p>
|
|
</>
|
|
)}
|
|
</div>
|
|
</ScaffoldContainer>
|
|
|
|
{categoryMeta?.attributes.map((attribute) => {
|
|
const chartData = chartMeta[attribute.key]?.data ?? []
|
|
|
|
return (
|
|
<Fragment key={attribute.key}>
|
|
<ScaffoldContainer id={attribute.anchor}>
|
|
<ScaffoldSection>
|
|
<ScaffoldSectionDetail>
|
|
<div className="sticky top-32 space-y-6">
|
|
<div className="space-y-1">
|
|
<div className="flex items-center space-x-2">
|
|
<h4 className="text-base capitalize m-0">{attribute.name}</h4>
|
|
</div>
|
|
<div className="grid gap-4">
|
|
{attribute.description.split('\n').map((value, idx) => (
|
|
<p key={`desc-${idx}`} className="text-sm text-foreground-light pr-8 m-0">
|
|
{value}
|
|
</p>
|
|
))}
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<p className="text-sm text-foreground mb-2">More information</p>
|
|
{attribute.links.map((link) => (
|
|
<div key={link.url}>
|
|
<Link href={link.url} target="_blank" rel="noreferrer">
|
|
<div className="flex items-center space-x-2 opacity-50 hover:opacity-100 transition">
|
|
<p className="text-sm m-0">{link.name}</p>
|
|
<ExternalLink size={16} strokeWidth={1.5} />
|
|
</div>
|
|
</Link>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</ScaffoldSectionDetail>
|
|
<ScaffoldSectionContent>
|
|
{attribute.key === 'disk_io_consumption' && (
|
|
<>
|
|
<DiskIOBandwidthWarnings
|
|
upgradeUrl={upgradeUrl}
|
|
hasAccessToComputeSizes={hasAccessToComputeSizes}
|
|
hasLatest={hasLatest}
|
|
currentBillingCycleSelected={currentBillingCycleSelected}
|
|
latestIoBudgetConsumption={latestIoBudgetConsumption}
|
|
highestIoBudgetConsumption={highestIoBudgetConsumption}
|
|
/>
|
|
<div className="space-y-1">
|
|
<p>Disk IO Bandwidth</p>
|
|
|
|
{currentComputeInstanceSpecs.baseline_disk_io_mbs ===
|
|
currentComputeInstanceSpecs.max_disk_io_mbs ? (
|
|
<p className="text-sm text-foreground-light">
|
|
Your current compute has a baseline and maximum disk throughput of{' '}
|
|
{currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps.
|
|
</p>
|
|
) : (
|
|
<p className="text-sm text-foreground-light">
|
|
Your current compute can burst above the baseline disk throughput of{' '}
|
|
{currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
|
|
Mbps for short periods of time.
|
|
</p>
|
|
)}
|
|
</div>
|
|
<div>
|
|
<p className="text-sm mb-2">Overview</p>
|
|
<div className="flex items-center justify-between border-b py-1">
|
|
<p className="text-xs text-foreground-light">Current compute instance</p>
|
|
<p className="text-xs">
|
|
{computeInstance?.variant?.name ??
|
|
capitalize(project?.infra_compute_size) ??
|
|
'Micro'}
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center justify-between border-b py-1">
|
|
<p className="text-xs text-foreground-light">Baseline IO Bandwidth</p>
|
|
<p className="text-xs">
|
|
{currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '}
|
|
Mbps
|
|
</p>
|
|
</div>
|
|
<div className="flex items-center justify-between border-b py-1">
|
|
<p className="text-xs text-foreground-light">
|
|
Maximum IO Bandwidth (burst limit)
|
|
</p>
|
|
<p className="text-xs">
|
|
{currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps
|
|
</p>
|
|
</div>
|
|
{currentComputeInstanceSpecs.max_disk_io_mbs !==
|
|
currentComputeInstanceSpecs?.baseline_disk_io_mbs && (
|
|
<div className="flex items-center justify-between py-1">
|
|
<p className="text-xs text-foreground-light">Daily burst time limit</p>
|
|
<p className="text-xs">30 mins</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</>
|
|
)}
|
|
{attribute.key === 'max_cpu_usage' && (
|
|
<CPUWarnings
|
|
upgradeUrl={upgradeUrl}
|
|
hasAccessToComputeSizes={hasAccessToComputeSizes}
|
|
severity={projectResourceWarnings?.cpu_exhaustion}
|
|
/>
|
|
)}
|
|
{attribute.key === 'ram_usage' && (
|
|
<RAMWarnings
|
|
upgradeUrl={upgradeUrl}
|
|
hasAccessToComputeSizes={hasAccessToComputeSizes}
|
|
severity={projectResourceWarnings?.memory_and_swap_exhaustion}
|
|
/>
|
|
)}
|
|
|
|
<div className="space-y-1">
|
|
<div className="flex flex-row justify-between">
|
|
{attribute.key === 'disk_io_consumption' ? (
|
|
<p>Disk IO consumed per {interval === '1d' ? 'day' : 'hour'}</p>
|
|
) : (
|
|
<p>
|
|
Max{' '}
|
|
<span className={attribute.key === 'ram_usage' ? 'lowercase' : ''}>
|
|
{attribute.name}
|
|
</span>{' '}
|
|
utilization per {interval === '1d' ? 'day' : 'hour'}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{attribute.key === 'ram_usage' && (
|
|
<div className="text-sm text-foreground-light">
|
|
<p>
|
|
Your compute instance has {currentComputeInstanceSpecs.memory_gb} GB of
|
|
memory.
|
|
</p>
|
|
{currentComputeInstanceSpecs.memory_gb === 1 && (
|
|
<p>
|
|
As your project is running on the smallest compute instance, it is not
|
|
unusual for your project to have a base memory usage of ~50%.
|
|
</p>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{attribute.key === 'max_cpu_usage' && (
|
|
<p className="text-sm text-foreground-light">
|
|
Your compute instance has {currentComputeInstanceSpecs.cpu_cores} CPU cores.
|
|
</p>
|
|
)}
|
|
|
|
{attribute.chartDescription.split('\n').map((paragraph, idx) => (
|
|
<p key={`para-${idx}`} className="text-sm text-foreground-light">
|
|
{paragraph}
|
|
</p>
|
|
))}
|
|
</div>
|
|
{attribute.key === 'disk_io_consumption' && hasDedicatedIOResources ? (
|
|
<>
|
|
<Admonition
|
|
type="note"
|
|
title={`Your compute instance of ${computeInstance.variant.name} comes with dedicated I/O resources`}
|
|
description="Your project thus does not rely on I/O balance or burst capacity as larger
|
|
add-ons are designed for sustained, high performance with specific IOPS and
|
|
throughput limits without needing to burst."
|
|
>
|
|
<DocsButton
|
|
abbrev={false}
|
|
href={`${DOCS_URL}/guides/platform/compute-add-ons#disk-throughput-and-iops`}
|
|
/>
|
|
</Admonition>
|
|
</>
|
|
) : chartMeta[attribute.key].isLoading ? (
|
|
<div className="space-y-2">
|
|
<ShimmeringLoader />
|
|
<ShimmeringLoader className="w-3/4" />
|
|
<ShimmeringLoader className="w-1/2" />
|
|
</div>
|
|
) : chartData.length ? (
|
|
<UsageBarChart
|
|
name={`${attribute.chartPrefix || ''} ${attribute.name}`}
|
|
unit={attribute.unit}
|
|
attributes={attribute.attributes}
|
|
data={chartData}
|
|
yFormatter={(value) => `${Math.round(Number(value))}%`}
|
|
tooltipFormatter={(value) => `${value}%`}
|
|
yLimit={100}
|
|
/>
|
|
) : (
|
|
<Panel>
|
|
<Panel.Content>
|
|
<div className="flex flex-col items-center justify-center space-y-2">
|
|
<BarChart2 size={18} className="text-foreground-light mb-2" />
|
|
<p className="text-sm">No data in period</p>
|
|
<p className="text-sm text-foreground-light">
|
|
May take a few minutes to show
|
|
</p>
|
|
</div>
|
|
</Panel.Content>
|
|
</Panel>
|
|
)}
|
|
{attribute.key === 'disk_io_consumption' && !hasDedicatedIOResources && (
|
|
<Admonition
|
|
type="default"
|
|
title="Looking for actual disk activity?"
|
|
description="The chart above shows your remaining burst budget, not real disk throughput. For detailed read/write IOPS and throughput charts, head to the Database Observability page."
|
|
>
|
|
<Button asChild type="default" icon={<ChartLine size={14} />}>
|
|
<Link href={`/project/${projectRef}/observability/database`}>
|
|
View detailed IOPS and throughput
|
|
</Link>
|
|
</Button>
|
|
</Admonition>
|
|
)}
|
|
</ScaffoldSectionContent>
|
|
</ScaffoldSection>
|
|
</ScaffoldContainer>
|
|
<ScaffoldDivider />
|
|
</Fragment>
|
|
)
|
|
})}
|
|
</>
|
|
)
|
|
}
|