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() 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 ( <>

Infrastructure Activity

Activity statistics related to your server instance

{!isLoadingSubscription && ( <>

{dayjs(startDate).format('DD MMM YYYY')} - {dayjs(endDate).format('DD MMM YYYY')}

)}
{categoryMeta?.attributes.map((attribute) => { const chartData = chartMeta[attribute.key]?.data ?? [] return (

{attribute.name}

{attribute.description.split('\n').map((value, idx) => (

{value}

))}

More information

{attribute.links.map((link) => (

{link.name}

))}
{attribute.key === 'disk_io_consumption' && ( <>

Disk IO Bandwidth

{currentComputeInstanceSpecs.baseline_disk_io_mbs === currentComputeInstanceSpecs.max_disk_io_mbs ? (

Your current compute has a baseline and maximum disk throughput of{' '} {currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps.

) : (

Your current compute can burst above the baseline disk throughput of{' '} {currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '} Mbps for short periods of time.

)}

Overview

Current compute instance

{computeInstance?.variant?.name ?? capitalize(project?.infra_compute_size) ?? 'Micro'}

Baseline IO Bandwidth

{currentComputeInstanceSpecs.baseline_disk_io_mbs?.toLocaleString()}{' '} Mbps

Maximum IO Bandwidth (burst limit)

{currentComputeInstanceSpecs.max_disk_io_mbs?.toLocaleString()} Mbps

{currentComputeInstanceSpecs.max_disk_io_mbs !== currentComputeInstanceSpecs?.baseline_disk_io_mbs && (

Daily burst time limit

30 mins

)}
)} {attribute.key === 'max_cpu_usage' && ( )} {attribute.key === 'ram_usage' && ( )}
{attribute.key === 'disk_io_consumption' ? (

Disk IO consumed per {interval === '1d' ? 'day' : 'hour'}

) : (

Max{' '} {attribute.name} {' '} utilization per {interval === '1d' ? 'day' : 'hour'}

)}
{attribute.key === 'ram_usage' && (

Your compute instance has {currentComputeInstanceSpecs.memory_gb} GB of memory.

{currentComputeInstanceSpecs.memory_gb === 1 && (

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%.

)}
)} {attribute.key === 'max_cpu_usage' && (

Your compute instance has {currentComputeInstanceSpecs.cpu_cores} CPU cores.

)} {attribute.chartDescription.split('\n').map((paragraph, idx) => (

{paragraph}

))}
{attribute.key === 'disk_io_consumption' && hasDedicatedIOResources ? ( <> ) : chartMeta[attribute.key].isLoading ? (
) : chartData.length ? ( `${Math.round(Number(value))}%`} tooltipFormatter={(value) => `${value}%`} yLimit={100} /> ) : (

No data in period

May take a few minutes to show

)} {attribute.key === 'disk_io_consumption' && !hasDedicatedIOResources && ( )}
) })} ) }