mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 05:39:11 +03:00
add cache chart
This commit is contained in:
1 parent
50006e70c1
commit
40662ceff3
4 files changed
+572
-528
No files matched your search
@@ -17,7 +17,12 @@ import {
|
||||
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
|
||||
import { useReadReplicasQuery } from 'data/read-replicas/replicas-query'
|
||||
|
||||
export type MetricType = 'rows_read' | 'rows_written' | 'query_latency' | 'queries_per_second'
|
||||
export type MetricType =
|
||||
| 'rows_read'
|
||||
| 'rows_written'
|
||||
| 'query_latency'
|
||||
| 'queries_per_second'
|
||||
| 'cache_hits'
|
||||
|
||||
const METRICS: { id: MetricType; label: string; description: string }[] = [
|
||||
{
|
||||
@@ -40,6 +45,11 @@ const METRICS: { id: MetricType; label: string; description: string }[] = [
|
||||
label: 'Queries per Second',
|
||||
description: 'Number of queries executed per second',
|
||||
},
|
||||
{
|
||||
id: 'cache_hits',
|
||||
label: 'Cache Hits',
|
||||
description: 'Statistics for shared buffer cache usage',
|
||||
},
|
||||
]
|
||||
|
||||
export const QueryInsights = () => {
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
import { QueryInsightsMetric } from 'data/query-insights/query-insights-query'
|
||||
import { formatLatency, formatMetricValue } from './QueryInsights.utils'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export interface ChartConfigType {
|
||||
[key: string]: {
|
||||
label: string
|
||||
color: string
|
||||
formatter: (value: number) => string
|
||||
}
|
||||
}
|
||||
|
||||
// Define return types for each chart configuration
|
||||
export interface RowsReadChartData {
|
||||
timestamp: string
|
||||
rows: number
|
||||
query_rows: number
|
||||
}
|
||||
|
||||
export interface QueryLatencyChartData {
|
||||
timestamp: string
|
||||
p50: number | undefined
|
||||
p95: number | undefined
|
||||
p99: number | undefined
|
||||
p99_9: number | undefined
|
||||
query_latency: number
|
||||
}
|
||||
|
||||
export interface CacheHitsChartData {
|
||||
timestamp: string
|
||||
cache_hit_ratio: number
|
||||
cache_miss_ratio: number
|
||||
shared_blks_hit: number
|
||||
shared_blks_read: number
|
||||
shared_blks_dirtied: number
|
||||
shared_blks_written: number
|
||||
}
|
||||
|
||||
export interface GenericChartData {
|
||||
timestamp: string
|
||||
ok_count: number
|
||||
warning_count: number
|
||||
error_count: number
|
||||
}
|
||||
|
||||
export interface ChartConfigResult<T> {
|
||||
chartData: T[]
|
||||
config: ChartConfigType
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares data and configuration for the Rows Read chart
|
||||
*/
|
||||
export function getRowsReadConfig(
|
||||
data: QueryInsightsMetric[],
|
||||
queryRowsData?: QueryInsightsMetric[]
|
||||
): ChartConfigResult<RowsReadChartData> {
|
||||
// Create the baseline chart data from the metrics
|
||||
const chartData = data.map((point) => ({
|
||||
timestamp: point.timestamp,
|
||||
rows: Number(point.value) ?? 0,
|
||||
query_rows: 0,
|
||||
}))
|
||||
|
||||
// Sort chart data by timestamp
|
||||
chartData.sort((a, b) => dayjs(a.timestamp).diff(dayjs(b.timestamp)))
|
||||
|
||||
// If we have query rows data, add it to the chart
|
||||
if (queryRowsData && queryRowsData.length > 0) {
|
||||
// Create a map of timestamps to query rows values
|
||||
const queryRowsMap = new Map<string, number>()
|
||||
queryRowsData.forEach((point) => {
|
||||
queryRowsMap.set(point.timestamp, Number(point.value) || 0)
|
||||
})
|
||||
|
||||
// Add query rows data to each chart data point
|
||||
chartData.forEach((point) => {
|
||||
if (queryRowsMap.has(point.timestamp)) {
|
||||
point.query_rows = queryRowsMap.get(point.timestamp)!
|
||||
}
|
||||
})
|
||||
|
||||
// Add any missing query rows points
|
||||
queryRowsData.forEach((point) => {
|
||||
if (!chartData.find((d) => d.timestamp === point.timestamp)) {
|
||||
chartData.push({
|
||||
timestamp: point.timestamp,
|
||||
rows: 0,
|
||||
query_rows: Number(point.value) || 0,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Re-sort chart data by timestamp
|
||||
chartData.sort((a, b) => dayjs(a.timestamp).diff(dayjs(b.timestamp)))
|
||||
}
|
||||
|
||||
const config: ChartConfigType = {
|
||||
rows: {
|
||||
label: 'Rows Read',
|
||||
color: '#3ECF8E',
|
||||
formatter: formatMetricValue,
|
||||
},
|
||||
query_rows: {
|
||||
label: 'Query Rows',
|
||||
color: '#9BA6B0',
|
||||
formatter: formatMetricValue,
|
||||
},
|
||||
}
|
||||
|
||||
return { chartData, config }
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares data and configuration for the Query Latency chart
|
||||
*/
|
||||
export function getQueryLatencyConfig(
|
||||
data: QueryInsightsMetric[],
|
||||
queryLatencyData?: QueryInsightsMetric[]
|
||||
): ChartConfigResult<QueryLatencyChartData> {
|
||||
// Create the baseline chart data from the metrics
|
||||
const chartData = data.map((point) => {
|
||||
// Keep the original timestamp to maintain 5-minute intervals
|
||||
const timestamp = point.timestamp
|
||||
return {
|
||||
timestamp,
|
||||
p50: point.p50,
|
||||
p95: point.p95,
|
||||
p99: point.p99,
|
||||
p99_9: point.p99_9,
|
||||
// Initialize query latency as 0
|
||||
query_latency: 0,
|
||||
}
|
||||
})
|
||||
|
||||
// Sort chart data by timestamp
|
||||
chartData.sort((a, b) => {
|
||||
return dayjs(a.timestamp).diff(dayjs(b.timestamp))
|
||||
})
|
||||
|
||||
// If we have query latency data, add it to the chart
|
||||
if (queryLatencyData && queryLatencyData.length > 0) {
|
||||
// Create a map of timestamps to query latency values
|
||||
const queryLatencyMap = new Map<string, number>()
|
||||
queryLatencyData.forEach((point) => {
|
||||
queryLatencyMap.set(point.timestamp, point.value || 0)
|
||||
})
|
||||
|
||||
// Add query latency data to each chart data point
|
||||
chartData.forEach((point) => {
|
||||
if (queryLatencyMap.has(point.timestamp)) {
|
||||
point.query_latency = queryLatencyMap.get(point.timestamp)!
|
||||
}
|
||||
})
|
||||
|
||||
// Add any missing query latency points
|
||||
queryLatencyData.forEach((point) => {
|
||||
if (!chartData.find((d) => d.timestamp === point.timestamp)) {
|
||||
chartData.push({
|
||||
timestamp: point.timestamp,
|
||||
p50: 0,
|
||||
p95: 0,
|
||||
p99: 0,
|
||||
p99_9: 0,
|
||||
query_latency: point.value || 0,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Re-sort chart data by timestamp
|
||||
chartData.sort((a, b) => dayjs(a.timestamp).diff(dayjs(b.timestamp)))
|
||||
}
|
||||
|
||||
const config: ChartConfigType = {
|
||||
p50: {
|
||||
label: 'p50',
|
||||
color: '#4B5563',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
p95: {
|
||||
label: 'p95',
|
||||
color: '#6B7280',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
p99: {
|
||||
label: 'p99',
|
||||
color: '#9CA3AF',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
p99_9: {
|
||||
label: 'p99.9',
|
||||
color: '#D1D5DB',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
query_latency: {
|
||||
label: 'Query latency',
|
||||
color: '#3B82F6',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
}
|
||||
|
||||
return { chartData, config }
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares data and configuration for the Cache Hits chart
|
||||
*/
|
||||
export function getCacheHitsConfig(
|
||||
data: QueryInsightsMetric[],
|
||||
queryCacheData?: QueryInsightsMetric[]
|
||||
): ChartConfigResult<CacheHitsChartData> {
|
||||
// Create the baseline chart data from the metrics
|
||||
const chartData = data.map((point) => {
|
||||
const hit = point.shared_blks_hit ?? 0
|
||||
const read = point.shared_blks_read ?? 0
|
||||
const total = hit + read
|
||||
|
||||
// Calculate percentages, handling division by zero
|
||||
const cache_hit_ratio = total > 0 ? (hit / total) * 100 : 0
|
||||
// Cache miss is the inverse of cache hit
|
||||
const cache_miss_ratio = total > 0 ? 100 - cache_hit_ratio : 0
|
||||
|
||||
return {
|
||||
timestamp: point.timestamp,
|
||||
shared_blks_hit: hit,
|
||||
shared_blks_read: read,
|
||||
shared_blks_dirtied: point.shared_blks_dirtied ?? 0,
|
||||
shared_blks_written: point.shared_blks_written ?? 0,
|
||||
cache_hit_ratio,
|
||||
cache_miss_ratio,
|
||||
}
|
||||
})
|
||||
|
||||
// Sort chart data by timestamp
|
||||
chartData.sort((a, b) => dayjs(a.timestamp).diff(dayjs(b.timestamp)))
|
||||
|
||||
// Format percentages with % sign
|
||||
const percentFormatter = (value: number) => `${value.toFixed(1)}%`
|
||||
|
||||
const config: ChartConfigType = {
|
||||
cache_hit_ratio: {
|
||||
label: 'Cache Hit Ratio',
|
||||
color: '#3ECF8E', // Green
|
||||
formatter: percentFormatter,
|
||||
},
|
||||
cache_miss_ratio: {
|
||||
label: 'Cache Miss Ratio',
|
||||
color: '#EF4444', // Red/danger
|
||||
formatter: percentFormatter,
|
||||
},
|
||||
shared_blks_dirtied: {
|
||||
label: 'Blocks Modified',
|
||||
color: '#9BA6B0', // Gray
|
||||
formatter: formatMetricValue,
|
||||
},
|
||||
shared_blks_written: {
|
||||
label: 'Blocks Written',
|
||||
color: '#6366F1', // Indigo
|
||||
formatter: formatMetricValue,
|
||||
},
|
||||
}
|
||||
|
||||
return { chartData, config }
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares data and configuration for generic metric charts like rows_written and queries_per_second
|
||||
*/
|
||||
export function getGenericMetricConfig(data: QueryInsightsMetric[]): {
|
||||
chartData: GenericChartData[]
|
||||
} {
|
||||
// For other metrics, prepare data for the bar chart
|
||||
const chartData = data.map((point) => ({
|
||||
timestamp: dayjs(point.timestamp).toISOString(),
|
||||
ok_count: point.value ?? 0,
|
||||
warning_count: 0,
|
||||
error_count: 0,
|
||||
}))
|
||||
|
||||
return { chartData }
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
ChartLegendContent,
|
||||
cn,
|
||||
} from 'ui'
|
||||
import { useState } from 'react'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useParams } from 'common'
|
||||
import { Button } from 'ui'
|
||||
import { MetricType } from '../QueryInsights'
|
||||
@@ -19,6 +19,17 @@ import { useSingleQueryLatency } from 'data/query-insights/single-query-latency-
|
||||
import { useSingleQueryRows } from 'data/query-insights/single-query-latency-query'
|
||||
import { motion, AnimatePresence } from 'framer-motion'
|
||||
import { formatLatency, formatMetricValue } from '../QueryInsights.utils'
|
||||
import {
|
||||
getRowsReadConfig,
|
||||
getQueryLatencyConfig,
|
||||
getGenericMetricConfig,
|
||||
ChartConfigType,
|
||||
RowsReadChartData,
|
||||
QueryLatencyChartData,
|
||||
GenericChartData,
|
||||
ChartConfigResult,
|
||||
getCacheHitsConfig,
|
||||
} from '../chartConfigs'
|
||||
|
||||
interface MetricsChartProps {
|
||||
data: QueryInsightsMetric[]
|
||||
@@ -29,14 +40,6 @@ interface MetricsChartProps {
|
||||
selectedQuery: QueryInsightsQuery | null
|
||||
}
|
||||
|
||||
interface ChartConfigType {
|
||||
[key: string]: {
|
||||
label: string
|
||||
color: string
|
||||
formatter: (value: number) => string
|
||||
}
|
||||
}
|
||||
|
||||
export function MetricsChart({
|
||||
data,
|
||||
metric,
|
||||
@@ -54,6 +57,12 @@ export function MetricsChart({
|
||||
query_latency: boolean
|
||||
rows: boolean
|
||||
query_rows: boolean
|
||||
shared_blks_hit: boolean
|
||||
shared_blks_read: boolean
|
||||
shared_blks_dirtied: boolean
|
||||
shared_blks_written: boolean
|
||||
cache_hit_ratio: boolean
|
||||
cache_miss_ratio: boolean
|
||||
}>({
|
||||
p50: true,
|
||||
p95: true,
|
||||
@@ -62,6 +71,12 @@ export function MetricsChart({
|
||||
query_latency: true,
|
||||
rows: true,
|
||||
query_rows: true,
|
||||
shared_blks_hit: true,
|
||||
shared_blks_read: true,
|
||||
shared_blks_dirtied: true,
|
||||
shared_blks_written: true,
|
||||
cache_hit_ratio: true,
|
||||
cache_miss_ratio: true,
|
||||
})
|
||||
|
||||
// Fetch single query latency data if a query is selected
|
||||
@@ -82,6 +97,27 @@ export function MetricsChart({
|
||||
enabled: !!selectedQuery && metric === 'rows_read',
|
||||
})
|
||||
|
||||
// Derive chart configuration based on the selected metric
|
||||
const chartConfig = useMemo(() => {
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
switch (metric) {
|
||||
case 'rows_read':
|
||||
return getRowsReadConfig(data, queryRowsData)
|
||||
case 'query_latency':
|
||||
return getQueryLatencyConfig(data, queryLatencyData)
|
||||
case 'cache_hits':
|
||||
return getCacheHitsConfig(data)
|
||||
case 'rows_written':
|
||||
case 'queries_per_second':
|
||||
return getGenericMetricConfig(data)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}, [data, metric, queryLatencyData, queryRowsData])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
@@ -98,7 +134,7 @@ export function MetricsChart({
|
||||
)
|
||||
}
|
||||
|
||||
if (!Array.isArray(data) || data.length === 0) {
|
||||
if (!Array.isArray(data) || data.length === 0 || !chartConfig) {
|
||||
return (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<motion.div
|
||||
@@ -114,59 +150,10 @@ export function MetricsChart({
|
||||
)
|
||||
}
|
||||
|
||||
if (metric === 'rows_read') {
|
||||
// Create the baseline chart data from the metrics
|
||||
const chartData = data.map((point) => ({
|
||||
timestamp: point.timestamp,
|
||||
rows: Number(point.value) ?? 0,
|
||||
query_rows: 0,
|
||||
}))
|
||||
|
||||
// Sort chart data by timestamp
|
||||
chartData.sort((a, b) => dayjs(a.timestamp).diff(dayjs(b.timestamp)))
|
||||
|
||||
// If we have query rows data, add it to the chart
|
||||
if (selectedQuery && queryRowsData && queryRowsData.length > 0) {
|
||||
// Create a map of timestamps to query rows values
|
||||
const queryRowsMap = new Map<string, number>()
|
||||
queryRowsData.forEach((point) => {
|
||||
queryRowsMap.set(point.timestamp, Number(point.value) || 0)
|
||||
})
|
||||
|
||||
// Add query rows data to each chart data point
|
||||
chartData.forEach((point) => {
|
||||
if (queryRowsMap.has(point.timestamp)) {
|
||||
point.query_rows = queryRowsMap.get(point.timestamp)!
|
||||
}
|
||||
})
|
||||
|
||||
// Add any missing query rows points
|
||||
queryRowsData.forEach((point) => {
|
||||
if (!chartData.find((d) => d.timestamp === point.timestamp)) {
|
||||
chartData.push({
|
||||
timestamp: point.timestamp,
|
||||
rows: 0,
|
||||
query_rows: Number(point.value) || 0,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Re-sort chart data by timestamp
|
||||
chartData.sort((a, b) => dayjs(a.timestamp).diff(dayjs(b.timestamp)))
|
||||
}
|
||||
|
||||
const config: ChartConfigType = {
|
||||
rows: {
|
||||
label: 'Rows Read',
|
||||
color: '#3ECF8E',
|
||||
formatter: formatMetricValue,
|
||||
},
|
||||
query_rows: {
|
||||
label: 'Query Rows',
|
||||
color: '#9BA6B0',
|
||||
formatter: formatMetricValue,
|
||||
},
|
||||
}
|
||||
// Render charts based on metric type
|
||||
if (metric === 'rows_read' || metric === 'query_latency' || metric === 'cache_hits') {
|
||||
const config = (chartConfig as ChartConfigResult<any>).config
|
||||
const chartData = (chartConfig as ChartConfigResult<any>).chartData
|
||||
|
||||
return (
|
||||
<div className="h-[320px] flex flex-col">
|
||||
@@ -174,238 +161,8 @@ export function MetricsChart({
|
||||
<div className="flex gap-2 px-5 mb-4">
|
||||
<AnimatePresence>
|
||||
{Object.entries(config).map(([key, value]) => {
|
||||
// Only show query rows button if we have a selected query
|
||||
if (key === 'query_rows' && !selectedQuery) {
|
||||
return null
|
||||
}
|
||||
|
||||
const metricValue = Number(
|
||||
chartData[chartData.length - 1]?.[key as keyof (typeof chartData)[0]]
|
||||
)
|
||||
const formattedValue = !isNaN(metricValue) ? value.formatter(metricValue) : '0'
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
key={key}
|
||||
initial={{ opacity: 0, scale: 0.95, x: -8 }}
|
||||
animate={{ opacity: 1, scale: 1, x: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, x: -8 }}
|
||||
transition={{ duration: 0.15 }}
|
||||
className={cn(
|
||||
'text-xs px-2 py-1 rounded-full transition-colors inline-flex items-center gap-1.5 border',
|
||||
visibleMetrics[key as keyof typeof visibleMetrics]
|
||||
? 'bg-surface-300'
|
||||
: 'hover:bg-surface-100 border-transparent'
|
||||
)}
|
||||
onClick={() =>
|
||||
setVisibleMetrics((prev) => ({
|
||||
...prev,
|
||||
[key]: !prev[key as keyof typeof visibleMetrics],
|
||||
}))
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'w-2 h-2 rounded-full',
|
||||
!visibleMetrics[key as keyof typeof visibleMetrics] && 'opacity-50'
|
||||
)}
|
||||
style={{ backgroundColor: value.color }}
|
||||
/>
|
||||
<span>
|
||||
{value.label}: {formattedValue}
|
||||
</span>
|
||||
</motion.button>
|
||||
)
|
||||
})}
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<div className="flex-1 min-h-0">
|
||||
<ChartContainer className="h-full w-full" config={config}>
|
||||
<AreaChart
|
||||
data={chartData}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
margin={{ right: 16 }}
|
||||
>
|
||||
<defs>
|
||||
{Object.entries(config).map(([key, value]) => (
|
||||
<linearGradient key={key} id={`gradient-${key}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={value.color} stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor={value.color} stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
<CartesianGrid vertical={false} />
|
||||
<XAxis
|
||||
dataKey="timestamp"
|
||||
interval="preserveStartEnd"
|
||||
minTickGap={50}
|
||||
tickLine={false}
|
||||
axisLine={false}
|
||||
tickMargin={8}
|
||||
tickFormatter={(value) => dayjs(value).format('HH:mm')}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={formatMetricValue}
|
||||
domain={['dataMin', 'dataMax']}
|
||||
allowDataOverflow={false}
|
||||
/>
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dot" hideLabel />}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
{visibleMetrics.rows && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="rows"
|
||||
fill={`url(#gradient-rows)`}
|
||||
stroke={config.rows.color}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
fillOpacity={selectedQuery ? 0.1 : 0.3}
|
||||
strokeOpacity={selectedQuery ? 0.3 : 1}
|
||||
/>
|
||||
)}
|
||||
{selectedQuery && visibleMetrics.query_rows && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="query_rows"
|
||||
fill={`url(#gradient-query_rows)`}
|
||||
stroke={config.query_rows.color}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
/>
|
||||
)}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (metric === 'query_latency') {
|
||||
// Create the baseline chart data from the metrics
|
||||
const chartData = data.map((point) => {
|
||||
// Keep the original timestamp to maintain 5-minute intervals
|
||||
const timestamp = point.timestamp
|
||||
return {
|
||||
timestamp,
|
||||
p50: point.p50,
|
||||
p95: point.p95,
|
||||
p99: point.p99,
|
||||
p99_9: point.p99_9,
|
||||
// Initialize query latency as 0 instead of null
|
||||
query_latency: 0,
|
||||
}
|
||||
})
|
||||
|
||||
// Sort chart data by timestamp
|
||||
chartData.sort((a, b) => {
|
||||
return dayjs(a.timestamp).diff(dayjs(b.timestamp))
|
||||
})
|
||||
|
||||
// If we have query latency data, add it to the chart
|
||||
if (selectedQuery && queryLatencyData && queryLatencyData.length > 0) {
|
||||
// Create a map of timestamps to query latency values
|
||||
const queryLatencyMap = new Map<string, number>()
|
||||
queryLatencyData.forEach((point) => {
|
||||
queryLatencyMap.set(point.timestamp, point.value || 0)
|
||||
})
|
||||
|
||||
// Add query latency data to each chart data point
|
||||
chartData.forEach((point) => {
|
||||
if (queryLatencyMap.has(point.timestamp)) {
|
||||
point.query_latency = queryLatencyMap.get(point.timestamp)!
|
||||
}
|
||||
})
|
||||
|
||||
// Add any missing query latency points
|
||||
queryLatencyData.forEach((point) => {
|
||||
if (!chartData.find((d) => d.timestamp === point.timestamp)) {
|
||||
chartData.push({
|
||||
timestamp: point.timestamp,
|
||||
p50: 0,
|
||||
p95: 0,
|
||||
p99: 0,
|
||||
p99_9: 0,
|
||||
query_latency: point.value || 0,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
// Re-sort chart data by timestamp
|
||||
chartData.sort((a, b) => dayjs(a.timestamp).diff(dayjs(b.timestamp)))
|
||||
}
|
||||
|
||||
const config = {
|
||||
p50: {
|
||||
label: 'p50',
|
||||
color: '#4B5563',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
p95: {
|
||||
label: 'p95',
|
||||
color: '#6B7280',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
p99: {
|
||||
label: 'p99',
|
||||
color: '#9CA3AF',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
p99_9: {
|
||||
label: 'p99.9',
|
||||
color: '#D1D5DB',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
query_latency: {
|
||||
label: 'Query latency',
|
||||
color: '#3B82F6',
|
||||
formatter: formatLatency,
|
||||
},
|
||||
} satisfies ChartConfigType
|
||||
|
||||
return (
|
||||
<div className="h-[320px] flex flex-col">
|
||||
<AnimatePresence>
|
||||
{/* {selectedQuery && (
|
||||
<motion.div
|
||||
className="px-5 mb-4"
|
||||
initial={{ opacity: 0, height: 0 }}
|
||||
animate={{ opacity: 1, height: 'auto' }}
|
||||
exit={{ opacity: 0, height: 0 }}
|
||||
transition={{ duration: 0.1 }}
|
||||
>
|
||||
<div className="text-sm bg-surface-100 rounded p-2 border">
|
||||
<strong>Selected Query:</strong>{' '}
|
||||
<span className="text-xs font-mono truncate">
|
||||
{selectedQuery.query.substring(0, 80)}...
|
||||
</span>
|
||||
<div className="flex gap-4 mt-1 text-xs">
|
||||
<span>
|
||||
Mean Time: <strong>{Math.round(selectedQuery.mean_exec_time)} ms</strong>
|
||||
</span>
|
||||
<span>
|
||||
Total Calls: <strong>{selectedQuery.calls.toLocaleString()}</strong>
|
||||
</span>
|
||||
{isLoadingQueryLatency && <span>Loading query latency data...</span>}
|
||||
{Boolean(queryLatencyError) && (
|
||||
<span className="text-red-500">Error loading query latency</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)} */}
|
||||
</AnimatePresence>
|
||||
<div className="flex flex-col flex-1 min-h-0">
|
||||
<div className="flex gap-2 px-5 mb-4">
|
||||
<AnimatePresence>
|
||||
{Object.entries(config).map(([key, value]) => {
|
||||
// Only show query latency button if we have a selected query
|
||||
if (key === 'query_latency' && !selectedQuery) {
|
||||
// Only show specific buttons based on conditions
|
||||
if ((key === 'query_rows' || key === 'query_latency') && !selectedQuery) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -413,8 +170,8 @@ export function MetricsChart({
|
||||
chartData[chartData.length - 1]?.[key as keyof (typeof chartData)[0]]
|
||||
)
|
||||
const formattedValue = !isNaN(metricValue)
|
||||
? `${Math.round(metricValue)} ms`
|
||||
: '0 ms'
|
||||
? (value as any).formatter(metricValue)
|
||||
: '0'
|
||||
|
||||
return (
|
||||
<motion.button
|
||||
@@ -441,10 +198,10 @@ export function MetricsChart({
|
||||
'w-2 h-2 rounded-full',
|
||||
!visibleMetrics[key as keyof typeof visibleMetrics] && 'opacity-50'
|
||||
)}
|
||||
style={{ backgroundColor: value.color }}
|
||||
style={{ backgroundColor: (value as any).color }}
|
||||
/>
|
||||
<span>
|
||||
{value.label}: {formattedValue}
|
||||
{(value as any).label}: {formattedValue}
|
||||
</span>
|
||||
</motion.button>
|
||||
)
|
||||
@@ -461,8 +218,8 @@ export function MetricsChart({
|
||||
<defs>
|
||||
{Object.entries(config).map(([key, value]) => (
|
||||
<linearGradient key={key} id={`gradient-${key}`} x1="0" y1="0" x2="0" y2="1">
|
||||
<stop offset="5%" stopColor={value.color} stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor={value.color} stopOpacity={0.05} />
|
||||
<stop offset="5%" stopColor={(value as any).color} stopOpacity={0.3} />
|
||||
<stop offset="95%" stopColor={(value as any).color} stopOpacity={0.05} />
|
||||
</linearGradient>
|
||||
))}
|
||||
</defs>
|
||||
@@ -477,81 +234,199 @@ export function MetricsChart({
|
||||
tickFormatter={(value) => dayjs(value).format('HH:mm')}
|
||||
/>
|
||||
<YAxis
|
||||
tickFormatter={formatMetricValue}
|
||||
width={80}
|
||||
tickCount={5}
|
||||
scale="linear"
|
||||
domain={['auto', 'auto']}
|
||||
tickFormatter={
|
||||
metric === 'cache_hits' ? (value) => `${value}%` : formatMetricValue
|
||||
}
|
||||
domain={
|
||||
metric === 'cache_hits'
|
||||
? [0, 100]
|
||||
: metric === 'query_latency'
|
||||
? ['auto', 'auto']
|
||||
: ['dataMin', 'dataMax']
|
||||
}
|
||||
allowDataOverflow={false}
|
||||
width={metric === 'query_latency' ? 80 : undefined}
|
||||
tickCount={metric === 'query_latency' ? 5 : undefined}
|
||||
yAxisId="left"
|
||||
/>
|
||||
{metric === 'cache_hits' && (
|
||||
<YAxis
|
||||
yAxisId="right"
|
||||
orientation="right"
|
||||
tickFormatter={formatMetricValue}
|
||||
domain={['auto', 'auto']}
|
||||
allowDataOverflow={false}
|
||||
width={50}
|
||||
/>
|
||||
)}
|
||||
<ChartTooltip
|
||||
cursor={false}
|
||||
content={<ChartTooltipContent indicator="dot" hideLabel />}
|
||||
/>
|
||||
<ChartLegend content={<ChartLegendContent />} />
|
||||
{visibleMetrics.p50 && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="p50"
|
||||
fill={config.p50.color}
|
||||
fillOpacity={selectedQuery && visibleMetrics.query_latency ? 0.05 : 0.4}
|
||||
stroke={config.p50.color}
|
||||
strokeOpacity={selectedQuery && visibleMetrics.query_latency ? 0.1 : 0.8}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
/>
|
||||
|
||||
{/* Render series based on metric type */}
|
||||
{metric === 'rows_read' && (
|
||||
<>
|
||||
{visibleMetrics.rows && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="rows"
|
||||
fill={`url(#gradient-rows)`}
|
||||
stroke={config.rows.color}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
fillOpacity={selectedQuery ? 0.1 : 0.3}
|
||||
strokeOpacity={selectedQuery ? 0.3 : 1}
|
||||
yAxisId="left"
|
||||
/>
|
||||
)}
|
||||
{selectedQuery && visibleMetrics.query_rows && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="query_rows"
|
||||
fill={`url(#gradient-query_rows)`}
|
||||
stroke={config.query_rows.color}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
yAxisId="left"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{visibleMetrics.p95 && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="p95"
|
||||
fill={config.p95.color}
|
||||
fillOpacity={selectedQuery && visibleMetrics.query_latency ? 0.05 : 0.4}
|
||||
stroke={config.p95.color}
|
||||
strokeOpacity={selectedQuery && visibleMetrics.query_latency ? 0.1 : 0.8}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
/>
|
||||
|
||||
{metric === 'query_latency' && (
|
||||
<>
|
||||
{visibleMetrics.p50 && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="p50"
|
||||
fill={config.p50.color}
|
||||
fillOpacity={selectedQuery && visibleMetrics.query_latency ? 0.05 : 0.4}
|
||||
stroke={config.p50.color}
|
||||
strokeOpacity={selectedQuery && visibleMetrics.query_latency ? 0.1 : 0.8}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
yAxisId="left"
|
||||
/>
|
||||
)}
|
||||
{visibleMetrics.p95 && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="p95"
|
||||
fill={config.p95.color}
|
||||
fillOpacity={selectedQuery && visibleMetrics.query_latency ? 0.05 : 0.4}
|
||||
stroke={config.p95.color}
|
||||
strokeOpacity={selectedQuery && visibleMetrics.query_latency ? 0.1 : 0.8}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
yAxisId="left"
|
||||
/>
|
||||
)}
|
||||
{visibleMetrics.p99 && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="p99"
|
||||
fill={config.p99.color}
|
||||
fillOpacity={selectedQuery && visibleMetrics.query_latency ? 0.05 : 0.4}
|
||||
stroke={config.p99.color}
|
||||
strokeOpacity={selectedQuery && visibleMetrics.query_latency ? 0.1 : 0.8}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
yAxisId="left"
|
||||
/>
|
||||
)}
|
||||
{visibleMetrics.p99_9 && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="p99_9"
|
||||
fill={config.p99_9.color}
|
||||
fillOpacity={selectedQuery && visibleMetrics.query_latency ? 0.05 : 0.4}
|
||||
stroke={config.p99_9.color}
|
||||
strokeOpacity={selectedQuery && visibleMetrics.query_latency ? 0.1 : 0.8}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
yAxisId="left"
|
||||
/>
|
||||
)}
|
||||
{selectedQuery && visibleMetrics.query_latency && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="query_latency"
|
||||
fill={config.query_latency.color}
|
||||
fillOpacity={0.6}
|
||||
stroke={config.query_latency.color}
|
||||
strokeOpacity={1}
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
yAxisId="left"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{visibleMetrics.p99 && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="p99"
|
||||
fill={config.p99.color}
|
||||
fillOpacity={selectedQuery && visibleMetrics.query_latency ? 0.05 : 0.4}
|
||||
stroke={config.p99.color}
|
||||
strokeOpacity={selectedQuery && visibleMetrics.query_latency ? 0.1 : 0.8}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
/>
|
||||
)}
|
||||
{visibleMetrics.p99_9 && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="p99_9"
|
||||
fill={config.p99_9.color}
|
||||
fillOpacity={selectedQuery && visibleMetrics.query_latency ? 0.05 : 0.4}
|
||||
stroke={config.p99_9.color}
|
||||
strokeOpacity={selectedQuery && visibleMetrics.query_latency ? 0.1 : 0.8}
|
||||
strokeWidth={1}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
/>
|
||||
)}
|
||||
{selectedQuery && visibleMetrics.query_latency && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="query_latency"
|
||||
fill={config.query_latency.color}
|
||||
fillOpacity={0.6}
|
||||
stroke={config.query_latency.color}
|
||||
strokeOpacity={1}
|
||||
strokeWidth={2.5}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
/>
|
||||
|
||||
{metric === 'cache_hits' && (
|
||||
<>
|
||||
{visibleMetrics.cache_hit_ratio && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cache_hit_ratio"
|
||||
fill={`url(#gradient-cache_hit_ratio)`}
|
||||
stroke={config.cache_hit_ratio.color}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
fillOpacity={0.3}
|
||||
name="Cache Hit Ratio"
|
||||
yAxisId="left"
|
||||
/>
|
||||
)}
|
||||
{visibleMetrics.cache_miss_ratio && (
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey="cache_miss_ratio"
|
||||
fill={`url(#gradient-cache_miss_ratio)`}
|
||||
stroke={config.cache_miss_ratio.color}
|
||||
strokeWidth={2}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
fillOpacity={0.3}
|
||||
name="Cache Miss Ratio"
|
||||
yAxisId="left"
|
||||
/>
|
||||
)}
|
||||
{visibleMetrics.shared_blks_dirtied && (
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="shared_blks_dirtied"
|
||||
stroke={config.shared_blks_dirtied.color}
|
||||
strokeWidth={1.5}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
name="Blocks Modified"
|
||||
yAxisId="right"
|
||||
/>
|
||||
)}
|
||||
{visibleMetrics.shared_blks_written && (
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="shared_blks_written"
|
||||
stroke={config.shared_blks_written.color}
|
||||
strokeWidth={1.5}
|
||||
dot={false}
|
||||
animationDuration={100}
|
||||
name="Blocks Written"
|
||||
yAxisId="right"
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</AreaChart>
|
||||
</ChartContainer>
|
||||
@@ -562,167 +437,27 @@ export function MetricsChart({
|
||||
}
|
||||
|
||||
// For other metrics, use the bar chart
|
||||
const chartData = data.map((point) => ({
|
||||
timestamp: dayjs(point.timestamp).toISOString(),
|
||||
ok_count: point.value ?? 0,
|
||||
warning_count: 0,
|
||||
error_count: 0,
|
||||
}))
|
||||
|
||||
const toggleButtons = () => {
|
||||
switch (metric as MetricType) {
|
||||
case 'query_latency':
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.p50 ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() => setVisibleMetrics((prev) => ({ ...prev, p50: !prev.p50 }))}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#4B5563]" />
|
||||
<span className="text-xs">p50: {formatLatency(data[data.length - 1]?.p50 ?? 0)}</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.p95 ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() => setVisibleMetrics((prev) => ({ ...prev, p95: !prev.p95 }))}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#6B7280]" />
|
||||
<span className="text-xs">p95: {formatLatency(data[data.length - 1]?.p95 ?? 0)}</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.p99 ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() => setVisibleMetrics((prev) => ({ ...prev, p99: !prev.p99 }))}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#9CA3AF]" />
|
||||
<span className="text-xs">p99: {formatLatency(data[data.length - 1]?.p99 ?? 0)}</span>
|
||||
</Button>
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.p99_9 ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() => setVisibleMetrics((prev) => ({ ...prev, p99_9: !prev.p99_9 }))}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#D1D5DB]" />
|
||||
<span className="text-xs">
|
||||
p99.9: {formatLatency(data[data.length - 1]?.p99_9 ?? 0)}
|
||||
</span>
|
||||
</Button>
|
||||
{selectedQuery && (
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.query_latency ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() =>
|
||||
setVisibleMetrics((prev) => ({ ...prev, query_latency: !prev.query_latency }))
|
||||
}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#3B82F6]" />
|
||||
<span className="text-xs">
|
||||
Query latency:{' '}
|
||||
{formatLatency(queryLatencyData?.[queryLatencyData.length - 1]?.value ?? 0)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
case 'rows_read':
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.rows ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() => setVisibleMetrics((prev) => ({ ...prev, rows: !prev.rows }))}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#10B981]" />
|
||||
<span className="text-xs">
|
||||
Rows: {formatMetricValue(data[data.length - 1]?.value ?? 0)}
|
||||
</span>
|
||||
</Button>
|
||||
{selectedQuery && (
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.query_rows ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() =>
|
||||
setVisibleMetrics((prev) => ({ ...prev, query_rows: !prev.query_rows }))
|
||||
}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#3B82F6]" />
|
||||
<span className="text-xs">
|
||||
Query rows:{' '}
|
||||
{formatMetricValue(queryRowsData?.[queryRowsData.length - 1]?.value ?? 0)}
|
||||
</span>
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
case 'rows_written':
|
||||
case 'queries_per_second':
|
||||
return (
|
||||
<Button
|
||||
type="text"
|
||||
className={cn(
|
||||
'flex items-center gap-2 px-3',
|
||||
visibleMetrics.rows ? 'opacity-100' : 'opacity-50'
|
||||
)}
|
||||
onClick={() => setVisibleMetrics((prev) => ({ ...prev, rows: !prev.rows }))}
|
||||
>
|
||||
<div className="h-2 w-2 rounded-full bg-[#10B981]" />
|
||||
<span className="text-xs">
|
||||
Value: {formatMetricValue(data[data.length - 1]?.value ?? 0)}
|
||||
</span>
|
||||
</Button>
|
||||
)
|
||||
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
const { chartData } = chartConfig as { chartData: GenericChartData[] }
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2 px-4">{toggleButtons()}</div>
|
||||
<div className="h-[320px]">
|
||||
<motion.div
|
||||
className="h-full"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<LogsBarChart
|
||||
data={chartData}
|
||||
DateTimeFormat="HH:mm"
|
||||
EmptyState={
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-sm text-foreground-light">No data available</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
<div className="h-[320px]">
|
||||
<motion.div
|
||||
className="h-full"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<LogsBarChart
|
||||
data={chartData}
|
||||
DateTimeFormat="HH:mm"
|
||||
EmptyState={
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-sm text-foreground-light">No data available</div>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -10,6 +10,8 @@ export type QueryInsightsMetric = {
|
||||
database: string
|
||||
shared_blks_read?: number
|
||||
shared_blks_hit?: number
|
||||
shared_blks_dirtied?: number
|
||||
shared_blks_written?: number
|
||||
mean_exec_time?: number
|
||||
total_exec_time?: number
|
||||
p50?: number
|
||||
@@ -119,6 +121,22 @@ const getMetricsSql = (metric: string, startTime: string, endTime: string) => {
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
case 'cache_hits':
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
bucket_start_time as timestamp,
|
||||
SUM(shared_blks_hit) as shared_blks_hit,
|
||||
SUM(shared_blks_read) as shared_blks_read,
|
||||
SUM(shared_blks_dirtied) as shared_blks_dirtied,
|
||||
SUM(shared_blks_written) as shared_blks_written,
|
||||
datname as database
|
||||
FROM pg_stat_monitor
|
||||
WHERE bucket_start_time >= '${startTime}'::timestamptz
|
||||
AND bucket_start_time <= '${endTime}'::timestamptz
|
||||
AND bucket_done = true -- Only include completed buckets
|
||||
GROUP BY bucket_start_time, datname
|
||||
ORDER BY timestamp ASC
|
||||
`
|
||||
default:
|
||||
return /* SQL */ `
|
||||
SELECT
|
||||
|
||||
Reference in new issue
Block a user