mirror of
https://github.com/supabase/supabase.git
synced 2026-10-12 04:45:08 +03:00
* Refactor unified logs count query logic Introduces getUnifiedLogsCountCTE to provide detailed unified logs CTE for count queries, replacing the previous getUnifiedLogsCTE usage in getLogsCountQuery. Removes host and auth_user facet counts from the query and updates the unified logs CTE to include more explicit log type handling and metadata extraction. * Update unified logs CTE to include all log sources Refactored getUnifiedLogsCTE to aggregate logs from PostgREST, Postgres, Edge Functions, Auth, and Supabase Storage sources. Also removed trailing comma from facet count CTEs for improved SQL syntax. * Remove unused 'host' field from log queries Eliminated the 'host' field from multiple log query functions and the final SELECT statement in UnifiedLogs.queries.ts, as it is no longer required for log processing or display. * Remove unused api_role and auth_user fields from log queries Eliminated references to api_role and auth_user fields, along with related joins, from multiple log query functions. This streamlines the queries and removes unnecessary data extraction for these fields. * Update UnifiedLogs.fields.tsx
731 lines
23 KiB
TypeScript
731 lines
23 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,
|
|
null as event_message,
|
|
edge_logs_request.method as method,
|
|
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
|
|
|
|
-- 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,
|
|
null as event_message,
|
|
edge_logs_request.method as method,
|
|
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
|
|
|
|
-- 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,
|
|
event_message as event_message,
|
|
null as method,
|
|
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,
|
|
COALESCE(function_logs_agg.last_event_message, '') as event_message,
|
|
fel_request.method as method,
|
|
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 (
|
|
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,
|
|
null as event_message,
|
|
el_in_al_request.method as method,
|
|
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
|
|
)
|
|
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,
|
|
null as event_message,
|
|
edge_logs_request.method as method,
|
|
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
|
|
-- 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
|
|
*/
|
|
export 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,
|
|
event_message,
|
|
method,
|
|
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"
|
|
*/
|
|
|
|
// Helper function to build WHERE clause excluding a specific field
|
|
const buildFacetWhere = (search: QuerySearchParamsType, 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 ')}` : ''
|
|
}
|
|
|
|
export const getFacetCountCTE = ({
|
|
search,
|
|
facet,
|
|
facetSearch,
|
|
}: {
|
|
search: QuerySearchParamsType
|
|
facet: string
|
|
facetSearch?: string
|
|
}) => {
|
|
const MAX_FACETS_QUANTITY = 20
|
|
|
|
return `
|
|
${facet}_count AS (
|
|
SELECT '${facet}' as dimension, ${facet} as value, COUNT(*) as count
|
|
FROM unified_logs
|
|
${buildFacetWhere(search, `${facet}`) || `WHERE ${facet} IS NOT NULL`}
|
|
${buildFacetWhere(search, `${facet}`) ? ` AND ${facet} IS NOT NULL` : ''}
|
|
${!!facetSearch ? `AND ${facet} LIKE '%${facetSearch}%'` : ''}
|
|
GROUP BY ${facet}
|
|
LIMIT ${MAX_FACETS_QUANTITY}
|
|
)
|
|
`.trim()
|
|
}
|
|
|
|
export const getUnifiedLogsCountCTE = () => {
|
|
return `
|
|
WITH unified_logs AS (
|
|
-- Edge logs (non-rest, non-storage)
|
|
select
|
|
id,
|
|
'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.method as method
|
|
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
|
|
WHERE edge_logs_request.path NOT LIKE '%/rest/%'
|
|
AND edge_logs_request.path NOT LIKE '%/storage/%'
|
|
|
|
union all
|
|
|
|
-- Postgrest logs
|
|
select
|
|
id,
|
|
'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.method as method
|
|
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
|
|
WHERE edge_logs_request.path LIKE '%/rest/%'
|
|
|
|
union all
|
|
|
|
-- Postgres logs
|
|
select
|
|
id,
|
|
'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 method
|
|
from postgres_logs as pgl
|
|
cross join unnest(pgl.metadata) as pgl_metadata
|
|
cross join unnest(pgl_metadata.parsed) as pgl_parsed
|
|
|
|
union all
|
|
|
|
-- Edge function logs
|
|
select
|
|
id,
|
|
'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.method as method
|
|
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
|
|
|
|
union all
|
|
|
|
-- Auth logs
|
|
select
|
|
al.id as id,
|
|
'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.method as method
|
|
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
|
|
)
|
|
on al_metadata.request_id = el_in_al_response_headers.cf_ray
|
|
WHERE al_metadata.request_id is not null
|
|
|
|
union all
|
|
|
|
-- Storage logs
|
|
select
|
|
id,
|
|
'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.method as method
|
|
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
|
|
WHERE edge_logs_request.path LIKE '%/storage/%'
|
|
)
|
|
`
|
|
}
|
|
|
|
export const getLogsCountQuery = (search: QuerySearchParamsType): string => {
|
|
const { finalWhere } = buildQueryConditions(search)
|
|
|
|
// Create a count query using the same unified logs CTE
|
|
const sql = `
|
|
${getUnifiedLogsCountCTE()},
|
|
${getFacetCountCTE({ search, facet: 'log_type' })},
|
|
${getFacetCountCTE({ search, facet: 'method' })},
|
|
${getFacetCountCTE({ search, facet: 'level' })},
|
|
${getFacetCountCTE({ search, facet: 'status' })},
|
|
${getFacetCountCTE({ search, facet: 'pathname' })}
|
|
|
|
-- 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 dimension, value, count from log_type_count
|
|
|
|
UNION ALL
|
|
|
|
-- Get counts by method (exclude method filter to avoid self-filtering)
|
|
SELECT dimension, value, count from method_count
|
|
|
|
UNION ALL
|
|
|
|
-- Get counts by level (exclude level filter to avoid self-filtering)
|
|
SELECT dimension, value, count from level_count
|
|
|
|
UNION ALL
|
|
|
|
-- Get counts by status (exclude status filter to avoid self-filtering)
|
|
SELECT dimension, value, count from status_count
|
|
|
|
UNION ALL
|
|
|
|
-- Get counts by pathname (exclude pathname filter to avoid self-filtering)
|
|
SELECT dimension, value, count from pathname_count
|
|
|
|
`
|
|
|
|
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
|
|
`
|
|
}
|