supamonitor.utils.ts 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210
  1. import { type SafeSqlFragment } from '@supabase/pg-meta'
  2. import type { QueryPerformanceRow } from '../../QueryPerformance/QueryPerformance.types'
  3. import type { Logs } from '../../Settings/Logs/Logs.types'
  4. import {
  5. SCHEMA_INTROSPECTION_REGEX,
  6. SUPAMONITOR_EXCLUDED_APP_NAMES,
  7. SUPAMONITOR_EXCLUDED_ROLES,
  8. TRANSACTION_CONTROL_REGEX,
  9. } from '../QueryInsights.constants'
  10. import type { ChartDataPoint, ParsedLogEntry } from '../QueryInsights.types'
  11. export function filterSystemLogs(
  12. logs: ParsedLogEntry[],
  13. { includeIntrospection = false }: { includeIntrospection?: boolean } = {}
  14. ): ParsedLogEntry[] {
  15. return logs.filter((log) => {
  16. if (log.user_name && (SUPAMONITOR_EXCLUDED_ROLES as readonly string[]).includes(log.user_name))
  17. return false
  18. if (
  19. log.application_name &&
  20. (SUPAMONITOR_EXCLUDED_APP_NAMES as readonly string[]).includes(log.application_name)
  21. )
  22. return false
  23. if (log.query && TRANSACTION_CONTROL_REGEX.test(log.query)) return false
  24. if (!includeIntrospection && log.query && SCHEMA_INTROSPECTION_REGEX.test(log.query))
  25. return false
  26. return true
  27. })
  28. }
  29. function asString(unknown: unknown): string | undefined {
  30. if (typeof unknown === 'string') return unknown
  31. if (unknown === null || unknown === undefined) return undefined
  32. return String(unknown)
  33. }
  34. function asNumber(unknown: unknown): number | undefined {
  35. if (typeof unknown === 'number') return unknown
  36. if (unknown === null || unknown === undefined) return undefined
  37. const parsed = Number(unknown)
  38. return Number.isNaN(parsed) ? undefined : parsed
  39. }
  40. export function parseSupamonitorLogs(logData: Logs['result']): ParsedLogEntry[] {
  41. if (!logData || logData.length === 0) return []
  42. return logData.map((log) => ({
  43. timestamp: asString(log.timestamp),
  44. application_name: asString(log.application_name),
  45. calls: asNumber(log.calls),
  46. database_name: asString(log.database_name),
  47. query: log.query,
  48. query_id: asNumber(log.query_id),
  49. total_exec_time: asNumber(log.total_exec_time),
  50. total_plan_time: asNumber(log.total_plan_time),
  51. user_name: asString(log.user_name),
  52. mean_exec_time: asNumber(log.mean_exec_time),
  53. mean_plan_time: asNumber(log.mean_plan_time),
  54. min_exec_time: asNumber(log.min_exec_time),
  55. max_exec_time: asNumber(log.max_exec_time),
  56. min_plan_time: asNumber(log.min_plan_time),
  57. max_plan_time: asNumber(log.max_plan_time),
  58. p50_exec_time: asNumber(log.p50_exec_time),
  59. p95_exec_time: asNumber(log.p95_exec_time),
  60. p50_plan_time: asNumber(log.p50_plan_time),
  61. p95_plan_time: asNumber(log.p95_plan_time),
  62. }))
  63. }
  64. export function transformLogsToChartData(parsedLogs: ParsedLogEntry[]): ChartDataPoint[] {
  65. if (!parsedLogs || parsedLogs.length === 0) return []
  66. return parsedLogs
  67. .map((log: ParsedLogEntry) => {
  68. if (!log.timestamp) return null
  69. const periodStart = new Date(log.timestamp).getTime()
  70. if (isNaN(periodStart)) return null
  71. const meanExecTime = parseFloat(String(log.mean_exec_time ?? 0))
  72. const meanPlanTime = parseFloat(String(log.mean_plan_time ?? 0))
  73. const calls = parseInt(String(log.calls ?? 0), 10)
  74. return {
  75. period_start: periodStart,
  76. timestamp: log.timestamp,
  77. query_latency: meanExecTime + meanPlanTime,
  78. mean_time: meanExecTime,
  79. min_time: (log.min_exec_time ?? 0) + (log.min_plan_time ?? 0),
  80. max_time: (log.max_exec_time ?? 0) + (log.max_plan_time ?? 0),
  81. stddev_time: 0,
  82. p50_time: (log.p50_exec_time ?? 0) + (log.p50_plan_time ?? 0),
  83. p95_time: (log.p95_exec_time ?? 0) + (log.p95_plan_time ?? 0),
  84. rows_read: 0,
  85. calls,
  86. cache_hits: 0,
  87. cache_misses: 0,
  88. }
  89. })
  90. .filter((item): item is NonNullable<typeof item> => item !== null)
  91. .sort((a, b) => a.period_start - b.period_start)
  92. }
  93. function normalizeQuery(query: string): string {
  94. return query.replace(/\s+/g, ' ').trim()
  95. }
  96. export function aggregateLogsByQuery(parsedLogs: ParsedLogEntry[]): QueryPerformanceRow[] {
  97. if (!parsedLogs || parsedLogs.length === 0) return []
  98. const queryGroups = new Map<string, ParsedLogEntry[]>()
  99. parsedLogs.forEach((log) => {
  100. const query = normalizeQuery(log.query || '')
  101. if (!query) return
  102. if (!queryGroups.has(query)) {
  103. queryGroups.set(query, [])
  104. }
  105. queryGroups.get(query)!.push(log)
  106. })
  107. const aggregatedData: QueryPerformanceRow[] = []
  108. let totalExecutionTime = 0
  109. const queryStats = Array.from(queryGroups.entries()).map(([query, logs]) => {
  110. const count = logs.length
  111. let totalCalls = 0
  112. let totalExecTime = 0
  113. let totalPlanTime = 0
  114. let p95Sum = 0
  115. let p95Count = 0
  116. let minTime = Infinity
  117. let maxTime = -Infinity
  118. const rolname = logs[0]?.user_name || ''
  119. const applicationName = logs[0]?.application_name || ''
  120. let firstSeen = logs[0]?.timestamp ?? ''
  121. logs.forEach((log) => {
  122. if (log.timestamp && (!firstSeen || log.timestamp < firstSeen)) firstSeen = log.timestamp
  123. const logCalls = parseInt(String(log.calls ?? 0), 10)
  124. totalCalls += logCalls
  125. totalExecTime += parseFloat(String(log.total_exec_time ?? 0))
  126. totalPlanTime += parseFloat(String(log.total_plan_time ?? 0))
  127. const logP95 =
  128. parseFloat(String(log.p95_exec_time ?? 0)) + parseFloat(String(log.p95_plan_time ?? 0))
  129. if (logP95 > 0) {
  130. p95Sum += logP95
  131. p95Count++
  132. }
  133. minTime = Math.min(
  134. minTime,
  135. parseFloat(String(log.min_exec_time ?? 0)) + parseFloat(String(log.min_plan_time ?? 0))
  136. )
  137. maxTime = Math.max(
  138. maxTime,
  139. parseFloat(String(log.max_exec_time ?? 0)) + parseFloat(String(log.max_plan_time ?? 0))
  140. )
  141. })
  142. const totalTime = totalExecTime + totalPlanTime
  143. const avgMeanTime = totalCalls > 0 ? totalTime / totalCalls : 0
  144. const avgP95Time = p95Count > 0 ? p95Sum / p95Count : 0
  145. const finalMinTime = minTime === Infinity ? 0 : minTime
  146. const finalMaxTime = maxTime === -Infinity ? 0 : maxTime
  147. totalExecutionTime += totalTime
  148. return {
  149. query,
  150. rolname,
  151. applicationName,
  152. firstSeen,
  153. count,
  154. avgMeanTime,
  155. avgP95Time,
  156. minTime: finalMinTime,
  157. maxTime: finalMaxTime,
  158. totalCalls,
  159. totalTime,
  160. }
  161. })
  162. queryStats.forEach((stats) => {
  163. const propTotalTime = totalExecutionTime > 0 ? (stats.totalTime / totalExecutionTime) * 100 : 0
  164. aggregatedData.push({
  165. query: stats.query as SafeSqlFragment,
  166. rolname: stats.rolname,
  167. application_name: stats.applicationName,
  168. calls: stats.totalCalls,
  169. mean_time: stats.avgMeanTime,
  170. p95_time: stats.avgP95Time,
  171. min_time: stats.minTime,
  172. max_time: stats.maxTime,
  173. total_time: stats.totalTime,
  174. rows_read: 0,
  175. cache_hit_rate: 0,
  176. prop_total_time: propTotalTime,
  177. index_advisor_result: null,
  178. _total_cache_hits: 0,
  179. _total_cache_misses: 0,
  180. _count: stats.count,
  181. first_seen: stats.firstSeen,
  182. })
  183. })
  184. return aggregatedData.sort((a, b) => b.total_time - a.total_time)
  185. }