| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457 |
- 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>
- )
- })}
- </>
- )
- }
|