mirror of
https://github.com/supabase/supabase.git
synced 2026-10-15 22:29:11 +03:00
* Update QueryOptions.ts * Create UnifiedLogs_InspectionPanel_Architecture.md * init new queries * Create UnifiedLogs_ServiceFlow_Implementation_Plan.md * Add service flow blocks to UnifiedLogs panel Introduces ServiceFlowBlocks components to visually represent the service flow in UnifiedLogs, including Origin, Network, PostgREST, Postgres, and Response steps. Adds utility formatters and updates ServiceFlowPanel to render the new blocks with enriched data and loading/error states. * Enable filterable fields in ServiceFlow blocks Refactored ServiceFlow blocks to support filterable fields by passing filterFields and table props, updating BlockField to render filterable values as clickable actions, and aligning field configs with filterable IDs. Updated ServiceFlowPanel to use new block names and pass required props. This enhances interactivity and consistency in the UnifiedLogs service flow UI. * Update ServiceFlowBlocks.tsx * Enhance Service Flow with enriched fields and filtering Expanded the Service Flow SQL query to extract 35+ flattened, meaningful fields including request, response, client location, network, headers, and JWT data. Updated the implementation plan to reflect new block architecture, filtering integration, and future enhancements. Added debug logging for enriched and raw log data in ServiceFlowPanel. * Start adding inspection panel * Moar logs * Add Postgres service flow support to Unified Logs Introduces Postgres as a first-class service flow type in Unified Logs, including SQL query, type definitions, UI blocks, and filtering. Updates ServiceFlow queries, types, and UI components to display enriched Postgres log details and event messages. Also refines filter fields, log type ordering, and minor UI adjustments for clarity and consistency. * Lots of tidying up, moving blocks into their own files. * Remove UnifiedLogs architecture and service flow docs Deleted documentation files related to UnifiedLogs inspection panel architecture, service flow field mapping, implementation plan, and integration example. This cleans up outdated or redundant design and implementation reference materials. * Refactor service flow blocks to use unified config system Replaces individual service block components with a generic, config-driven system. Introduces a shared Block component and centralizes block configuration in blockConfigs.ts, reducing duplication and improving maintainability. Updates imports and removes now-unnecessary block files. Also refactors ServiceFlowPanel to use the new block structure and simplifies layer icon logic. * Refactor ServiceFlow blocks and utilities structure Consolidates edge service flow SQL queries to reduce duplication, removes ServiceFlowBlocks.tsx in favor of more modular block/component structure, and moves storage-specific utilities to a new utils/storageUtils.ts file. Updates imports in config files and ServiceFlowPanel to reflect the new structure. Type definitions are clarified and extended in types.ts. * Update ServiceFlow.sql.ts * Refactor ServiceFlow types for stronger type safety Replaces generic 'any' types with specific types such as ColumnSchema, QuerySearchParamsType, and SearchParamsType in ServiceFlow components and ServiceFlowPanel. This improves type safety and code clarity, and removes unused metadata and workaround variables from ServiceFlowPanel. * Remove debug flags and console logs from ServiceFlow components Eliminated unused DEBUG_SERVICE_FLOW flags and related console logging from ServiceFlow.sql.ts and ServiceFlowPanel.tsx to clean up the codebase and reduce unnecessary output. * Remove unused log query and filtering functions Deleted unused functions related to log level filtering, base condition building, and the edge/supavisor logs queries. This cleanup simplifies the UnifiedLogs.queries.ts file by removing legacy or currently unused code. * revert * Remove ServiceFlow panel and types, update DataTableToolbar Deleted the ServiceFlowPanel component and its associated types from the UnifiedLogs interface. Updated DataTableToolbar to toggle between PanelLeftClose and PanelLeftOpen icons based on the open state. * Remove unused totalRows prop from DataTable Eliminates the totalRows prop from UnifiedLogs and its context interface, as it is no longer used. This helps clean up the component API and related type definitions. * Refactor log field configs and unify event message component Replaces specialized PostgresEventMessage with a generic EventMessage component and updates references accordingly. Consolidates all log field configuration files into serviceFlowFields.ts, removing separate config files for each service. Cleans up unused types and utility imports, and deletes redundant formatters. * Enhance UnifiedLogs filters and facets handling Added PATCH to allowed HTTP methods and improved status code rendering in filters. Changed Host, Pathname, and Auth User filters to use dynamic checkbox options populated from facets. Updated SQL queries to provide global facets for hardcoded fields and filtered facets for dynamic fields, and fixed field naming for path/pathname consistency. Improved UI to show an empty state when no filter options are available. * Fix method options mapping and add debug logs Corrects the mapping variable name in UnifiedLogs.fields.tsx from 'region' to 'method' for clarity. Adds debug console logs in unified-logs-count-query.ts to inspect count query responses and help diagnose missing data. * Show empty state for Postgres block with no data Added logic to display an empty state in the Postgres block when no Postgres data is available in non-Postgres logs. Also updated ServiceFlowPanel to render both PostgREST and Postgres blocks, ensuring the empty state is shown when appropriate. * Remove Log Details tab from ServiceFlowPanel The Log Details tab and its associated content have been removed from the ServiceFlowPanel component. This simplifies the UI by focusing on the Overview and Raw JSON tabs. * Enhance UnifiedLogs filters with status code labels and facet logic Added human-readable labels for HTTP and Postgres status codes and improved the UnifiedLogs filter UI to display these labels. Refactored the logs count query to avoid self-filtering for facet counts, ensuring accurate facet behavior. Improved filter option rendering for better usability and consistency, and updated input styling for filter search. * Update UnifiedLogs.queries.ts * Update UnifiedLogs.queries.ts * Update apps/studio/components/interfaces/UnifiedLogs/ServiceFlow/components/shared/BlockField.tsx * Refactor UnifiedLogs layout and improve service type formatting Refactored UnifiedLogs.tsx to use a more flexible ResizablePanelGroup layout, replaced inline function logs panel with a new LogsListPanel component, and removed unused imports and code. Updated ServiceFlow.sql.ts to simplify JWT payload extraction and added a dedicated query for edge function service flow. Added a utility to format service type strings for display in UnifiedLogs.utils.ts. * Refactor logs UI and improve service flow handling Renamed FunctionLogsTab to LogsList and added a new LogsListPanel for displaying function logs in a side panel. Improved ServiceFlowPanel with better panel sizing, conditional query enabling, and UI adjustments. Enhanced EventMessage with badge styling and service type formatting. Updated CollapsibleSection for cleaner layout. Enabled debug mode in unified-log-inspection-query. Updated FilterSideBar to use ResizablePanel for improved layout flexibility. * Refactor UnifiedLogs UI and ServiceFlow query fields Removes unused skeletonClassName from BlockFieldConfig and related UI, simplifies ServiceFlow SQL query by returning null for unused JWT fields, and updates UnifiedLogs and LogsListPanel for improved layout and collapsible logs panel. Also adjusts table and log list row heights for better visual consistency. * Update ServiceFlowHeader.tsx * Update ServiceFlowHeader.tsx * Refactor ServiceFlowPanel and header tooltip logic Replaces custom tooltip implementation in ServiceFlowHeader with ButtonTooltip for navigation buttons. Simplifies serviceFlowType logic in ServiceFlowPanel by removing the getServiceFlowType helper and using direct mapping, reducing code complexity. * Update UnifiedLogs.constants.tsx * Remove commented code and add doc comment in UnifiedLogs Removed an unused commented-out span in UnifiedLogs.fields.tsx and added a documentation comment for the Supabase storage logs query fragment in UnifiedLogs.queries.ts. * Update UnifiedLogs.tsx * Remove 'live' level from DataTable components Eliminated the 'live' level from LEVELS constant and related color utility logic. Also removed commented code and references to 'live' in DataTableFilterControls and LiveRow, streamlining the DataTable component's status handling. * Update UnifiedLogs.constants.tsx * cleanup * remove console logs * Update status filter parsing to use string arrays Changed the status filter in UnifiedLogs to parse and validate as arrays of strings instead of numbers. This affects both the search params parser and the column filter schema, aligning the status filter handling with other string-based filters. --------- Co-authored-by: Alaister Young <a@alaisteryoung.com>
643 lines
21 KiB
TypeScript
643 lines
21 KiB
TypeScript
import dayjs from 'dayjs'
|
|
|
|
import { QuerySearchParamsType, SearchParamsType } from './UnifiedLogs.types'
|
|
|
|
// Pagination and control parameters
|
|
const PAGINATION_PARAMS = ['sort', 'start', 'size', 'uuid', 'cursor', 'direction', 'live'] as const
|
|
|
|
// Special filter parameters that need custom handling
|
|
const SPECIAL_FILTER_PARAMS = ['date'] as const
|
|
|
|
// Combined list of all parameters to exclude from standard filtering
|
|
const EXCLUDED_QUERY_PARAMS = [...PAGINATION_PARAMS, ...SPECIAL_FILTER_PARAMS] as const
|
|
const BASE_CONDITIONS_EXCLUDED_PARAMS = [...PAGINATION_PARAMS, 'date', 'level'] as const
|
|
|
|
/**
|
|
* Builds query conditions from search parameters and returns WHERE clause
|
|
* @param search SearchParamsType object containing query parameters
|
|
* @returns Object with whereConditions array and formatted WHERE clause
|
|
*/
|
|
const buildQueryConditions = (search: QuerySearchParamsType) => {
|
|
const whereConditions: string[] = []
|
|
|
|
// Process all search parameters for filtering
|
|
Object.entries(search).forEach(([key, value]) => {
|
|
// Skip pagination/control parameters
|
|
if (EXCLUDED_QUERY_PARAMS.includes(key as any)) {
|
|
return
|
|
}
|
|
|
|
// Handle array filters (IN clause)
|
|
if (Array.isArray(value) && value.length > 0) {
|
|
whereConditions.push(`${key} IN (${value.map((v) => `'${v}'`).join(',')})`)
|
|
return
|
|
}
|
|
|
|
// Handle scalar values
|
|
if (value !== null && value !== undefined) {
|
|
if (['host', 'pathname'].includes(key)) {
|
|
whereConditions.push(`${key} LIKE '%${value}%'`)
|
|
} else {
|
|
whereConditions.push(`${key} = '${value}'`)
|
|
}
|
|
}
|
|
})
|
|
|
|
// Create final WHERE clause
|
|
const finalWhere = whereConditions.length > 0 ? `WHERE ${whereConditions.join(' AND ')}` : ''
|
|
|
|
return { whereConditions, finalWhere }
|
|
}
|
|
|
|
/**
|
|
* Builds level-specific condition for different log types
|
|
*/
|
|
const buildLevelConditions = (logType: string, levelFilter: string[]) => {
|
|
const conditions = []
|
|
|
|
switch (logType) {
|
|
case 'edge':
|
|
if (levelFilter.includes('success'))
|
|
conditions.push('edge_logs_response.status_code BETWEEN 200 AND 299')
|
|
if (levelFilter.includes('warning'))
|
|
conditions.push('edge_logs_response.status_code BETWEEN 400 AND 499')
|
|
if (levelFilter.includes('error')) conditions.push('edge_logs_response.status_code >= 500')
|
|
break
|
|
case 'postgres':
|
|
if (levelFilter.includes('success')) conditions.push("pgl_parsed.error_severity = 'LOG'")
|
|
if (levelFilter.includes('warning')) conditions.push("pgl_parsed.error_severity = 'WARNING'")
|
|
if (levelFilter.includes('error')) conditions.push("pgl_parsed.error_severity = 'ERROR'")
|
|
break
|
|
case 'edge function':
|
|
if (levelFilter.includes('success'))
|
|
conditions.push('fel_response.status_code BETWEEN 200 AND 299')
|
|
if (levelFilter.includes('warning'))
|
|
conditions.push('fel_response.status_code BETWEEN 400 AND 499')
|
|
if (levelFilter.includes('error')) conditions.push('fel_response.status_code >= 500')
|
|
break
|
|
case 'auth':
|
|
if (levelFilter.includes('success'))
|
|
conditions.push('el_in_al_response.status_code BETWEEN 200 AND 299')
|
|
if (levelFilter.includes('warning'))
|
|
conditions.push('el_in_al_response.status_code BETWEEN 400 AND 499')
|
|
if (levelFilter.includes('error')) conditions.push('el_in_al_response.status_code >= 500')
|
|
break
|
|
case 'supavisor':
|
|
if (levelFilter.includes('success'))
|
|
conditions.push("LOWER(svl_metadata.level) NOT IN ('error', 'warn', 'warning')")
|
|
if (levelFilter.includes('warning'))
|
|
conditions.push(
|
|
"(LOWER(svl_metadata.level) = 'warn' OR LOWER(svl_metadata.level) = 'warning')"
|
|
)
|
|
if (levelFilter.includes('error')) conditions.push("LOWER(svl_metadata.level) = 'error'")
|
|
break
|
|
}
|
|
|
|
return conditions
|
|
}
|
|
|
|
/**
|
|
* Creates WHERE clause for a specific log type including level filtering
|
|
*/
|
|
const createFilterWhereClause = (
|
|
logType: string,
|
|
levelFilter: string[],
|
|
baseConditions: string[]
|
|
) => {
|
|
const hasLevelFilter = levelFilter.length > 0
|
|
|
|
let where = ''
|
|
|
|
if (hasLevelFilter) {
|
|
const levelConditions = buildLevelConditions(logType, levelFilter)
|
|
|
|
if (levelConditions.length > 0) {
|
|
if (baseConditions.length > 0) {
|
|
where = `WHERE (${levelConditions.join(' OR ')}) AND ${baseConditions.join(' AND ')}`
|
|
} else {
|
|
where = `WHERE (${levelConditions.join(' OR ')})`
|
|
}
|
|
} else if (baseConditions.length > 0) {
|
|
where = `WHERE ${baseConditions.join(' AND ')}`
|
|
}
|
|
} else if (baseConditions.length > 0) {
|
|
where = `WHERE ${baseConditions.join(' AND ')}`
|
|
}
|
|
|
|
// Special case for auth logs
|
|
if (logType === 'auth') {
|
|
if (where) {
|
|
where = where.replace('WHERE', 'WHERE al_metadata.request_id is not null AND')
|
|
} else {
|
|
where = 'WHERE al_metadata.request_id is not null'
|
|
}
|
|
}
|
|
|
|
return where
|
|
}
|
|
|
|
/**
|
|
* Builds base conditions array from search params
|
|
*/
|
|
const buildBaseConditions = (search: SearchParamsType): string[] => {
|
|
const baseConditions: string[] = []
|
|
|
|
Object.entries(search).forEach(([key, value]) => {
|
|
// Skip pagination/control parameters, date and level (handled separately)
|
|
if (BASE_CONDITIONS_EXCLUDED_PARAMS.includes(key as any)) {
|
|
return
|
|
}
|
|
|
|
// Handle array filters (IN clause)
|
|
if (Array.isArray(value) && value.length > 0) {
|
|
baseConditions.push(`${key} IN (${value.map((v) => `'${v}'`).join(', ')})`)
|
|
}
|
|
// Handle scalar values
|
|
else if (value !== null && value !== undefined) {
|
|
baseConditions.push(`${key} = '${value}'`)
|
|
}
|
|
})
|
|
|
|
return baseConditions
|
|
}
|
|
|
|
/**
|
|
* Calculates how much the chart start datetime should be offset given the current datetime filter params
|
|
* and determines the appropriate bucketing level (minute, hour, day)
|
|
* Ported from the older implementation (apps/studio/components/interfaces/Settings/Logs/Logs.utils.ts)
|
|
*/
|
|
const calculateChartBucketing = (search: SearchParamsType | Record<string, any>): string => {
|
|
// Extract start and end times from the date array if available
|
|
const dateRange = search.date || []
|
|
|
|
// Handle timestamps that could be in various formats
|
|
const convertToMillis = (timestamp: any) => {
|
|
if (!timestamp) return null
|
|
// If timestamp is a Date object
|
|
if (timestamp instanceof Date) return timestamp.getTime()
|
|
|
|
// If timestamp is a string that needs parsing
|
|
if (typeof timestamp === 'string') return dayjs(timestamp).valueOf()
|
|
|
|
// If timestamp is already a number (unix timestamp)
|
|
// Check if microseconds (16 digits) and convert to milliseconds
|
|
if (typeof timestamp === 'number') {
|
|
const str = timestamp.toString()
|
|
if (str.length >= 16) return Math.floor(timestamp / 1000)
|
|
return timestamp
|
|
}
|
|
|
|
return null
|
|
}
|
|
|
|
let startMillis = convertToMillis(dateRange[0])
|
|
let endMillis = convertToMillis(dateRange[1])
|
|
|
|
// Default values if not set
|
|
if (!startMillis) startMillis = dayjs().subtract(1, 'hour').valueOf()
|
|
if (!endMillis) endMillis = dayjs().valueOf()
|
|
|
|
const startTime = dayjs(startMillis)
|
|
const endTime = dayjs(endMillis)
|
|
|
|
let truncationLevel = 'MINUTE'
|
|
|
|
const minuteDiff = endTime.diff(startTime, 'minute')
|
|
const hourDiff = endTime.diff(startTime, 'hour')
|
|
const dayDiff = endTime.diff(startTime, 'day')
|
|
|
|
console.log(`Time difference: ${minuteDiff} minutes, ${hourDiff} hours, ${dayDiff} days`)
|
|
|
|
// Adjust bucketing based on time range
|
|
if (dayDiff >= 2) {
|
|
truncationLevel = 'DAY'
|
|
} else if (hourDiff >= 12) {
|
|
truncationLevel = 'HOUR'
|
|
} else {
|
|
truncationLevel = 'MINUTE'
|
|
}
|
|
|
|
return truncationLevel
|
|
}
|
|
|
|
/**
|
|
* Edge logs query fragment
|
|
*
|
|
* excludes `/rest/` in the path
|
|
*/
|
|
const getEdgeLogsQuery = () => {
|
|
return `
|
|
select
|
|
id,
|
|
el.timestamp as timestamp,
|
|
'edge' as log_type,
|
|
CAST(edge_logs_response.status_code AS STRING) as status,
|
|
CASE
|
|
WHEN edge_logs_response.status_code BETWEEN 200 AND 299 THEN 'success'
|
|
WHEN edge_logs_response.status_code BETWEEN 400 AND 499 THEN 'warning'
|
|
WHEN edge_logs_response.status_code >= 500 THEN 'error'
|
|
ELSE 'success'
|
|
END as level,
|
|
edge_logs_request.path as pathname,
|
|
edge_logs_request.host as host,
|
|
null as event_message,
|
|
edge_logs_request.method as method,
|
|
authorization_payload.role as api_role,
|
|
COALESCE(sb.auth_user, null) as auth_user,
|
|
null as log_count,
|
|
null as logs
|
|
from edge_logs as el
|
|
cross join unnest(metadata) as edge_logs_metadata
|
|
cross join unnest(edge_logs_metadata.request) as edge_logs_request
|
|
cross join unnest(edge_logs_metadata.response) as edge_logs_response
|
|
left join unnest(edge_logs_request.sb) as sb
|
|
left join unnest(sb.jwt) as jwt
|
|
left join unnest(jwt.authorization) as auth
|
|
left join unnest(auth.payload) as authorization_payload
|
|
|
|
-- ONLY include logs where the path does not include /rest/
|
|
WHERE edge_logs_request.path NOT LIKE '%/rest/%'
|
|
AND edge_logs_request.path NOT LIKE '%/storage/%'
|
|
|
|
`
|
|
}
|
|
|
|
// Postgrest logs
|
|
|
|
// WHERE pathname includes `/rest/`
|
|
const getPostgrestLogsQuery = () => {
|
|
return `
|
|
select
|
|
id,
|
|
el.timestamp as timestamp,
|
|
'postgrest' as log_type,
|
|
CAST(edge_logs_response.status_code AS STRING) as status,
|
|
CASE
|
|
WHEN edge_logs_response.status_code BETWEEN 200 AND 299 THEN 'success'
|
|
WHEN edge_logs_response.status_code BETWEEN 400 AND 499 THEN 'warning'
|
|
WHEN edge_logs_response.status_code >= 500 THEN 'error'
|
|
ELSE 'success'
|
|
END as level,
|
|
edge_logs_request.path as pathname,
|
|
edge_logs_request.host as host,
|
|
null as event_message,
|
|
edge_logs_request.method as method,
|
|
authorization_payload.role as api_role,
|
|
COALESCE(sb.auth_user, null) as auth_user,
|
|
null as log_count,
|
|
null as logs
|
|
from edge_logs as el
|
|
cross join unnest(metadata) as edge_logs_metadata
|
|
cross join unnest(edge_logs_metadata.request) as edge_logs_request
|
|
cross join unnest(edge_logs_metadata.response) as edge_logs_response
|
|
left join unnest(edge_logs_request.sb) as sb
|
|
left join unnest(sb.jwt) as jwt
|
|
left join unnest(jwt.authorization) as auth
|
|
left join unnest(auth.payload) as authorization_payload
|
|
|
|
-- ONLY include logs where the path includes /rest/
|
|
WHERE edge_logs_request.path LIKE '%/rest/%'
|
|
`
|
|
}
|
|
|
|
/**
|
|
* Postgres logs query fragment
|
|
*/
|
|
const getPostgresLogsQuery = () => {
|
|
return `
|
|
select
|
|
id,
|
|
pgl.timestamp as timestamp,
|
|
'postgres' as log_type,
|
|
CAST(pgl_parsed.sql_state_code AS STRING) as status,
|
|
CASE
|
|
WHEN pgl_parsed.error_severity = 'LOG' THEN 'success'
|
|
WHEN pgl_parsed.error_severity = 'WARNING' THEN 'warning'
|
|
WHEN pgl_parsed.error_severity = 'FATAL' THEN 'error'
|
|
WHEN pgl_parsed.error_severity = 'ERROR' THEN 'error'
|
|
ELSE null
|
|
END as level,
|
|
null as pathname,
|
|
null as host,
|
|
event_message as event_message,
|
|
null as method,
|
|
'api_role' as api_role,
|
|
null as auth_user,
|
|
null as log_count,
|
|
null as logs
|
|
from postgres_logs as pgl
|
|
cross join unnest(pgl.metadata) as pgl_metadata
|
|
cross join unnest(pgl_metadata.parsed) as pgl_parsed
|
|
`
|
|
}
|
|
|
|
/**
|
|
* Edge function logs query fragment
|
|
*/
|
|
const getEdgeFunctionLogsQuery = () => {
|
|
return `
|
|
select
|
|
id,
|
|
fel.timestamp as timestamp,
|
|
'edge function' as log_type,
|
|
CAST(fel_response.status_code AS STRING) as status,
|
|
CASE
|
|
WHEN fel_response.status_code BETWEEN 200 AND 299 THEN 'success'
|
|
WHEN fel_response.status_code BETWEEN 400 AND 499 THEN 'warning'
|
|
WHEN fel_response.status_code >= 500 THEN 'error'
|
|
ELSE 'success'
|
|
END as level,
|
|
fel_request.pathname as pathname,
|
|
fel_request.host as host,
|
|
COALESCE(function_logs_agg.last_event_message, '') as event_message,
|
|
fel_request.method as method,
|
|
authorization_payload.role as api_role,
|
|
COALESCE(sb.auth_user, null) as auth_user,
|
|
function_logs_agg.function_log_count as log_count,
|
|
function_logs_agg.logs as logs
|
|
from function_edge_logs as fel
|
|
cross join unnest(metadata) as fel_metadata
|
|
cross join unnest(fel_metadata.response) as fel_response
|
|
cross join unnest(fel_metadata.request) as fel_request
|
|
left join unnest(fel_request.sb) as sb
|
|
left join unnest(sb.jwt) as jwt
|
|
left join unnest(jwt.authorization) as auth
|
|
left join unnest(auth.payload) as authorization_payload
|
|
left join (
|
|
SELECT
|
|
fl_metadata.execution_id,
|
|
COUNT(fl.id) as function_log_count,
|
|
ANY_VALUE(fl.event_message) as last_event_message,
|
|
ARRAY_AGG(STRUCT(fl.id, fl.timestamp, fl.event_message, fl_metadata.level, fl_metadata.event_type)) as logs
|
|
FROM function_logs as fl
|
|
CROSS JOIN UNNEST(fl.metadata) as fl_metadata
|
|
WHERE fl_metadata.execution_id IS NOT NULL
|
|
GROUP BY fl_metadata.execution_id
|
|
) as function_logs_agg on fel_metadata.execution_id = function_logs_agg.execution_id
|
|
`
|
|
}
|
|
|
|
/**
|
|
* Auth logs query fragment
|
|
*/
|
|
const getAuthLogsQuery = () => {
|
|
return `
|
|
select
|
|
al.id as id,
|
|
el_in_al.timestamp as timestamp,
|
|
'auth' as log_type,
|
|
CAST(el_in_al_response.status_code AS STRING) as status,
|
|
CASE
|
|
WHEN el_in_al_response.status_code BETWEEN 200 AND 299 THEN 'success'
|
|
WHEN el_in_al_response.status_code BETWEEN 400 AND 499 THEN 'warning'
|
|
WHEN el_in_al_response.status_code >= 500 THEN 'error'
|
|
ELSE 'success'
|
|
END as level,
|
|
el_in_al_request.path as pathname,
|
|
el_in_al_request.host as host,
|
|
null as event_message,
|
|
el_in_al_request.method as method,
|
|
authorization_payload.role as api_role,
|
|
COALESCE(sb.auth_user, null) as auth_user,
|
|
null as log_count,
|
|
null as logs
|
|
from auth_logs as al
|
|
cross join unnest(metadata) as al_metadata
|
|
left join (
|
|
edge_logs as el_in_al
|
|
cross join unnest (metadata) as el_in_al_metadata
|
|
cross join unnest (el_in_al_metadata.response) as el_in_al_response
|
|
cross join unnest (el_in_al_response.headers) as el_in_al_response_headers
|
|
cross join unnest (el_in_al_metadata.request) as el_in_al_request
|
|
left join unnest(el_in_al_request.sb) as sb
|
|
left join unnest(sb.jwt) as jwt
|
|
left join unnest(jwt.authorization) as auth
|
|
left join unnest(auth.payload) as authorization_payload
|
|
)
|
|
on al_metadata.request_id = el_in_al_response_headers.cf_ray
|
|
WHERE al_metadata.request_id is not null
|
|
`
|
|
}
|
|
|
|
/**
|
|
* Supabase storage logs query fragment
|
|
*/
|
|
const getSupabaseStorageLogsQuery = () => {
|
|
return `
|
|
select
|
|
id,
|
|
el.timestamp as timestamp,
|
|
'storage' as log_type,
|
|
CAST(edge_logs_response.status_code AS STRING) as status,
|
|
CASE
|
|
WHEN edge_logs_response.status_code BETWEEN 200 AND 299 THEN 'success'
|
|
WHEN edge_logs_response.status_code BETWEEN 400 AND 499 THEN 'warning'
|
|
WHEN edge_logs_response.status_code >= 500 THEN 'error'
|
|
ELSE 'success'
|
|
END as level,
|
|
edge_logs_request.path as pathname,
|
|
edge_logs_request.host as host,
|
|
null as event_message,
|
|
edge_logs_request.method as method,
|
|
authorization_payload.role as api_role,
|
|
COALESCE(sb.auth_user, null) as auth_user,
|
|
null as log_count,
|
|
null as logs
|
|
from edge_logs as el
|
|
cross join unnest(metadata) as edge_logs_metadata
|
|
cross join unnest(edge_logs_metadata.request) as edge_logs_request
|
|
cross join unnest(edge_logs_metadata.response) as edge_logs_response
|
|
left join unnest(edge_logs_request.sb) as sb
|
|
left join unnest(sb.jwt) as jwt
|
|
left join unnest(jwt.authorization) as auth
|
|
left join unnest(auth.payload) as authorization_payload
|
|
-- ONLY include logs where the path includes /storage/
|
|
WHERE edge_logs_request.path LIKE '%/storage/%'
|
|
`
|
|
}
|
|
|
|
/**
|
|
* Combine all log sources to create the unified logs CTE
|
|
*/
|
|
const getUnifiedLogsCTE = () => {
|
|
return `
|
|
WITH unified_logs AS (
|
|
${getPostgrestLogsQuery()}
|
|
union all
|
|
${getPostgresLogsQuery()}
|
|
union all
|
|
${getEdgeFunctionLogsQuery()}
|
|
union all
|
|
${getAuthLogsQuery()}
|
|
union all
|
|
${getSupabaseStorageLogsQuery()}
|
|
)
|
|
`
|
|
}
|
|
|
|
/**
|
|
* Unified logs SQL query
|
|
*/
|
|
export const getUnifiedLogsQuery = (search: QuerySearchParamsType): string => {
|
|
// Use the buildQueryConditions helper
|
|
const { finalWhere } = buildQueryConditions(search)
|
|
|
|
// The unified SQL query with UNION ALL statements
|
|
const sql = `
|
|
${getUnifiedLogsCTE()}
|
|
SELECT
|
|
id,
|
|
timestamp,
|
|
log_type,
|
|
status,
|
|
level,
|
|
pathname,
|
|
host,
|
|
event_message,
|
|
method,
|
|
api_role,
|
|
auth_user,
|
|
log_count,
|
|
logs
|
|
FROM unified_logs
|
|
${finalWhere}
|
|
`
|
|
|
|
return sql
|
|
}
|
|
|
|
/**
|
|
* Get a count query for the total logs within the timeframe
|
|
* Uses proper faceted search behavior where facets show "what would I get if I selected ONLY this option"
|
|
*/
|
|
export const getLogsCountQuery = (search: QuerySearchParamsType): string => {
|
|
const { finalWhere } = buildQueryConditions(search)
|
|
|
|
// Helper function to build WHERE clause excluding a specific field
|
|
const buildFacetWhere = (excludeField: string): string => {
|
|
const conditions: string[] = []
|
|
|
|
Object.entries(search).forEach(([key, value]) => {
|
|
if (key === excludeField) return // Skip the field we're getting facets for
|
|
if (EXCLUDED_QUERY_PARAMS.includes(key as any)) return // Skip pagination and special params
|
|
|
|
// Handle array filters (IN clause)
|
|
if (Array.isArray(value) && value.length > 0) {
|
|
conditions.push(`${key} IN (${value.map((v) => `'${v}'`).join(',')})`)
|
|
return
|
|
}
|
|
|
|
// Handle scalar values
|
|
if (value !== null && value !== undefined) {
|
|
if (['host', 'pathname'].includes(key)) {
|
|
conditions.push(`${key} LIKE '%${value}%'`)
|
|
} else {
|
|
conditions.push(`${key} = '${value}'`)
|
|
}
|
|
}
|
|
})
|
|
|
|
return conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''
|
|
}
|
|
|
|
// Create a count query using the same unified logs CTE
|
|
const sql = `
|
|
${getUnifiedLogsCTE()}
|
|
-- Get total count
|
|
SELECT 'total' as dimension, 'all' as value, COUNT(*) as count
|
|
FROM unified_logs
|
|
${finalWhere}
|
|
|
|
UNION ALL
|
|
|
|
-- Get counts by log_type (exclude log_type filter to avoid self-filtering)
|
|
SELECT 'log_type' as dimension, log_type as value, COUNT(*) as count
|
|
FROM unified_logs
|
|
${buildFacetWhere('log_type') || 'WHERE log_type IS NOT NULL'}
|
|
${buildFacetWhere('log_type') ? ' AND log_type IS NOT NULL' : ''}
|
|
GROUP BY log_type
|
|
|
|
UNION ALL
|
|
|
|
-- Get counts by method (exclude method filter to avoid self-filtering)
|
|
SELECT 'method' as dimension, method as value, COUNT(*) as count
|
|
FROM unified_logs
|
|
${buildFacetWhere('method') || 'WHERE method IS NOT NULL'}
|
|
${buildFacetWhere('method') ? ' AND method IS NOT NULL' : ''}
|
|
GROUP BY method
|
|
|
|
UNION ALL
|
|
|
|
-- Get counts by level (exclude level filter to avoid self-filtering)
|
|
SELECT 'level' as dimension, level as value, COUNT(*) as count
|
|
FROM unified_logs
|
|
${buildFacetWhere('level') || 'WHERE level IS NOT NULL'}
|
|
${buildFacetWhere('level') ? ' AND level IS NOT NULL' : ''}
|
|
GROUP BY level
|
|
|
|
UNION ALL
|
|
|
|
-- Get counts by status (exclude status filter to avoid self-filtering)
|
|
SELECT 'status' as dimension, status as value, COUNT(*) as count
|
|
FROM unified_logs
|
|
${buildFacetWhere('status') || 'WHERE status IS NOT NULL'}
|
|
${buildFacetWhere('status') ? ' AND status IS NOT NULL' : ''}
|
|
GROUP BY status
|
|
|
|
UNION ALL
|
|
|
|
-- Get counts by host (exclude host filter to avoid self-filtering)
|
|
SELECT 'host' as dimension, host as value, COUNT(*) as count
|
|
FROM unified_logs
|
|
${buildFacetWhere('host') || 'WHERE host IS NOT NULL'}
|
|
${buildFacetWhere('host') ? ' AND host IS NOT NULL' : ''}
|
|
GROUP BY host
|
|
|
|
UNION ALL
|
|
|
|
-- Get counts by pathname (exclude pathname filter to avoid self-filtering)
|
|
SELECT 'pathname' as dimension, pathname as value, COUNT(*) as count
|
|
FROM unified_logs
|
|
${buildFacetWhere('pathname') || 'WHERE pathname IS NOT NULL'}
|
|
${buildFacetWhere('pathname') ? ' AND pathname IS NOT NULL' : ''}
|
|
GROUP BY pathname
|
|
|
|
UNION ALL
|
|
|
|
-- Get counts by auth_user (exclude auth_user filter to avoid self-filtering)
|
|
SELECT 'auth_user' as dimension, auth_user as value, COUNT(*) as count
|
|
FROM unified_logs
|
|
${buildFacetWhere('auth_user') || 'WHERE auth_user IS NOT NULL'}
|
|
${buildFacetWhere('auth_user') ? ' AND auth_user IS NOT NULL' : ''}
|
|
GROUP BY auth_user
|
|
`
|
|
|
|
return sql
|
|
}
|
|
|
|
/**
|
|
* Enhanced logs chart query with dynamic bucketing based on time range
|
|
* Incorporates dynamic bucketing from the older implementation
|
|
*/
|
|
export const getLogsChartQuery = (search: QuerySearchParamsType): string => {
|
|
// Use the buildQueryConditions helper
|
|
const { finalWhere } = buildQueryConditions(search)
|
|
|
|
// Determine appropriate bucketing level based on time range
|
|
const truncationLevel = calculateChartBucketing(search)
|
|
|
|
return `
|
|
${getUnifiedLogsCTE()}
|
|
SELECT
|
|
TIMESTAMP_TRUNC(timestamp, ${truncationLevel}) as time_bucket,
|
|
COUNTIF(level = 'success') as success,
|
|
COUNTIF(level = 'warning') as warning,
|
|
COUNTIF(level = 'error') as error,
|
|
COUNT(*) as total_per_bucket
|
|
FROM unified_logs
|
|
${finalWhere}
|
|
GROUP BY time_bucket
|
|
ORDER BY time_bucket ASC
|
|
`
|
|
}
|