Logs.utils.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845
  1. import { useMonaco } from '@monaco-editor/react'
  2. import { IS_PLATFORM } from 'common'
  3. import dayjs, { Dayjs } from 'dayjs'
  4. import { get } from 'lodash'
  5. import uniqBy from 'lodash/uniqBy'
  6. import { useEffect } from 'react'
  7. import logConstants from 'shared-data/log-constants'
  8. import { LogsTableName, SQL_FILTER_TEMPLATES } from './Logs.constants'
  9. import type { Filters, LogData, LogsEndpointParams, QueryType } from './Logs.types'
  10. import { convertResultsToCSV } from '@/components/interfaces/SQLEditor/UtilityPanel/Results.utils'
  11. import BackwardIterator from '@/components/ui/CodeEditor/Providers/BackwardIterator'
  12. /**
  13. * Convert a micro timestamp from number/string to iso timestamp
  14. */
  15. export const unixMicroToIsoTimestamp = (unix: string | number): string => {
  16. return dayjs.utc(Number(unix) / 1000).toISOString()
  17. }
  18. export const isUnixMicro = (unix: string | number): boolean => {
  19. const digitLength = String(unix).length === 16
  20. const isNum = !Number.isNaN(Number(unix))
  21. return isNum && digitLength
  22. }
  23. /**
  24. * Boolean check to verify that there are 3 columns:
  25. * - id
  26. * - timestamp
  27. * - event_message
  28. */
  29. export const isDefaultLogPreviewFormat = (log: LogData) =>
  30. log && log.timestamp && log.event_message && log.id
  31. /**
  32. * Recursively retrieve all nested object key paths.
  33. *
  34. * TODO: move to utils
  35. *
  36. * @param obj any object
  37. * @param parent a string representing the parent key
  38. * @returns string[] all dot paths for keys.
  39. */
  40. const getDotKeys = (obj: { [k: string]: unknown }, parent?: string): string[] => {
  41. const keys = Object.keys(obj).filter((k) => obj[k])
  42. return keys.flatMap((k) => {
  43. const currKey = parent ? `${parent}.${k}` : k
  44. if (typeof obj[k] === 'object') {
  45. return getDotKeys(obj[k] as any, currKey)
  46. } else {
  47. return [currKey]
  48. }
  49. })
  50. }
  51. /**
  52. * Root keys in the filter object are considered to be AND filters.
  53. * Nested keys under a root key are considered to be OR filters.
  54. *
  55. * For example:
  56. * ```
  57. * {my_value: 'something', nested: {id: 123, test: 123 }}
  58. * ```
  59. * This would be converted into `WHERE (my_value = 'something') and (id = 123 or test = 123)
  60. *
  61. * The template of the filter determines the actual filter statement. If no template is provided, a generic equality statement will be used.
  62. * This only applies for root keys of the filter.
  63. * For example:
  64. * ```
  65. * {'my.nested.value': 123}
  66. * ```
  67. * with no template, it will be converted into `WHERE (my.nested.value = 123)
  68. *
  69. * @returns a where statement with WHERE clause.
  70. */
  71. const genWhereStatement = (table: LogsTableName, filters: Filters) => {
  72. const keys = Object.keys(filters)
  73. const filterTemplates = SQL_FILTER_TEMPLATES[table]
  74. const _resolveTemplateToStatement = (dotKey: string): string | null => {
  75. const template = filterTemplates[dotKey]
  76. const value = get(filters, dotKey)
  77. if (value !== undefined && typeof template === 'function') {
  78. return template(value)
  79. } else if (template === undefined) {
  80. // resolve unknown filters (possibly from filter overrides)
  81. // no template, set a default
  82. if (typeof value === 'string') {
  83. return `${dotKey} = '${value}'`
  84. } else {
  85. return `${dotKey} = ${value}`
  86. }
  87. } else if (value === undefined && typeof template === 'function') {
  88. return null
  89. } else if (template && value === false) {
  90. // template present, but value is false
  91. return null
  92. } else {
  93. return template
  94. }
  95. }
  96. const statement = keys
  97. .map((rootKey) => {
  98. if (
  99. filters[rootKey] === undefined ||
  100. (typeof filters[rootKey] === 'string' && (filters[rootKey] as string).length === 0)
  101. ) {
  102. return null
  103. } else if (typeof filters[rootKey] === 'object') {
  104. // join all statements with an OR
  105. const nestedStatements = getDotKeys(filters[rootKey] as Filters, rootKey)
  106. .map(_resolveTemplateToStatement)
  107. .filter(Boolean)
  108. if (nestedStatements.length > 0) {
  109. return `(${nestedStatements.join(' or ')})`
  110. } else {
  111. return null
  112. }
  113. } else {
  114. const nestedStatement = _resolveTemplateToStatement(rootKey)
  115. if (nestedStatement === null) return null
  116. return `(${nestedStatement})`
  117. }
  118. })
  119. .filter(Boolean)
  120. // join all root statements with AND
  121. .join(' and ')
  122. if (statement) {
  123. return 'where ' + statement
  124. } else {
  125. return ''
  126. }
  127. }
  128. export const genDefaultQuery = (table: LogsTableName, filters: Filters, limit: number = 100) => {
  129. const where = genWhereStatement(table, filters)
  130. const joins = genCrossJoinUnnests(table)
  131. const orderBy = 'order by timestamp desc'
  132. switch (table) {
  133. case 'edge_logs':
  134. if (!IS_PLATFORM) {
  135. return `
  136. -- local dev edge_logs query
  137. select id, edge_logs.timestamp, event_message, request.method, request.path, request.search, response.status_code
  138. from edge_logs
  139. ${joins}
  140. ${where}
  141. ${orderBy}
  142. limit ${limit};
  143. `
  144. }
  145. return `select id, identifier, timestamp, event_message, request.method, request.path, request.search, response.status_code
  146. from ${table}
  147. ${joins}
  148. ${where}
  149. ${orderBy}
  150. limit ${limit}
  151. `
  152. case 'postgres_logs':
  153. if (!IS_PLATFORM) {
  154. return `
  155. select postgres_logs.timestamp, id, event_message, parsed.error_severity, parsed.detail, parsed.hint
  156. from postgres_logs
  157. ${joins}
  158. ${where}
  159. ${orderBy}
  160. limit ${limit}
  161. `
  162. }
  163. return `select identifier, postgres_logs.timestamp, id, event_message, parsed.error_severity, parsed.detail, parsed.hint from ${table}
  164. ${joins}
  165. ${where}
  166. ${orderBy}
  167. limit ${limit}
  168. `
  169. case 'function_logs':
  170. return `select id, ${table}.timestamp, event_message, metadata.event_type, metadata.function_id, metadata.execution_id, metadata.level from ${table}
  171. ${joins}
  172. ${where}
  173. ${orderBy}
  174. limit ${limit}
  175. `
  176. case 'auth_logs':
  177. return `select id, ${table}.timestamp, event_message, metadata.level, metadata.status, metadata.path, metadata.msg as msg, metadata.error from ${table}
  178. ${joins}
  179. ${where}
  180. ${orderBy}
  181. limit ${limit}
  182. `
  183. case 'function_edge_logs':
  184. if (!IS_PLATFORM) {
  185. return `
  186. select id, function_edge_logs.timestamp, event_message
  187. from function_edge_logs
  188. ${orderBy}
  189. limit ${limit}
  190. `
  191. }
  192. return `select id, ${table}.timestamp, event_message, response.status_code, request.method, request.pathname, m.function_id, m.execution_id, m.execution_time_ms, m.deployment_id, m.version from ${table}
  193. ${joins}
  194. ${where}
  195. ${orderBy}
  196. limit ${limit}
  197. `
  198. case 'supavisor_logs':
  199. return `select id, ${table}.timestamp, event_message from ${table} ${joins} ${where} ${orderBy} limit ${limit}`
  200. case 'pg_upgrade_logs':
  201. return `select id, ${table}.timestamp, event_message from ${table} ${joins} ${where} ${orderBy} limit 100`
  202. default:
  203. return `select id, ${table}.timestamp, event_message from ${table}
  204. ${where}
  205. ${orderBy}
  206. limit ${limit}
  207. `
  208. case 'pg_cron_logs':
  209. const pgCronWhere = where ? `${basePgCronWhere} AND ${where.substring(6)}` : basePgCronWhere
  210. return `select id, postgres_logs.timestamp, event_message, parsed.error_severity, parsed.query
  211. from postgres_logs
  212. ${joins}
  213. ${pgCronWhere}
  214. ${orderBy}
  215. limit ${limit}
  216. `
  217. }
  218. }
  219. /**
  220. * Hardcoded cross join unnests and aliases for each table.
  221. * Should be used together with the getWhereStatements to allow for filtering on aliases
  222. */
  223. const genCrossJoinUnnests = (table: LogsTableName) => {
  224. switch (table) {
  225. case 'edge_logs':
  226. return `cross join unnest(metadata) as m
  227. cross join unnest(m.request) as request
  228. cross join unnest(m.response) as response`
  229. case 'pg_cron_logs':
  230. case 'postgres_logs':
  231. return `cross join unnest(metadata) as m
  232. cross join unnest(m.parsed) as parsed`
  233. case 'function_logs':
  234. return `cross join unnest(metadata) as metadata`
  235. case 'auth_logs':
  236. return `cross join unnest(metadata) as metadata`
  237. case 'function_edge_logs':
  238. return `cross join unnest(metadata) as m
  239. cross join unnest(m.response) as response
  240. cross join unnest(m.request) as request`
  241. case 'supavisor_logs':
  242. return `cross join unnest(metadata) as m`
  243. default:
  244. return ''
  245. }
  246. }
  247. /**
  248. * SQL query to retrieve only one log
  249. */
  250. export const genSingleLogQuery = (table: LogsTableName, id: string) =>
  251. `select id, timestamp, event_message, metadata from ${table} where id = '${id}' limit 1`
  252. /**
  253. * Determine if we should show the user an upgrade prompt while browsing logs
  254. */
  255. export const maybeShowUpgradePromptIfNotEntitled = (
  256. from: string | null | undefined,
  257. entitledToDays: number | undefined
  258. ) => {
  259. if (!entitledToDays) return false
  260. const day = Math.abs(dayjs().diff(dayjs(from), 'day'))
  261. return day > entitledToDays
  262. }
  263. export const genCountQuery = (table: LogsTableName, filters: Filters): string => {
  264. let where = genWhereStatement(table, filters)
  265. // pg_cron logs are a subset of postgres logs
  266. // to calculate the chart, we need to query postgres logs
  267. if (table === LogsTableName.PG_CRON) {
  268. table = LogsTableName.POSTGRES
  269. where = basePgCronWhere
  270. }
  271. const joins = genCrossJoinUnnests(table)
  272. return `SELECT count(*) as count FROM ${table} ${joins} ${where}`
  273. }
  274. /** calculates how much the chart start datetime should be offset given the current datetime filter params */
  275. const calcChartStart = (
  276. params: Partial<LogsEndpointParams>
  277. ): [Dayjs, 'minute' | 'hour' | 'day'] => {
  278. const ite = params.iso_timestamp_end ? dayjs(params.iso_timestamp_end) : dayjs()
  279. // todo @TzeYiing needs typing
  280. const its: any = params.iso_timestamp_start ? dayjs(params.iso_timestamp_start) : dayjs()
  281. let trunc: 'minute' | 'hour' | 'day' = 'minute'
  282. let extendValue = 60 * 6
  283. const minuteDiff = ite.diff(its, 'minute')
  284. const hourDiff = ite.diff(its, 'hour')
  285. if (minuteDiff > 60 * 12) {
  286. trunc = 'hour'
  287. extendValue = 24 * 5
  288. } else if (hourDiff > 24 * 3) {
  289. trunc = 'day'
  290. extendValue = 7
  291. }
  292. return [its.add(-extendValue, trunc), trunc]
  293. }
  294. // TODO(qiao): workaround for self-hosted cron logs error until logflare is fixed
  295. const basePgCronWhere = IS_PLATFORM
  296. ? `where ( parsed.application_name = 'pg_cron' or regexp_contains(event_message, 'cron job') )`
  297. : `where ( parsed.application_name = 'pg_cron' or event_message::text LIKE '%cron job%' )`
  298. /**
  299. *
  300. * generates log event chart query
  301. */
  302. export const genChartQuery = (
  303. table: LogsTableName,
  304. params: LogsEndpointParams,
  305. filters: Filters
  306. ) => {
  307. const [startOffset, trunc] = calcChartStart(params)
  308. let where = genWhereStatement(table, filters)
  309. const errorCondition = getErrorCondition(table)
  310. const warningCondition = getWarningCondition(table)
  311. // pg_cron logs are a subset of postgres logs
  312. // to calculate the chart, we need to query postgres logs
  313. if (table === LogsTableName.PG_CRON) {
  314. table = LogsTableName.POSTGRES
  315. where = basePgCronWhere
  316. }
  317. let joins = genCrossJoinUnnests(table)
  318. const q = `
  319. SELECT
  320. -- log-event-chart
  321. timestamp_trunc(t.timestamp, ${trunc}) as timestamp,
  322. count(CASE WHEN NOT (${errorCondition} OR ${warningCondition}) THEN 1 END) as ok_count,
  323. count(CASE WHEN ${errorCondition} THEN 1 END) as error_count,
  324. count(CASE WHEN ${warningCondition} THEN 1 END) as warning_count,
  325. FROM
  326. ${table} t
  327. ${joins}
  328. ${
  329. where
  330. ? where + ` and t.timestamp > '${startOffset.toISOString()}'`
  331. : `where t.timestamp > '${startOffset.toISOString()}'`
  332. }
  333. GROUP BY
  334. timestamp
  335. ORDER BY
  336. timestamp ASC
  337. `
  338. return q
  339. }
  340. type TsPair = [string | '', string | '']
  341. export const ensureNoTimestampConflict = (
  342. [initialStart, initialEnd]: TsPair,
  343. [nextStart, nextEnd]: TsPair
  344. ): TsPair => {
  345. if (initialStart && initialEnd && nextEnd && !nextStart) {
  346. const resolvedDiff = dayjs(nextEnd).diff(dayjs(initialStart))
  347. let start = dayjs(initialStart)
  348. if (resolvedDiff <= 0) {
  349. // start ts is definitely before end ts
  350. const currDiff = Math.abs(dayjs(initialEnd).diff(start, 'minute'))
  351. // shift start ts backwards by the current ts difference
  352. start = dayjs(nextEnd).subtract(currDiff, 'minute')
  353. }
  354. return [start.toISOString(), nextEnd]
  355. } else if (!nextEnd && nextStart) {
  356. return [nextStart, initialEnd]
  357. } else {
  358. return [nextStart, nextEnd]
  359. }
  360. }
  361. /**
  362. * Adds SQL code hints to logs explorer code editor
  363. */
  364. export const useEditorHints = () => {
  365. const monaco = useMonaco()
  366. useEffect(() => {
  367. if (monaco) {
  368. const competionProvider = {
  369. triggerCharacters: ['`', ' ', '.'],
  370. provideCompletionItems: function (model: any, position: any, context: any) {
  371. let iterator = new BackwardIterator(model, position.column - 2, position.lineNumber - 1)
  372. if (iterator.isNextDQuote()) return { suggestions: [] }
  373. let suggestions: { label: string; kind: any; insertText: string }[] = []
  374. let schemasInUse = logConstants.schemas.filter((schema) =>
  375. iterator._text.includes(schema.reference)
  376. )
  377. if (schemasInUse.length === 0) {
  378. schemasInUse = logConstants.schemas
  379. }
  380. if (iterator.isNextPeriod()) {
  381. // should be nested key reference, suggest all tail endings of available fields
  382. const fields = schemasInUse.flatMap((schema) => schema.fields)
  383. const trailingKeys = fields.flatMap((field) => {
  384. const [_head, ...rest] = field.path.split('.')
  385. return rest
  386. })
  387. const trailingToAdd = trailingKeys.map((key) => ({
  388. label: key,
  389. kind: monaco.languages.CompletionItemKind.Property,
  390. insertText: key,
  391. }))
  392. suggestions = suggestions.concat(trailingToAdd)
  393. }
  394. if (context.triggerCharacter === '`' || context.triggerCharacter === ' ') {
  395. // should be reference or start of key
  396. const referencesToAdd = logConstants.schemas.map((schema) => ({
  397. label: schema.reference,
  398. kind: monaco.languages.CompletionItemKind.Class,
  399. insertText: schema.reference,
  400. }))
  401. const fields = schemasInUse.flatMap((schema) => schema.fields)
  402. const leadingKeys = fields.flatMap((field) => {
  403. const splitPath = field.path.split('.')
  404. return splitPath.slice(0, -1)
  405. })
  406. const leadingToAdd = leadingKeys.map((key) => ({
  407. label: key,
  408. kind: monaco.languages.CompletionItemKind.Property,
  409. insertText: key,
  410. }))
  411. suggestions = suggestions.concat(leadingToAdd)
  412. suggestions = suggestions.concat(referencesToAdd)
  413. }
  414. return {
  415. suggestions: uniqBy(suggestions, 'label'),
  416. }
  417. },
  418. } as any
  419. // register completion item provider for pgsql
  420. const completeProvider = monaco.languages.registerCompletionItemProvider(
  421. 'pgsql',
  422. competionProvider
  423. )
  424. return () => {
  425. completeProvider.dispose()
  426. }
  427. }
  428. }, [monaco])
  429. }
  430. /**
  431. * Assumes that all timestamps are in ISO-8601 UTC timezone.
  432. *
  433. * min/max are the datetime strings that extend beyond the given timeseries data.
  434. */
  435. export const fillTimeseries = (
  436. timeseriesData: any[],
  437. timestampKey: string,
  438. valueKey: string | string[],
  439. defaultValue: number,
  440. min?: string,
  441. max?: string,
  442. minPointsToFill: number = 20,
  443. interval?: string
  444. ) => {
  445. if (timeseriesData.length === 0 && !(min && max)) {
  446. return []
  447. }
  448. // If we have more points than minPointsToFill, just normalize timestamps and return
  449. if (timeseriesData.length > minPointsToFill) {
  450. return timeseriesData.map((datum) => {
  451. const timestamp = datum[timestampKey]
  452. const iso = isUnixMicro(timestamp)
  453. ? unixMicroToIsoTimestamp(timestamp)
  454. : dayjs.utc(timestamp).toISOString()
  455. datum[timestampKey] = iso
  456. return datum
  457. })
  458. }
  459. if (timeseriesData.length <= 1 && !(min && max)) return timeseriesData
  460. const dates: unknown[] = timeseriesData.map((datum) => dayjs.utc(datum[timestampKey]))
  461. const maxDate = max ? dayjs.utc(max) : dayjs.utc(Math.max.apply(null, dates as number[]))
  462. const minDate = min ? dayjs.utc(min) : dayjs.utc(Math.min.apply(null, dates as number[]))
  463. // When no data exists but min/max are provided, we need to determine truncation from the time range
  464. const truncationSamples = timeseriesData.length > 0 ? dates : [minDate, maxDate]
  465. let truncation: 'second' | 'minute' | 'hour' | 'day'
  466. let step = 1
  467. if (interval) {
  468. const match = interval.match(/^(\d+)(m|h|d|s)$/)
  469. if (match) {
  470. step = parseInt(match[1], 10)
  471. const unitChar = match[2] as 'm' | 'h' | 'd' | 's'
  472. const unitMap = { s: 'second', m: 'minute', h: 'hour', d: 'day' } as const
  473. truncation = unitMap[unitChar]
  474. } else {
  475. // Fallback for invalid format
  476. truncation = getTimestampTruncation(truncationSamples as Dayjs[])
  477. }
  478. } else {
  479. truncation = getTimestampTruncation(truncationSamples as Dayjs[])
  480. }
  481. // If no data exists and no interval specified, default to minute precision
  482. if (timeseriesData.length === 0 && !interval) {
  483. truncation = 'minute'
  484. }
  485. const newData = timeseriesData.map((datum) => {
  486. const timestamp = datum[timestampKey]
  487. const iso = isUnixMicro(timestamp)
  488. ? unixMicroToIsoTimestamp(timestamp)
  489. : dayjs.utc(timestamp).toISOString()
  490. if (Array.isArray(valueKey) && valueKey.length === 0) {
  491. return { [timestampKey]: iso }
  492. }
  493. datum[timestampKey] = iso
  494. return datum
  495. })
  496. let currentDate = minDate
  497. while (currentDate.isBefore(maxDate) || currentDate.isSame(maxDate)) {
  498. const found = dates.find((d) => {
  499. const d_date = d as Dayjs
  500. return (
  501. d_date.year() === currentDate.year() &&
  502. d_date.month() === currentDate.month() &&
  503. d_date.date() === currentDate.date() &&
  504. d_date.hour() === currentDate.hour() &&
  505. d_date.minute() === currentDate.minute() &&
  506. d_date.second() === currentDate.second()
  507. )
  508. })
  509. if (!found) {
  510. const keys = typeof valueKey === 'string' ? [valueKey] : valueKey
  511. const toMerge = keys.reduce(
  512. (acc, key) => ({
  513. ...acc,
  514. [key]: defaultValue,
  515. }),
  516. {}
  517. )
  518. newData.push({
  519. [timestampKey]: currentDate.toISOString(),
  520. ...toMerge,
  521. })
  522. }
  523. currentDate = currentDate.add(step, truncation)
  524. }
  525. return newData
  526. }
  527. const getTimestampTruncation = (samples: Dayjs[]): 'second' | 'minute' | 'hour' | 'day' => {
  528. const truncationCounts = samples.reduce(
  529. (acc, sample) => {
  530. const truncation = _getTruncation(sample)
  531. acc[truncation] += 1
  532. return acc
  533. },
  534. {
  535. second: 0,
  536. minute: 0,
  537. hour: 0,
  538. day: 0,
  539. }
  540. )
  541. const mostLikelyTruncation = (
  542. Object.keys(truncationCounts) as (keyof typeof truncationCounts)[]
  543. ).reduce((a, b) => (truncationCounts[a] > truncationCounts[b] ? a : b))
  544. return mostLikelyTruncation
  545. }
  546. const _getTruncation = (date: Dayjs) => {
  547. const values = ['second', 'minute', 'hour'].map((key) => date.get(key as dayjs.UnitType))
  548. const zeroCount = values.reduce((acc, value) => {
  549. if (value === 0) {
  550. acc += 1
  551. }
  552. return acc
  553. }, 0)
  554. const truncation = {
  555. 0: 'second' as const,
  556. 1: 'minute' as const,
  557. 2: 'hour' as const,
  558. 3: 'day' as const,
  559. }[zeroCount]!
  560. return truncation
  561. }
  562. export function checkForWithClause(query: string) {
  563. const queryWithoutComments = query.replace(/--.*$/gm, '').replace(/\/\*[\s\S]*?\*\//gm, '')
  564. const withClauseRegex = /\b(WITH)\b(?=(?:[^']*'[^']*')*[^']*$)/i
  565. return withClauseRegex.test(queryWithoutComments)
  566. }
  567. export function checkForILIKEClause(query: string) {
  568. const queryWithoutComments = query.replace(/--.*$/gm, '').replace(/\/\*[\s\S]*?\*\//gm, '')
  569. const ilikeClauseRegex = /\b(ILIKE)\b(?=(?:[^']*'[^']*')*[^']*$)/i
  570. return ilikeClauseRegex.test(queryWithoutComments)
  571. }
  572. export function checkForWildcard(query: string) {
  573. const queryWithoutComments = query.replace(/--.*$/gm, '').replace(/\/\*[\s\S]*?\*\//gm, '')
  574. const queryWithoutCount = queryWithoutComments.replace(/count\(\*\)/gi, '')
  575. const wildcardRegex = /\*/
  576. return wildcardRegex.test(queryWithoutCount)
  577. }
  578. function getErrorCondition(table: LogsTableName): string {
  579. switch (table) {
  580. case 'edge_logs':
  581. return 'response.status_code >= 500'
  582. case 'postgres_logs':
  583. return "parsed.error_severity IN ('ERROR', 'FATAL', 'PANIC')"
  584. case 'auth_logs':
  585. return "metadata.level = 'error' OR SAFE_CAST(metadata.status AS INT64) >= 400"
  586. case 'function_edge_logs':
  587. return 'response.status_code >= 500'
  588. case 'function_logs':
  589. return "metadata.level IN ('error', 'fatal')"
  590. case 'pg_cron_logs':
  591. return "parsed.error_severity IN ('ERROR', 'FATAL', 'PANIC')"
  592. default:
  593. return 'false'
  594. }
  595. }
  596. function getWarningCondition(table: LogsTableName): string {
  597. switch (table) {
  598. case 'edge_logs':
  599. return 'response.status_code >= 400 AND response.status_code < 500'
  600. case 'postgres_logs':
  601. return "parsed.error_severity IN ('WARNING')"
  602. case 'auth_logs':
  603. return "metadata.level = 'warning'"
  604. case 'function_edge_logs':
  605. return 'response.status_code >= 400 AND response.status_code < 500'
  606. case 'function_logs':
  607. return "metadata.level IN ('warning')"
  608. default:
  609. return 'false'
  610. }
  611. }
  612. export function jwtAPIKey(metadata: any) {
  613. const apikeyHeader = metadata?.[0]?.request?.[0]?.sb?.[0]?.jwt?.[0]?.apikey?.[0]
  614. if (!apikeyHeader) {
  615. return undefined
  616. }
  617. if (apikeyHeader.invalid) {
  618. return '<invalid>'
  619. }
  620. const payload = apikeyHeader?.payload?.[0]
  621. if (!payload) {
  622. return '<unrecognized>'
  623. }
  624. if (
  625. payload.algorithm === 'HS256' &&
  626. payload.issuer === 'briven' &&
  627. ['anon', 'service_role'].includes(payload.role) &&
  628. !payload.subject
  629. ) {
  630. return payload.role
  631. }
  632. return '<unrecognized>'
  633. }
  634. export function apiKey(metadata: any) {
  635. const apikeyHeader = metadata?.[0]?.request?.[0]?.sb?.[0]?.apikey?.[0]?.apikey?.[0]
  636. if (!apikeyHeader) {
  637. return undefined
  638. }
  639. if (apikeyHeader.error) {
  640. return `${apikeyHeader.prefix}... <invalid: ${apikeyHeader.error}>`
  641. }
  642. return `${apikeyHeader.prefix}...`
  643. }
  644. export function role(metadata: any) {
  645. const authorizationHeader = metadata?.[0]?.request?.[0]?.sb?.[0]?.jwt?.[0]?.authorization?.[0]
  646. if (!authorizationHeader) {
  647. return undefined
  648. }
  649. if (authorizationHeader.invalid) {
  650. return undefined
  651. }
  652. const payload = authorizationHeader?.payload?.[0]
  653. if (!payload || !payload.role) {
  654. return undefined
  655. }
  656. return payload.role
  657. }
  658. export function formatLogsAsJson(rows: LogData[]): string {
  659. return JSON.stringify(rows, null, 2)
  660. }
  661. export function formatLogsAsCsv(rows: LogData[]): string {
  662. return convertResultsToCSV(rows as unknown as Record<string, unknown>[]) ?? ''
  663. }
  664. export function formatLogsAsMarkdown(rows: LogData[]): string {
  665. return rows
  666. .map((row, i) => {
  667. const lines: string[] = [`## Log ${i + 1}`]
  668. if (row.timestamp) {
  669. const numTs = Number(row.timestamp)
  670. let tsString: string
  671. if (isFinite(numTs)) {
  672. tsString = new Date(numTs / 1000).toISOString()
  673. } else if (typeof row.timestamp === 'string') {
  674. const d = new Date(row.timestamp)
  675. tsString = isNaN(d.getTime()) ? row.timestamp : d.toISOString()
  676. } else {
  677. tsString = String(row.timestamp)
  678. }
  679. lines.push(`**Timestamp:** ${tsString}`)
  680. }
  681. if (row.event_message) {
  682. lines.push(`**Message:** ${row.event_message}`)
  683. }
  684. const { id: _id, timestamp: _ts, event_message: _msg, ...rest } = row as any
  685. if (Object.keys(rest).length > 0) {
  686. lines.push('', '**Details:**', '```json', JSON.stringify(rest, null, 2), '```')
  687. }
  688. return lines.join('\n')
  689. })
  690. .join('\n\n---\n\n')
  691. }
  692. const QUERY_TYPE_LABELS: Record<QueryType, string> = {
  693. api: 'API Gateway (Edge Network)',
  694. database: 'Postgres Database',
  695. functions: 'Edge Functions',
  696. fn_edge: 'Edge Functions (edge runtime)',
  697. auth: 'Auth',
  698. realtime: 'Realtime',
  699. storage: 'Storage',
  700. supavisor: 'Supavisor (connection pooling)',
  701. postgrest: 'PostgREST',
  702. pg_upgrade: 'Postgres upgrade',
  703. pg_cron: 'pg_cron',
  704. pgbouncer: 'PgBouncer',
  705. etl: 'ETL',
  706. }
  707. const LOG_TABLE_TO_SERVICE_LABEL: Record<LogsTableName, string> = {
  708. edge_logs: 'API Gateway (Edge Network)',
  709. postgres_logs: 'Postgres Database',
  710. function_logs: 'Edge Functions',
  711. function_edge_logs: 'Edge Functions (edge runtime)',
  712. auth_logs: 'Auth',
  713. auth_audit_logs: 'Auth (audit)',
  714. realtime_logs: 'Realtime',
  715. storage_logs: 'Storage',
  716. postgrest_logs: 'PostgREST',
  717. supavisor_logs: 'Supavisor (connection pooling)',
  718. pgbouncer_logs: 'PgBouncer',
  719. pg_upgrade_logs: 'Postgres upgrade',
  720. pg_cron_logs: 'pg_cron',
  721. etl_replication_logs: 'ETL',
  722. }
  723. const isLogsTableName = (value: string): value is LogsTableName =>
  724. value in LOG_TABLE_TO_SERVICE_LABEL
  725. const isQueryType = (value: string): value is QueryType => value in QUERY_TYPE_LABELS
  726. export function extractEdgeFunctionName(pathname: unknown): string {
  727. if (typeof pathname !== 'string' || !pathname) return ''
  728. const parts = pathname.split('/').filter(Boolean)
  729. return parts[parts.length - 1] ?? ''
  730. }
  731. function extractServiceLabelFromSql(sql: string): string | null {
  732. const match = sql.match(/\bfrom\s+(\w+)/i)
  733. const tableName = match?.[1]
  734. return tableName && isLogsTableName(tableName) ? LOG_TABLE_TO_SERVICE_LABEL[tableName] : null
  735. }
  736. export function buildLogsPrompt(rows: LogData[], queryType?: string, sqlQuery?: string): string {
  737. const serviceLabel =
  738. (queryType && isQueryType(queryType) ? QUERY_TYPE_LABELS[queryType] : null) ??
  739. (sqlQuery ? extractServiceLabelFromSql(sqlQuery) : null)
  740. const serviceContext = serviceLabel ? ` from the **${serviceLabel}** service` : ''
  741. const sqlContext = sqlQuery ? `\n\n**Query used:**\n\`\`\`sql\n${sqlQuery.trim()}\n\`\`\`` : ''
  742. const header = `I have ${rows.length} Briven log entr${rows.length === 1 ? 'y' : 'ies'}${serviceContext} I'd like help debugging:\n\n`
  743. const body = formatLogsAsMarkdown(rows)
  744. return (
  745. header +
  746. body +
  747. sqlContext +
  748. '\n\nWhat do these logs indicate? What steps can I take to resolve it? Keep your answer very concise and actionable. Max 2 or 3 bullet points.'
  749. )
  750. }