feat: Show resource usage in function metrics (#18956)

* feat: show resource usage charts in function metrics

* fix: show highlighted value in StackedBarChart

* fix: PR feedback updates
This commit is contained in:
Lakshan Perera authored and GitHub committed 2023-11-16 07:10:18 +11:00
1 parent d42ac302f9
commit ca8007ec80
5 files changed
+453 -49

No files matched your search

@@ -1,5 +1,7 @@
import { useState } from 'react'
import { Bar, BarChart, Cell, Legend, Tooltip, XAxis } from 'recharts'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import ChartHeader from './ChartHeader'
import {
CHART_COLORS,
@@ -9,8 +11,16 @@ import {
ValidStackColor,
} from './Charts.constants'
import { CommonChartProps } from './Charts.types'
import { precisionFormatter, timestampFormatter, useChartSize, useStacked } from './Charts.utils'
import {
numberFormatter,
precisionFormatter,
timestampFormatter,
useChartSize,
useStacked,
} from './Charts.utils'
import NoDataPlaceholder from './NoDataPlaceholder'
dayjs.extend(utc)
interface Props extends CommonChartProps<any> {
xAxisKey: string
yAxisKey: string
@@ -31,8 +41,11 @@ const StackedBarChart: React.FC<Props> = ({
yAxisKey,
customDateFormat = DateTimeFormats.FULL,
title,
highlightedValue,
highlightedLabel,
format,
minimalHeader = false,
valuePrecision,
onBarClick,
variant,
xAxisFormatAsDate = true,
@@ -50,6 +63,18 @@ const StackedBarChart: React.FC<Props> = ({
variant,
})
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
const day = (value: number | string) => (displayDateInUtc ? dayjs(value).utc() : dayjs(value))
const resolvedHighlightedLabel =
(focusDataIndex !== null &&
data &&
data[focusDataIndex] !== undefined &&
day(data[focusDataIndex][xAxisKey]).format(customDateFormat)) ||
highlightedLabel
const resolvedHighlightedValue =
focusDataIndex !== null ? data[focusDataIndex]?.[yAxisKey] : highlightedValue
if (!data || data.length === 0) return <NoDataPlaceholder size={size} />
const stackColorScales = genStackColorScales(stackColors)
return (
@@ -60,6 +85,12 @@ const StackedBarChart: React.FC<Props> = ({
format={format}
customDateFormat={customDateFormat}
minimalHeader={minimalHeader}
highlightedValue={
typeof resolvedHighlightedValue === 'number'
? numberFormatter(resolvedHighlightedValue, valuePrecision)
: resolvedHighlightedValue
}
highlightedLabel={resolvedHighlightedLabel}
/>
)}
<Container>
@@ -137,7 +168,11 @@ const StackedBarChart: React.FC<Props> = ({
}}
cursor={false}
labelClassName="text-white"
contentStyle={{ backgroundColor: '#444444', borderColor: '#444444', fontSize: '12px' }}
contentStyle={{
backgroundColor: '#444444',
borderColor: '#444444',
fontSize: '12px',
}}
wrapperClassName="bg-gray-600 rounded min-w-md"
/>
</BarChart>
@@ -0,0 +1,62 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { analyticsKeys } from './keys'
export type FunctionsReqStatsVariables = {
projectRef?: string
functionId?: string
interval?: string
}
export type FunctionsReqStatsResponse = any
export async function getFunctionsReqStats(
{ projectRef, functionId, interval }: FunctionsReqStatsVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
if (!functionId) {
throw new Error('functionId is required')
}
if (!interval) {
throw new Error('interval is required')
}
const response = await get<FunctionsReqStatsResponse>(
`${API_URL}/projects/${projectRef}/analytics/endpoints/functions.req-stats?interval=${interval}&function_id=${functionId}`,
{
signal,
}
)
if (response.error) {
throw response.error
}
return response
}
export type FunctionsReqStatsData = Awaited<ReturnType<typeof getFunctionsReqStats>>
export type FunctionsReqStatsError = unknown
export const useFunctionsReqStatsQuery = <TData = FunctionsReqStatsData>(
{ projectRef, functionId, interval }: FunctionsReqStatsVariables,
{
enabled = true,
...options
}: UseQueryOptions<FunctionsReqStatsData, FunctionsReqStatsError, TData> = {}
) =>
useQuery<FunctionsReqStatsData, FunctionsReqStatsError, TData>(
analyticsKeys.functionsReqStats(projectRef, { functionId, interval }),
({ signal }) => getFunctionsReqStats({ projectRef, functionId, interval }, signal),
{
enabled:
enabled &&
typeof projectRef !== 'undefined' &&
typeof functionId !== 'undefined' &&
typeof interval !== 'undefined',
...options,
}
)
@@ -0,0 +1,62 @@
import { useQuery, UseQueryOptions } from '@tanstack/react-query'
import { get } from 'lib/common/fetch'
import { API_URL } from 'lib/constants'
import { analyticsKeys } from './keys'
export type FunctionsResourceUsageVariables = {
projectRef?: string
functionId?: string
interval?: string
}
export type FunctionsResourceUsageResponse = any
export async function getFunctionsResourceUsage(
{ projectRef, functionId, interval }: FunctionsResourceUsageVariables,
signal?: AbortSignal
) {
if (!projectRef) {
throw new Error('projectRef is required')
}
if (!functionId) {
throw new Error('functionId is required')
}
if (!interval) {
throw new Error('interval is required')
}
const response = await get<FunctionsResourceUsageResponse>(
`${API_URL}/projects/${projectRef}/analytics/endpoints/functions.resource-usage?interval=${interval}&function_id=${functionId}`,
{
signal,
}
)
if (response.error) {
throw response.error
}
return response
}
export type FunctionsResourceUsageData = Awaited<ReturnType<typeof getFunctionsResourceUsage>>
export type FunctionsResourceUsageError = unknown
export const useFunctionsResourceUsageQuery = <TData = FunctionsResourceUsageData>(
{ projectRef, functionId, interval }: FunctionsResourceUsageVariables,
{
enabled = true,
...options
}: UseQueryOptions<FunctionsResourceUsageData, FunctionsResourceUsageError, TData> = {}
) =>
useQuery<FunctionsResourceUsageData, FunctionsResourceUsageError, TData>(
analyticsKeys.functionsResourceUsage(projectRef, { functionId, interval }),
({ signal }) => getFunctionsResourceUsage({ projectRef, functionId, interval }, signal),
{
enabled:
enabled &&
typeof projectRef !== 'undefined' &&
typeof functionId !== 'undefined' &&
typeof interval !== 'undefined',
...options,
}
)
+68 -4
View File
@@ -1,8 +1,62 @@
export const analyticsKeys = {
functionsInvStats: (
projectRef: string | undefined,
{ interval, functionId }: { functionId: string | undefined; interval: string | undefined }
) => ['projects', projectRef, 'functions-inv-stats', { interval, functionId }] as const,
{
interval,
functionId,
}: {
functionId: string | undefined
interval: string | undefined
}
) =>
[
'projects',
projectRef,
'functions-inv-stats',
{
interval,
functionId,
},
] as const,
functionsReqStats: (
projectRef: string | undefined,
{
interval,
functionId,
}: {
functionId: string | undefined
interval: string | undefined
}
) =>
[
'projects',
projectRef,
'functions-req-stats',
{
interval,
functionId,
},
] as const,
functionsResourceUsage: (
projectRef: string | undefined,
{
interval,
functionId,
}: {
functionId: string | undefined
interval: string | undefined
}
) =>
[
'projects',
projectRef,
'functions-resource-usage',
{
interval,
functionId,
},
] as const,
dailyStats: (
projectRef: string | undefined,
{
@@ -10,7 +64,12 @@ export const analyticsKeys = {
startDate,
endDate,
interval,
}: { attribute?: string; startDate?: string; endDate?: string; interval?: string }
}: {
attribute?: string
startDate?: string
endDate?: string
interval?: string
}
) =>
[
'projects',
@@ -58,7 +117,12 @@ export const analyticsKeys = {
startDate,
endDate,
interval,
}: { attribute?: string; startDate?: string; endDate?: string; interval?: string }
}: {
attribute?: string
startDate?: string
endDate?: string
interval?: string
}
) =>
[
'projects',
@@ -5,11 +5,14 @@ import { isUnixMicro, unixMicroToIsoTimestamp } from 'components/interfaces/Sett
import FunctionsLayout from 'components/layouts/FunctionsLayout'
import AreaChart from 'components/ui/Charts/AreaChart'
import BarChart from 'components/ui/Charts/BarChart'
import StackedBarChart from 'components/ui/Charts/StackedBarChart'
import NoPermission from 'components/ui/NoPermission'
import { useFunctionsInvStatsQuery } from 'data/analytics/functions-inv-stats-query'
import { useFunctionsReqStatsQuery } from 'data/analytics/functions-req-stats-query'
import { useFunctionsResourceUsageQuery } from 'data/analytics/functions-resource-usage-query'
import { useEdgeFunctionQuery } from 'data/edge-functions/edge-function-query'
import dayjs, { Dayjs } from 'dayjs'
import { useCheckPermissions } from 'hooks'
import { useCheckPermissions, useFlag } from 'hooks'
import useFillTimeseriesSorted from 'hooks/analytics/useFillTimeseriesSorted'
import sumBy from 'lodash/sumBy'
import { observer } from 'mobx-react-lite'
@@ -26,10 +29,34 @@ const CHART_INTERVALS: ChartIntervals[] = [
startUnit: 'minute',
format: 'MMM D, h:mm:ssa',
},
{ key: '15min', label: '15 min', startValue: 15, startUnit: 'minute', format: 'MMM D, h:mma' },
{ key: '1hr', label: '1 hour', startValue: 1, startUnit: 'hour', format: 'MMM D, h:mma' },
{ key: '1day', label: '1 day', startValue: 1, startUnit: 'hour', format: 'MMM D, h:mma' },
{ key: '7day', label: '7 days', startValue: 7, startUnit: 'day', format: 'MMM D' },
{
key: '15min',
label: '15 min',
startValue: 15,
startUnit: 'minute',
format: 'MMM D, h:mma',
},
{
key: '1hr',
label: '1 hour',
startValue: 1,
startUnit: 'hour',
format: 'MMM D, h:mma',
},
{
key: '1day',
label: '1 day',
startValue: 1,
startUnit: 'hour',
format: 'MMM D, h:mma',
},
{
key: '7day',
label: '7 days',
startValue: 7,
startUnit: 'day',
format: 'MMM D',
},
]
const PageLayout: NextPageWithLayout = () => {
@@ -37,21 +64,50 @@ const PageLayout: NextPageWithLayout = () => {
const { ref: projectRef, functionSlug } = useParams()
const [interval, setInterval] = useState<string>('15min')
const selectedInterval = CHART_INTERVALS.find((i) => i.key === interval) || CHART_INTERVALS[1]
const { data: selectedFunction } = useEdgeFunctionQuery({ projectRef, slug: functionSlug })
const id = selectedFunction?.id
const { data, error } = useFunctionsInvStatsQuery({
const { data: selectedFunction } = useEdgeFunctionQuery({
projectRef,
functionId: id,
interval: selectedInterval.key,
slug: functionSlug,
})
const isChartLoading = !data?.result && !error ? true : false
const normalizedData = useMemo(() => {
return (data?.result || []).map((d: any) => ({
...d,
timestamp: isUnixMicro(d.timestamp) ? unixMicroToIsoTimestamp(d.timestamp) : d.timestamp,
}))
}, [data?.result])
const id = selectedFunction?.id
const resourceUsageMetricsEnabled = useFlag('enableResourceUsageMetricsForEdgeFunctions')
const invStatsResult = useFunctionsInvStatsQuery(
{
projectRef,
functionId: id,
interval: selectedInterval.key,
},
{ enabled: !resourceUsageMetricsEnabled }
)
const reqStatsResult = useFunctionsReqStatsQuery(
{
projectRef,
functionId: id,
interval: selectedInterval.key,
},
{ enabled: resourceUsageMetricsEnabled }
)
const resourceUsageResult = useFunctionsResourceUsageQuery(
{
projectRef,
functionId: id,
interval: selectedInterval.key,
},
{ enabled: resourceUsageMetricsEnabled }
)
const reqStatsData = useMemo(() => {
const result = resourceUsageMetricsEnabled
? reqStatsResult.data?.result
: invStatsResult.data?.result
return result || []
}, [invStatsResult.data, reqStatsResult.data, resourceUsageMetricsEnabled])
const resourceUsageData = useMemo(() => {
return resourceUsageResult.data?.result || []
}, [resourceUsageResult.data])
const [startDate, endDate]: [Dayjs, Dayjs] = useMemo(() => {
const start = dayjs()
@@ -61,10 +117,29 @@ const PageLayout: NextPageWithLayout = () => {
const end = dayjs().startOf(selectedInterval.startUnit as dayjs.ManipulateType)
return [start, end]
}, [selectedInterval])
const chartData = useFillTimeseriesSorted(
normalizedData,
const execTimeChartData = useFillTimeseriesSorted(
reqStatsData,
'timestamp',
['avg_execution_time', 'count'],
['avg_execution_time'],
0,
startDate.toISOString(),
endDate.toISOString()
)
const invocationsChartData = useFillTimeseriesSorted(
reqStatsData,
'timestamp',
['count', 'success_count', 'redirect_count', 'client_err_count', 'server_err_count'],
0,
startDate.toISOString(),
endDate.toISOString()
)
const resourceUsageChartData = useFillTimeseriesSorted(
resourceUsageData,
'timestamp',
['avg_cpu_time_used', 'avg_memory_used'],
0,
startDate.toISOString(),
endDate.toISOString()
@@ -114,10 +189,12 @@ const PageLayout: NextPageWithLayout = () => {
<ReportWidget
title="Execution time"
tooltip="Average execution time of function invocations"
data={chartData}
isLoading={isChartLoading}
data={execTimeChartData}
isLoading={
resourceUsageMetricsEnabled ? reqStatsResult.isLoading : invStatsResult.isLoading
}
renderer={(props) => {
const latest = normalizedData[normalizedData.length - 1]
const latest = props.data[props.data.length - 1]
let highlightedValue
if (latest) {
highlightedValue = latest['avg_execution_time']
@@ -135,28 +212,132 @@ const PageLayout: NextPageWithLayout = () => {
)
}}
/>
<ReportWidget
title="Invocations"
data={chartData}
isLoading={isChartLoading}
renderer={(props) => (
<BarChart
className="w-full"
xAxisKey="timestamp"
yAxisKey="count"
data={props.data}
highlightedValue={sumBy(props.data, 'count')}
customDateFormat={selectedInterval.format}
onBarClick={(v) => {
router.push(
`/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}&ite=${
v.timestamp
}`
{resourceUsageMetricsEnabled ? (
<ReportWidget
title="Invocations"
data={invocationsChartData}
isLoading={reqStatsResult.isLoading}
renderer={(props) => {
const data = props.data
.map((d: any) => [
{
status: '2xx',
count: d.success_count,
timestamp: d.timestamp,
},
{
status: '3xx',
count: d.redirect_count,
timestamp: d.timestamp,
},
{
status: '4xx',
count: d.client_err_count,
timestamp: d.timestamp,
},
{
status: '5xx',
count: d.server_err_count,
timestamp: d.timestamp,
},
])
.flat()
return (
<StackedBarChart
className="w-full"
xAxisKey="timestamp"
yAxisKey="count"
stackKey="status"
data={data}
highlightedValue={sumBy(data, 'count')}
customDateFormat={selectedInterval.format}
stackColors={['brand', 'slate', 'yellow', 'red']}
onBarClick={() => {
router.push(
`/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}`
)
}}
/>
)
}}
/>
) : (
<ReportWidget
title="Invocations"
data={invocationsChartData}
isLoading={invStatsResult.isLoading}
renderer={(props) => (
<BarChart
className="w-full"
xAxisKey="timestamp"
yAxisKey="count"
data={props.data}
highlightedValue={sumBy(props.data, 'count')}
customDateFormat={selectedInterval.format}
onBarClick={(v) => {
router.push(
`/project/${projectRef}/functions/${functionSlug}/invocations?its=${startDate.toISOString()}&ite=${
v.timestamp
}`
)
}}
/>
)}
/>
)}
{resourceUsageMetricsEnabled && (
<>
<ReportWidget
title="CPU time"
tooltip="Average CPU time usage for the function"
data={resourceUsageChartData}
isLoading={resourceUsageResult.isLoading}
renderer={(props) => {
const latest = props.data[props.data.length - 1]
let highlightedValue
if (latest) {
highlightedValue = latest['avg_cpu_time_used']
}
return (
<AreaChart
className="w-full"
xAxisKey="timestamp"
customDateFormat={selectedInterval.format}
yAxisKey="avg_cpu_time_used"
data={props.data}
format="ms"
highlightedValue={highlightedValue}
/>
)
}}
/>
)}
/>
<ReportWidget
title="Memory"
tooltip="Average memory usage for the function"
data={resourceUsageChartData}
isLoading={resourceUsageResult.isLoading}
renderer={(props) => {
const latest = props.data[props.data.length - 1]
let highlightedValue
if (latest) {
highlightedValue = latest['avg_memory_used']
}
return (
<AreaChart
className="w-full"
xAxisKey="timestamp"
customDateFormat={selectedInterval.format}
yAxisKey="avg_memory_used"
data={props.data}
format="MB"
highlightedValue={highlightedValue}
/>
)
}}
/>
</>
)}
</div>
</div>
</div>