UnifiedLogs.utils.ts 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. import { type Table as TTable } from '@tanstack/react-table'
  2. import { cn } from 'ui'
  3. import { FacetMetadataSchema } from './UnifiedLogs.schema'
  4. import { LEVELS } from '@/components/ui/DataTable/DataTable.constants'
  5. export const logEventBus = {
  6. listeners: new Map<string, Set<(rowId: string) => void>>(),
  7. on(event: 'selectTraceTab', callback: (rowId: string) => void) {
  8. if (!this.listeners.has(event)) {
  9. this.listeners.set(event, new Set())
  10. }
  11. this.listeners.get(event)?.add(callback)
  12. return () => this.listeners.get(event)?.delete(callback)
  13. },
  14. emit(event: 'selectTraceTab', rowId: string) {
  15. this.listeners.get(event)?.forEach((callback) => callback(rowId))
  16. },
  17. }
  18. export const getFacetedUniqueValues = <TData>(facets?: Record<string, FacetMetadataSchema>) => {
  19. return (_table: TTable<TData>, columnId: string) => {
  20. return new Map(facets?.[columnId]?.rows?.map(({ value, total }) => [value, total]) || [])
  21. }
  22. }
  23. export const getFacetedMinMaxValues = <TData>(facets?: Record<string, FacetMetadataSchema>) => {
  24. return (_table: TTable<TData>, columnId: string) => {
  25. const min = facets?.[columnId]?.min
  26. const max = facets?.[columnId]?.max
  27. if (typeof min === 'number' && typeof max === 'number') return [min, max]
  28. if (typeof min === 'number') return [min, min]
  29. if (typeof max === 'number') return [max, max]
  30. return undefined
  31. }
  32. }
  33. /**
  34. * Returns a unified-logs row's timestamp in epoch milliseconds.
  35. *
  36. * The row mapper attaches a pre-parsed `date` (works for both BigQuery
  37. * microsecond timestamps and OTEL ISO strings); fall back to the raw
  38. * `timestamp` value when it's a number (older BQ-style microseconds).
  39. */
  40. export function getRowTimestampMs(
  41. row: { date?: Date | null; timestamp?: number | string | null } | null | undefined
  42. ): number | null {
  43. if (row?.date instanceof Date) return row.date.getTime()
  44. if (typeof row?.timestamp === 'number') return row.timestamp / 1000
  45. return null
  46. }
  47. export const getLevelLabel = (value: (typeof LEVELS)[number]): string => {
  48. switch (value) {
  49. case 'success':
  50. return '2xx'
  51. case 'warning':
  52. return '4xx'
  53. case 'error':
  54. return '5xx'
  55. }
  56. }
  57. // Helper function to determine level from HTTP status code
  58. export const getStatusLevel = (status?: number | string): string => {
  59. if (!status) return 'success'
  60. const statusNum = Number(status)
  61. if (statusNum >= 500) return 'error'
  62. if (statusNum >= 400) return 'warning'
  63. if (statusNum >= 300) return 'info' // 3xx redirects are informational
  64. if (statusNum >= 200) return 'success'
  65. if (statusNum >= 100) return 'info'
  66. return 'success'
  67. }
  68. export function getLevelRowClassName(value: (typeof LEVELS)[number]): string {
  69. switch (value) {
  70. case 'success':
  71. return ''
  72. case 'warning':
  73. return cn(
  74. 'bg-warning/5 hover:bg-warning/10',
  75. 'data-[state=selected]:bg-warning/20 focus-visible:bg-warning/10',
  76. 'dark:bg-warning/10 dark:hover:bg-warning/20 dark:data-[state=selected]:bg-warning/30 dark:focus-visible:bg-warning/20'
  77. )
  78. case 'error':
  79. return cn(
  80. 'bg-destructive/5 hover:bg-destructive/10',
  81. 'data-[state=selected]:bg-destructive/20 focus-visible:bg-destructive/10',
  82. 'dark:bg-error/10 dark:hover:bg-destructive/20 dark:data-[state=selected]:bg-destructive/30 dark:focus-visible:bg-destructive/20'
  83. )
  84. default:
  85. return ''
  86. }
  87. }
  88. /**
  89. * Formats service type strings for display purposes
  90. * Handles special cases like "edge function" -> "Edge Function"
  91. * and applies proper capitalization to other service types
  92. */
  93. export function formatServiceTypeForDisplay(serviceType: string): string {
  94. if (!serviceType) return ''
  95. // Handle special cases
  96. const specialCases: Record<string, string> = {
  97. 'edge function': 'Edge Function',
  98. postgrest: 'PostgREST',
  99. postgres: 'Postgres',
  100. auth: 'Auth',
  101. storage: 'Storage',
  102. }
  103. return specialCases[serviceType.toLowerCase()] || serviceType
  104. }
  105. /**
  106. * Parses an auth log event_message that may be a stringified JSON object.
  107. * Auth log entries store metadata as JSON in event_message (e.g. {"msg":"...","level":"info"}).
  108. * Extracts the human-readable msg field, falling back to error, then the raw string.
  109. * The fallback ensures self-hosted versions with different formats still render correctly.
  110. */
  111. export function parseAuthLogEventMessage(value: string | undefined): string | undefined {
  112. if (!value) return value
  113. try {
  114. const parsed = JSON.parse(value)
  115. if (parsed && typeof parsed === 'object') {
  116. const msg = parsed.msg
  117. const err = parsed.error
  118. if (typeof msg === 'string' && msg.trim()) return msg
  119. if (typeof err === 'string' && err.trim()) return err
  120. }
  121. return value
  122. } catch {
  123. return value
  124. }
  125. }