mirror of
https://github.com/supabase/supabase.git
synced 2026-10-07 10:25:06 +03:00
feat: add network traffic charting
This commit is contained in:
1 parent
3dbc231a3f
commit
68dca39bf5
8 files changed
+139
-28
No files matched your search
@@ -75,8 +75,8 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
|
||||
timestamp ASC`,
|
||||
},
|
||||
topRoutes: {
|
||||
queryType: "logs",
|
||||
sql: (filters)=> `
|
||||
queryType: 'logs',
|
||||
sql: (filters) => `
|
||||
select
|
||||
request.path as path,
|
||||
request.method as method,
|
||||
@@ -95,7 +95,7 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
|
||||
count desc
|
||||
limit
|
||||
3
|
||||
`
|
||||
`,
|
||||
},
|
||||
errorCounts: {
|
||||
queryType: 'logs',
|
||||
@@ -118,8 +118,8 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
|
||||
`,
|
||||
},
|
||||
topErrorRoutes: {
|
||||
queryType: "logs",
|
||||
sql: (filters)=> `
|
||||
queryType: 'logs',
|
||||
sql: (filters) => `
|
||||
select
|
||||
request.path as path,
|
||||
request.method as method,
|
||||
@@ -140,7 +140,7 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
|
||||
count desc
|
||||
limit
|
||||
3
|
||||
`
|
||||
`,
|
||||
},
|
||||
responseSpeed: {
|
||||
queryType: 'logs',
|
||||
@@ -162,8 +162,8 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
|
||||
`,
|
||||
},
|
||||
topSlowRoutes: {
|
||||
queryType: "logs",
|
||||
sql: (filters)=> `
|
||||
queryType: 'logs',
|
||||
sql: (filters) => `
|
||||
select
|
||||
request.path as path,
|
||||
request.method as method,
|
||||
@@ -183,7 +183,44 @@ export const PRESET_CONFIG: Record<Presets, PresetConfig> = {
|
||||
avg desc
|
||||
limit
|
||||
3
|
||||
`
|
||||
`,
|
||||
},
|
||||
networkTraffic: {
|
||||
queryType: 'logs',
|
||||
sql: (filters) => `
|
||||
select
|
||||
cast(timestamp_trunc(t.timestamp, hour) as datetime) as timestamp,
|
||||
coalesce(
|
||||
safe_divide(
|
||||
sum(
|
||||
cast(coalesce(headers.content_length, "0") as int64)
|
||||
),
|
||||
1000000
|
||||
),
|
||||
0
|
||||
) as ingress_mb,
|
||||
coalesce(
|
||||
safe_divide(
|
||||
sum(
|
||||
cast(coalesce(resp_headers.content_length, "0") as int64)
|
||||
),
|
||||
1000000
|
||||
),
|
||||
0
|
||||
) as egress_mb,
|
||||
FROM
|
||||
edge_logs t
|
||||
cross join unnest(metadata) as m
|
||||
cross join unnest(m.response) as response
|
||||
cross join unnest(m.request) as request
|
||||
cross join unnest(request.headers) as headers
|
||||
cross join unnest(response.headers) as resp_headers
|
||||
${generateRexepWhere(filters)}
|
||||
GROUP BY
|
||||
timestamp
|
||||
ORDER BY
|
||||
timestamp ASC
|
||||
`,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -9,7 +9,59 @@ import { Button, Collapsible, IconChevronRight } from 'ui'
|
||||
import { queryParamsToObject } from '../Reports.utils'
|
||||
import { Fragment } from 'react'
|
||||
import useFillTimeseriesSorted from 'hooks/analytics/useFillTimeseriesSorted'
|
||||
import sumBy from 'lodash/sumBy'
|
||||
|
||||
export const networkTrafficRenderer = (
|
||||
props: ReportWidgetProps<{
|
||||
timestamp: string
|
||||
ingress: number
|
||||
egress: number
|
||||
}>
|
||||
) => {
|
||||
const data = useFillTimeseriesSorted(
|
||||
props.data,
|
||||
'timestamp',
|
||||
['ingress_mb', 'egress_mb'],
|
||||
0,
|
||||
props.params?.iso_timestamp_start,
|
||||
props.params?.iso_timestamp_end
|
||||
)
|
||||
const totalIngress = sumBy(props.data, 'ingress_mb')
|
||||
const totalEgress = sumBy(props.data, 'egress_mb')
|
||||
|
||||
function determinePrecision(valueInMb: number) {
|
||||
return valueInMb < 0.001 ? 7 : totalIngress > 1 ? 2 : 4
|
||||
}
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<BarChart
|
||||
size="small"
|
||||
title="Ingress"
|
||||
highlightedValue={sumBy(props.data, 'ingress_mb')}
|
||||
format="MB"
|
||||
className="w-full"
|
||||
valuePrecision={determinePrecision(totalIngress)}
|
||||
data={data}
|
||||
yAxisKey="ingress_mb"
|
||||
xAxisKey="timestamp"
|
||||
displayDateInUtc
|
||||
/>
|
||||
|
||||
<BarChart
|
||||
size="small"
|
||||
title="Egress"
|
||||
highlightedValue={totalEgress}
|
||||
format="MB"
|
||||
valuePrecision={determinePrecision(totalEgress)}
|
||||
className="w-full"
|
||||
data={data}
|
||||
yAxisKey="egress_mb"
|
||||
xAxisKey="timestamp"
|
||||
displayDateInUtc
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export const TotalRequestsChartRenderer = (
|
||||
props: ReportWidgetProps<{
|
||||
timestamp: string
|
||||
|
||||
@@ -24,7 +24,7 @@ export interface AreaChartProps<D = Datum> extends CommonChartProps<D> {
|
||||
displayDateInUtc?: boolean
|
||||
}
|
||||
|
||||
const AreaChart: React.FC<AreaChartProps> = ({
|
||||
const AreaChart = ({
|
||||
data,
|
||||
yAxisKey,
|
||||
xAxisKey,
|
||||
@@ -36,8 +36,9 @@ const AreaChart: React.FC<AreaChartProps> = ({
|
||||
displayDateInUtc,
|
||||
minimalHeader,
|
||||
className = '',
|
||||
valuePrecision,
|
||||
size = 'normal',
|
||||
}) => {
|
||||
}: AreaChartProps) => {
|
||||
const { Container } = useChartSize(size)
|
||||
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
|
||||
|
||||
@@ -62,7 +63,7 @@ const AreaChart: React.FC<AreaChartProps> = ({
|
||||
customDateFormat={customDateFormat}
|
||||
highlightedValue={
|
||||
typeof resolvedHighlightedValue === 'number'
|
||||
? numberFormatter(resolvedHighlightedValue)
|
||||
? numberFormatter(resolvedHighlightedValue, valuePrecision)
|
||||
: resolvedHighlightedValue
|
||||
}
|
||||
highlightedLabel={resolvedHighlightedLabel}
|
||||
|
||||
@@ -13,13 +13,12 @@ dayjs.extend(utc)
|
||||
export interface BarChartProps<D = Datum> extends CommonChartProps<D> {
|
||||
yAxisKey: string
|
||||
xAxisKey: string
|
||||
format?: string
|
||||
customDateFormat?: string
|
||||
displayDateInUtc?: boolean
|
||||
onBarClick?: (datum: Datum, tooltipData?: Parameters<CategoricalChartProps['onClick']>[0]) => void
|
||||
}
|
||||
|
||||
const BarChart: React.FC<BarChartProps> = ({
|
||||
const BarChart = ({
|
||||
data,
|
||||
yAxisKey,
|
||||
xAxisKey,
|
||||
@@ -30,10 +29,11 @@ const BarChart: React.FC<BarChartProps> = ({
|
||||
highlightedLabel,
|
||||
displayDateInUtc,
|
||||
minimalHeader,
|
||||
valuePrecision,
|
||||
className = '',
|
||||
size = 'normal',
|
||||
onBarClick,
|
||||
}) => {
|
||||
}: BarChartProps) => {
|
||||
const { Container } = useChartSize(size)
|
||||
const [focusDataIndex, setFocusDataIndex] = useState<number | null>(null)
|
||||
|
||||
@@ -58,7 +58,7 @@ const BarChart: React.FC<BarChartProps> = ({
|
||||
customDateFormat={customDateFormat}
|
||||
highlightedValue={
|
||||
typeof resolvedHighlightedValue === 'number'
|
||||
? numberFormatter(resolvedHighlightedValue)
|
||||
? numberFormatter(resolvedHighlightedValue, valuePrecision)
|
||||
: resolvedHighlightedValue
|
||||
}
|
||||
highlightedLabel={resolvedHighlightedLabel}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { DateTimeFormats } from './Charts.constants'
|
||||
|
||||
export interface ChartHeaderProps {
|
||||
title?: string
|
||||
format?: string
|
||||
format?: string | ((value: unknown) => string)
|
||||
customDateFormat?: string
|
||||
minimalHeader?: boolean
|
||||
displayDateInUtc?: boolean
|
||||
@@ -15,7 +13,7 @@ const ChartHeader: React.FC<ChartHeaderProps> = ({
|
||||
highlightedLabel,
|
||||
title,
|
||||
minimalHeader = false,
|
||||
}) => {
|
||||
}: ChartHeaderProps) => {
|
||||
const chartTitle = (
|
||||
<h3 className={'text-scale-900 ' + (minimalHeader ? 'text-xs' : 'text-sm')}>{title}</h3>
|
||||
)
|
||||
@@ -24,8 +22,10 @@ const ChartHeader: React.FC<ChartHeaderProps> = ({
|
||||
<h5
|
||||
className={`text-scale-1200 text-xl font-normal ${minimalHeader ? 'text-base' : 'text-2xl'}`}
|
||||
>
|
||||
{highlightedValue}
|
||||
<span className="text-lg">{format}</span>
|
||||
{highlightedValue !== undefined && String(highlightedValue)}
|
||||
<span className="text-lg">
|
||||
{typeof format === 'function' ? format(highlightedValue) : format}
|
||||
</span>
|
||||
</h5>
|
||||
)
|
||||
const label = <h5 className="text-scale-900 text-xs">{highlightedLabel}</h5>
|
||||
@@ -35,7 +35,7 @@ const ChartHeader: React.FC<ChartHeaderProps> = ({
|
||||
<div className="flex flex-row items-center gap-x-4" style={{ minHeight: '1.8rem' }}>
|
||||
{title && chartTitle}
|
||||
<div className="flex flex-row items-baseline gap-x-2">
|
||||
{highlightedValue && highlighted}
|
||||
{highlightedValue !== undefined && highlighted}
|
||||
{label}
|
||||
</div>
|
||||
</div>
|
||||
@@ -45,7 +45,7 @@ const ChartHeader: React.FC<ChartHeaderProps> = ({
|
||||
return (
|
||||
<div className="h-16">
|
||||
{title && chartTitle}
|
||||
{highlightedValue && highlighted}
|
||||
{highlightedValue !== undefined && highlighted}
|
||||
{label}
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@ export interface CommonChartProps<D>
|
||||
> {
|
||||
title?: string
|
||||
className?: string
|
||||
valuePrecision?: number
|
||||
size?: 'tiny' | 'small' | 'normal' | 'large'
|
||||
}
|
||||
|
||||
@@ -27,7 +28,7 @@ export interface StackedChartProps<D> extends CommonChartProps<D> {
|
||||
export type HeaderType<D> = {
|
||||
attribute: string
|
||||
focus: number | null
|
||||
format?: string
|
||||
format?: string | ((value: unknown) => string);
|
||||
highlightedValue?: number | string
|
||||
highlightedLabel?: string
|
||||
data: D[]
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
ErrorCountsChartRenderer,
|
||||
ResponseSpeedChartRenderer,
|
||||
TopApiRoutesRenderer,
|
||||
networkTrafficRenderer,
|
||||
} from 'components/interfaces/Reports/renderers/ApiRenderers'
|
||||
import { useState, useEffect } from 'react'
|
||||
import ReportHeader from 'components/interfaces/Reports/ReportHeader'
|
||||
@@ -88,6 +89,15 @@ export const ApiReport: NextPageWithLayout = () => {
|
||||
appendProps={{ data: report.data.topSlowRoutes || [] }}
|
||||
append={TopApiRoutesRenderer}
|
||||
/>
|
||||
|
||||
<ReportWidget
|
||||
isLoading={report.isLoading}
|
||||
params={report.params.networkTraffic}
|
||||
title="Network Traffic"
|
||||
tooltip="Ingress and egress of requests and responses respectively"
|
||||
data={report.data.networkTraffic || []}
|
||||
renderer={networkTrafficRenderer}
|
||||
/>
|
||||
</ReportPadding>
|
||||
)
|
||||
}
|
||||
@@ -106,6 +116,7 @@ const useApiReport = () => {
|
||||
const topErrorRoutes = queryHooks.topErrorRoutes()
|
||||
const responseSpeed = queryHooks.responseSpeed()
|
||||
const topSlowRoutes = queryHooks.topSlowRoutes()
|
||||
const networkTraffic = queryHooks.networkTraffic()
|
||||
const activeHooks = [
|
||||
totalRequests,
|
||||
topRoutes,
|
||||
@@ -113,6 +124,7 @@ const useApiReport = () => {
|
||||
topErrorRoutes,
|
||||
responseSpeed,
|
||||
topSlowRoutes,
|
||||
networkTraffic,
|
||||
]
|
||||
const [filters, setFilters] = useState<ReportFilterItem[]>([])
|
||||
const addFilter = (filter: ReportFilterItem) => {
|
||||
@@ -161,17 +173,21 @@ const useApiReport = () => {
|
||||
if (topSlowRoutes.changeQuery) {
|
||||
topSlowRoutes.changeQuery(PRESET_CONFIG.api.queries.topSlowRoutes.sql(filters))
|
||||
}
|
||||
|
||||
if (networkTraffic.changeQuery) {
|
||||
networkTraffic.changeQuery(PRESET_CONFIG.api.queries.networkTraffic.sql(filters))
|
||||
}
|
||||
}, [JSON.stringify(filters)])
|
||||
|
||||
const handleRefresh = async () => {
|
||||
activeHooks.forEach(hook => hook.runQuery())
|
||||
activeHooks.forEach((hook) => hook.runQuery())
|
||||
}
|
||||
const handleSetParams = (params: Partial<LogsEndpointParams>) => {
|
||||
activeHooks.forEach(hook => {
|
||||
activeHooks.forEach((hook) => {
|
||||
hook.setParams?.((prev: LogsEndpointParams) => ({ ...prev, ...params }))
|
||||
})
|
||||
}
|
||||
const isLoading = activeHooks.some(hook => hook.isLoading)
|
||||
const isLoading = activeHooks.some((hook) => hook.isLoading)
|
||||
return {
|
||||
data: {
|
||||
totalRequests: totalRequests.logData,
|
||||
@@ -180,6 +196,7 @@ const useApiReport = () => {
|
||||
topRoutes: topRoutes.logData,
|
||||
topErrorRoutes: topErrorRoutes.logData,
|
||||
topSlowRoutes: topSlowRoutes.logData,
|
||||
networkTraffic: networkTraffic.logData,
|
||||
},
|
||||
params: {
|
||||
totalRequests: totalRequests.params,
|
||||
@@ -188,6 +205,7 @@ const useApiReport = () => {
|
||||
topRoutes: topRoutes.params,
|
||||
topErrorRoutes: topErrorRoutes.params,
|
||||
topSlowRoutes: topSlowRoutes.params,
|
||||
networkTraffic: networkTraffic.params,
|
||||
},
|
||||
mergeParams: handleSetParams,
|
||||
filters,
|
||||
|
||||
@@ -15,6 +15,8 @@ test(`static elements`, async () => {
|
||||
await screen.findByText('Total Requests')
|
||||
await screen.findByText('Response Errors')
|
||||
await screen.findByText('Response Speed')
|
||||
await screen.findByText('Network Ingress')
|
||||
await screen.findByText('Network Egress')
|
||||
await screen.findByText(/Last 24 hours/)
|
||||
await screen.findByText(/Custom/)
|
||||
})
|
||||
|
||||
Reference in new issue
Block a user