Files
supabase/apps/studio/components/interfaces/QueryInsights/utils/supamonitor.utils.ts
T
7ed8ab83a8 feat(studio): query insights improvements (#43109)
## I have read the
[CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md)
file.

YES

## What kind of change does this PR introduce?

This introduces Query Insights. It's the first edition of possible
future updates. This takes our old prototype and builds upon it for a
more action driven insights view.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Ali Waseem <waseema393@gmail.com>
2026-03-13 15:09:26 +00:00

195 lines
6.2 KiB
TypeScript

import type { QueryPerformanceRow } from '../../QueryPerformance/QueryPerformance.types'
import type { ChartDataPoint, ParsedLogEntry } from '../QueryInsights.types'
import {
SUPAMONITOR_EXCLUDED_ROLES,
SUPAMONITOR_EXCLUDED_APP_NAMES,
TRANSACTION_CONTROL_REGEX,
SCHEMA_INTROSPECTION_REGEX,
} from '../QueryInsights.constants'
export function filterSystemLogs(
logs: ParsedLogEntry[],
{ includeIntrospection = false }: { includeIntrospection?: boolean } = {}
): ParsedLogEntry[] {
return logs.filter((log) => {
if (log.user_name && (SUPAMONITOR_EXCLUDED_ROLES as readonly string[]).includes(log.user_name))
return false
if (
log.application_name &&
(SUPAMONITOR_EXCLUDED_APP_NAMES as readonly string[]).includes(log.application_name)
)
return false
if (log.query && TRANSACTION_CONTROL_REGEX.test(log.query)) return false
if (!includeIntrospection && log.query && SCHEMA_INTROSPECTION_REGEX.test(log.query))
return false
return true
})
}
export function parseSupamonitorLogs(logData: any[]): ParsedLogEntry[] {
if (!logData || logData.length === 0) return []
return logData.map((log) => ({
timestamp: log.timestamp,
application_name: log.application_name,
calls: log.calls,
database_name: log.database_name,
query: log.query,
query_id: log.query_id,
total_exec_time: log.total_exec_time,
total_plan_time: log.total_plan_time,
user_name: log.user_name,
mean_exec_time: log.mean_exec_time,
mean_plan_time: log.mean_plan_time,
min_exec_time: log.min_exec_time,
max_exec_time: log.max_exec_time,
min_plan_time: log.min_plan_time,
max_plan_time: log.max_plan_time,
p50_exec_time: log.p50_exec_time,
p95_exec_time: log.p95_exec_time,
p50_plan_time: log.p50_plan_time,
p95_plan_time: log.p95_plan_time,
}))
}
export function transformLogsToChartData(parsedLogs: ParsedLogEntry[]): ChartDataPoint[] {
if (!parsedLogs || parsedLogs.length === 0) return []
return parsedLogs
.map((log: ParsedLogEntry) => {
if (!log.timestamp) return null
const periodStart = new Date(log.timestamp).getTime()
if (isNaN(periodStart)) return null
const meanExecTime = parseFloat(String(log.mean_exec_time ?? 0))
const meanPlanTime = parseFloat(String(log.mean_plan_time ?? 0))
const calls = parseInt(String(log.calls ?? 0), 10)
return {
period_start: periodStart,
timestamp: log.timestamp,
query_latency: meanExecTime + meanPlanTime,
mean_time: meanExecTime,
min_time: (log.min_exec_time ?? 0) + (log.min_plan_time ?? 0),
max_time: (log.max_exec_time ?? 0) + (log.max_plan_time ?? 0),
stddev_time: 0,
p50_time: (log.p50_exec_time ?? 0) + (log.p50_plan_time ?? 0),
p95_time: (log.p95_exec_time ?? 0) + (log.p95_plan_time ?? 0),
rows_read: 0,
calls,
cache_hits: 0,
cache_misses: 0,
}
})
.filter((item): item is NonNullable<typeof item> => item !== null)
.sort((a, b) => a.period_start - b.period_start)
}
function normalizeQuery(query: string): string {
return query.replace(/\s+/g, ' ').trim()
}
export function aggregateLogsByQuery(parsedLogs: ParsedLogEntry[]): QueryPerformanceRow[] {
if (!parsedLogs || parsedLogs.length === 0) return []
const queryGroups = new Map<string, ParsedLogEntry[]>()
parsedLogs.forEach((log) => {
const query = normalizeQuery(log.query || '')
if (!query) return
if (!queryGroups.has(query)) {
queryGroups.set(query, [])
}
queryGroups.get(query)!.push(log)
})
const aggregatedData: QueryPerformanceRow[] = []
let totalExecutionTime = 0
const queryStats = Array.from(queryGroups.entries()).map(([query, logs]) => {
const count = logs.length
let totalCalls = 0
let totalExecTime = 0
let totalPlanTime = 0
let p95Sum = 0
let p95Count = 0
let minTime = Infinity
let maxTime = -Infinity
const rolname = logs[0]?.user_name || ''
const applicationName = logs[0]?.application_name || ''
let firstSeen = logs[0]?.timestamp ?? ''
logs.forEach((log) => {
if (log.timestamp && (!firstSeen || log.timestamp < firstSeen)) firstSeen = log.timestamp
const logCalls = parseInt(String(log.calls ?? 0), 10)
totalCalls += logCalls
totalExecTime += parseFloat(String(log.total_exec_time ?? 0))
totalPlanTime += parseFloat(String(log.total_plan_time ?? 0))
const logP95 =
parseFloat(String(log.p95_exec_time ?? 0)) + parseFloat(String(log.p95_plan_time ?? 0))
if (logP95 > 0) {
p95Sum += logP95
p95Count++
}
minTime = Math.min(
minTime,
parseFloat(String(log.min_exec_time ?? 0)) + parseFloat(String(log.min_plan_time ?? 0))
)
maxTime = Math.max(
maxTime,
parseFloat(String(log.max_exec_time ?? 0)) + parseFloat(String(log.max_plan_time ?? 0))
)
})
const totalTime = totalExecTime + totalPlanTime
const avgMeanTime = totalCalls > 0 ? totalTime / totalCalls : 0
const avgP95Time = p95Count > 0 ? p95Sum / p95Count : 0
const finalMinTime = minTime === Infinity ? 0 : minTime
const finalMaxTime = maxTime === -Infinity ? 0 : maxTime
totalExecutionTime += totalTime
return {
query,
rolname,
applicationName,
firstSeen,
count,
avgMeanTime,
avgP95Time,
minTime: finalMinTime,
maxTime: finalMaxTime,
totalCalls,
totalTime,
}
})
queryStats.forEach((stats) => {
const propTotalTime = totalExecutionTime > 0 ? (stats.totalTime / totalExecutionTime) * 100 : 0
aggregatedData.push({
query: stats.query,
rolname: stats.rolname,
application_name: stats.applicationName,
calls: stats.totalCalls,
mean_time: stats.avgMeanTime,
p95_time: stats.avgP95Time,
min_time: stats.minTime,
max_time: stats.maxTime,
total_time: stats.totalTime,
rows_read: 0,
cache_hit_rate: 0,
prop_total_time: propTotalTime,
index_advisor_result: null,
_total_cache_hits: 0,
_total_cache_misses: 0,
_count: stats.count,
first_seen: stats.firstSeen,
})
})
return aggregatedData.sort((a, b) => b.total_time - a.total_time)
}