add cache chart

This commit is contained in:
Jonathan Summers-Muir authored and kemal committed 2025-08-08 11:39:10 +01:00
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