diff --git a/apps/studio/components/interfaces/Home/ServiceStatus.tsx b/apps/studio/components/interfaces/Home/ServiceStatus.tsx deleted file mode 100644 index 367f2f6e58f..00000000000 --- a/apps/studio/components/interfaces/Home/ServiceStatus.tsx +++ /dev/null @@ -1,377 +0,0 @@ -import { useParams } from 'common' -import dayjs from 'dayjs' -import { AlertTriangle, CheckCircle2, ChevronRight, Loader2 } from 'lucide-react' -import Link from 'next/link' -import { useEffect, useState } from 'react' -import { Button, InfoIcon, Popover, PopoverContent, PopoverTrigger } from 'ui' - -import { InlineLink } from '@/components/ui/InlineLink' -import { useBranchesQuery } from '@/data/branches/branches-query' -import { useEdgeFunctionServiceStatusQuery } from '@/data/service-status/edge-functions-status-query' -import { - ProjectServiceStatus as APIProjectServiceStatus, - ServiceHealthResponse, - useProjectServiceStatusQuery, -} from '@/data/service-status/service-status-query' -import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' -import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' -import { DOCS_URL } from '@/lib/constants' - -const SERVICE_STATUS_THRESHOLD = 5 // minutes - -export type ProjectServiceStatus = APIProjectServiceStatus | 'DISABLED' - -export const StatusMessage = ({ - status, - isLoading, - isProjectNew, -}: { - isLoading: boolean - isProjectNew: boolean - status?: ProjectServiceStatus -}) => { - if (isLoading) return 'Checking status' - if (status === 'DISABLED') return 'Disabled' - if (status === 'UNHEALTHY') return 'Unhealthy' - if (status === 'COMING_UP') return 'Coming up...' - if (status === 'ACTIVE_HEALTHY') return 'Healthy' - // isProjectNew has to be after all other statuses - if (isProjectNew) return 'Coming up...' - if (status) return status - return 'Unable to connect' -} - -const iconProps = { - size: 18, - strokeWidth: 1.5, -} -const LoaderIcon = () => -const AlertIcon = () => -const CheckIcon = () => - -export const StatusIcon = ({ - isLoading, - isProjectNew, - projectStatus, -}: { - isLoading: boolean - isProjectNew: boolean - projectStatus?: ProjectServiceStatus -}) => { - // - if (projectStatus === 'ACTIVE_HEALTHY') return - if (projectStatus === 'DISABLED') return - if (projectStatus === 'COMING_UP') return - if (isLoading) return - // isProjectNew has to be above UNHEALTHY because in the first few minutes, some services might be starting up and show as UNHEALTHY - if (isProjectNew) return - if (projectStatus === 'UNHEALTHY') return - return -} - -/* - * Extract the db_schema from the response.info object - */ -export const extractDbSchema = (response: ServiceHealthResponse | undefined) => { - if (response?.info && 'db_schema' in response.info) { - return response.info.db_schema - } - return undefined -} - -export const ServiceStatus = () => { - const { ref } = useParams() - const { data: project } = useSelectedProjectQuery() - const [open, setOpen] = useState(false) - - const { - projectAuthAll: authEnabled, - projectEdgeFunctionAll: edgeFunctionsEnabled, - realtimeAll: realtimeEnabled, - projectStorageAll: storageEnabled, - } = useIsFeatureEnabled([ - 'project_auth:all', - 'project_edge_function:all', - 'realtime:all', - 'project_storage:all', - ]) - - const isBranch = project?.parentRef !== project?.ref - - // Get branches data when on a branch - const { data: branches, isPending: isBranchesLoading } = useBranchesQuery( - { projectRef: isBranch ? project?.parentRef : undefined }, - { - enabled: isBranch, - refetchInterval: (query) => { - const data = query.state.data - if (!data) return false - const currentBranch = data.find((branch) => branch.project_ref === ref) - return ['FUNCTIONS_DEPLOYED', 'MIGRATIONS_FAILED', 'FUNCTIONS_FAILED'].includes( - currentBranch?.status || '' - ) - ? false - : 5000 - }, - } - ) - - const currentBranch = isBranch - ? branches?.find((branch) => branch.project_ref === ref) - : undefined - - // [Joshen] Need pooler service check eventually - const { - data: status, - isPending: isLoading, - refetch: refetchServiceStatus, - } = useProjectServiceStatusQuery( - { - projectRef: ref, - }, - { - refetchInterval: (query) => { - const data = query.state.data - const isServiceUnhealthy = data?.some((service) => { - // if the postgrest service has an empty schema, the user chose to turn off postgrest during project creation - if (service.name === 'rest' && extractDbSchema(service) === '') { - return false - } - if (service.status === 'ACTIVE_HEALTHY') { - return false - } - return true - }) - - return isServiceUnhealthy ? 5000 : false - }, - } - ) - const { data: edgeFunctionsStatus, refetch: refetchEdgeFunctionServiceStatus } = - useEdgeFunctionServiceStatusQuery( - { - projectRef: ref, - }, - { - refetchInterval: (query) => { - const data = query.state.data - return !data?.healthy ? 5000 : false - }, - } - ) - - const authStatus = status?.find((service) => service.name === 'auth') - const restStatus = status?.find((service) => service.name === 'rest') - const realtimeStatus = status?.find((service) => service.name === 'realtime') - const storageStatus = status?.find((service) => service.name === 'storage') - const dbStatus = status?.find((service) => service.name === 'db') - - // [Joshen] Need individual troubleshooting docs for each service eventually for users to self serve - const services: { - name: string - error?: string - docsUrl?: string - isLoading: boolean - status: ProjectServiceStatus - logsUrl: string - }[] = [ - { - name: 'Database', - error: undefined, - docsUrl: undefined, - isLoading: isLoading, - status: dbStatus?.status ?? 'UNHEALTHY', - logsUrl: '/logs/postgres-logs', - }, - { - name: 'PostgREST', - error: restStatus?.error, - docsUrl: undefined, - isLoading, - // If PostgREST has an empty schema, it means it's been disabled - status: extractDbSchema(restStatus) === '' ? 'DISABLED' : (restStatus?.status ?? 'UNHEALTHY'), - logsUrl: '/logs/postgrest-logs', - }, - ...(authEnabled - ? [ - { - name: 'Auth', - error: authStatus?.error, - docsUrl: undefined, - isLoading, - status: authStatus?.status ?? 'UNHEALTHY', - logsUrl: '/logs/auth-logs', - }, - ] - : []), - ...(realtimeEnabled - ? [ - { - name: 'Realtime', - error: realtimeStatus?.error, - docsUrl: undefined, - isLoading, - status: realtimeStatus?.status ?? 'UNHEALTHY', - logsUrl: '/logs/realtime-logs', - }, - ] - : []), - ...(storageEnabled - ? [ - { - name: 'Storage', - error: storageStatus?.error, - docsUrl: undefined, - isLoading, - status: storageStatus?.status ?? 'UNHEALTHY', - logsUrl: '/logs/storage-logs', - }, - ] - : []), - ...(edgeFunctionsEnabled - ? [ - { - name: 'Edge Functions', - error: undefined, - docsUrl: `${DOCS_URL}/guides/functions/troubleshooting`, - isLoading, - status: edgeFunctionsStatus?.healthy - ? 'ACTIVE_HEALTHY' - : isLoading - ? 'COMING_UP' - : ('UNHEALTHY' as ProjectServiceStatus), - logsUrl: '/logs/edge-functions-logs', - }, - ] - : []), - ...(isBranch - ? [ - { - name: 'Migrations', - error: undefined, - docsUrl: undefined, - isLoading: isBranchesLoading, - status: (currentBranch?.status === 'FUNCTIONS_DEPLOYED' - ? 'ACTIVE_HEALTHY' - : currentBranch?.status === 'FUNCTIONS_FAILED' || - currentBranch?.status === 'MIGRATIONS_FAILED' - ? 'UNHEALTHY' - : 'COMING_UP') as ProjectServiceStatus, - logsUrl: '/branches', - }, - ] - : []), - ] - - const isMigrationLoading = - isBranchesLoading || - currentBranch?.status === 'CREATING_PROJECT' || - currentBranch?.status === 'RUNNING_MIGRATIONS' - const isLoadingChecks = services.some((service) => service.isLoading) - // We consider a service operational if it's healthy or intentionally disabled - const allServicesOperational = services.every( - (service) => service.status === 'ACTIVE_HEALTHY' || service.status === 'DISABLED' - ) - - // If the project is less than 5 minutes old, and status is not operational, then it's likely the service is still starting up - const isProjectNew = - dayjs.utc().diff(dayjs.utc(project?.inserted_at), 'minute') < SERVICE_STATUS_THRESHOLD || - project?.status === 'COMING_UP' - - useEffect(() => { - let timer: any - - if (isProjectNew) { - const secondsSinceProjectCreated = dayjs - .utc() - .diff(dayjs.utc(project?.inserted_at), 'seconds') - const remainingTimeTillNextCheck = SERVICE_STATUS_THRESHOLD * 60 - secondsSinceProjectCreated - - timer = setTimeout(() => { - refetchServiceStatus() - refetchEdgeFunctionServiceStatus() - }, remainingTimeTillNextCheck * 1000) - } - - return () => { - clearTimeout(timer) - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isProjectNew]) - - return ( - - - - - - {services.map((service) => ( - -
- -
-

{service.name}

-

- -

-
-
-
- View logs - -
- - ))} - {!allServicesOperational && ( -
-
- -
-
-

- {isProjectNew ? 'New' : 'Recently restored'} projects can take up to{' '} - {SERVICE_STATUS_THRESHOLD} minutes to become fully operational. -

-

- If services stay unhealthy, refer to our{' '} - - docs - {' '} - for more information. -

-
-
- )} -
-
- ) -} diff --git a/apps/studio/components/interfaces/Observability/ObservabilityOverview.tsx b/apps/studio/components/interfaces/Observability/ObservabilityOverview.tsx index 7b3eef9ab6a..e18c746b70d 100644 --- a/apps/studio/components/interfaces/Observability/ObservabilityOverview.tsx +++ b/apps/studio/components/interfaces/Observability/ObservabilityOverview.tsx @@ -6,6 +6,7 @@ import { useRouter } from 'next/router' import { useCallback, useMemo, useState } from 'react' import { Badge, Button, Tooltip, TooltipContent, TooltipTrigger } from 'ui' +import { useUnifiedLogsPreview } from '../App/FeaturePreview/FeaturePreviewContext' import { DatabaseInfrastructureSection } from './DatabaseInfrastructureSection' import { useObservabilityOverviewData } from './ObservabilityOverview.utils' import { ObservabilityOverviewFooter } from './ObservabilityOverviewFooter' @@ -30,6 +31,7 @@ export const ObservabilityOverview = () => { const { data: organization } = useSelectedOrganizationQuery() const queryClient = useQueryClient() + const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview() const { projectStorageAll: storageSupported } = useIsFeatureEnabled(['project_storage:all']) const { isEnabled: isDataApiEnabled } = useIsDataApiEnabled({ projectRef }) @@ -79,7 +81,9 @@ export const ObservabilityOverview = () => { key: 'db' as const, name: 'Database', reportUrl: `/project/${projectRef}/observability/database`, - logsUrl: `/project/${projectRef}/logs/postgres-logs`, + logsUrl: isUnifiedLogsEnabled + ? `/project/${projectRef}/logs?filter=log_type:eq:postgres` + : `/project/${projectRef}/logs/postgres-logs`, enabled: true, hasReport: true, }, @@ -87,7 +91,9 @@ export const ObservabilityOverview = () => { key: 'postgrest' as const, name: 'PostgREST', reportUrl: `/project/${projectRef}/observability/postgrest`, - logsUrl: `/project/${projectRef}/logs/postgrest-logs`, + logsUrl: isUnifiedLogsEnabled + ? `/project/${projectRef}/logs?filter=log_type:eq:postgrest` + : `/project/${projectRef}/logs/postgrest-logs`, enabled: true, hasReport: true, }, @@ -95,7 +101,9 @@ export const ObservabilityOverview = () => { key: 'auth' as const, name: 'Auth', reportUrl: `/project/${projectRef}/observability/auth`, - logsUrl: `/project/${projectRef}/logs/auth-logs`, + logsUrl: isUnifiedLogsEnabled + ? `/project/${projectRef}/logs?filter=log_type:eq:auth` + : `/project/${projectRef}/logs/auth-logs`, enabled: true, hasReport: true, }, @@ -103,7 +111,9 @@ export const ObservabilityOverview = () => { key: 'functions' as const, name: 'Edge Functions', reportUrl: `/project/${projectRef}/observability/edge-functions`, - logsUrl: `/project/${projectRef}/logs/edge-functions-logs`, + logsUrl: isUnifiedLogsEnabled + ? `/project/${projectRef}/logs?filter=log_type:eq:edge+function` + : `/project/${projectRef}/logs/edge-functions-logs`, enabled: true, hasReport: true, }, @@ -111,7 +121,9 @@ export const ObservabilityOverview = () => { key: 'storage' as const, name: 'Storage', reportUrl: `/project/${projectRef}/observability/storage`, - logsUrl: `/project/${projectRef}/logs/storage-logs`, + logsUrl: isUnifiedLogsEnabled + ? `/project/${projectRef}/logs?filter=log_type:eq:storage` + : `/project/${projectRef}/logs/storage-logs`, enabled: storageSupported, hasReport: true, }, @@ -119,12 +131,14 @@ export const ObservabilityOverview = () => { key: 'realtime' as const, name: 'Realtime', reportUrl: `/project/${projectRef}/observability/realtime`, - logsUrl: `/project/${projectRef}/logs/realtime-logs`, + logsUrl: isUnifiedLogsEnabled + ? `/project/${projectRef}/logs?filter=log_type:eq:realtime` + : `/project/${projectRef}/logs/realtime-logs`, enabled: true, hasReport: true, }, ], - [projectRef, storageSupported, isDataApiEnabled] + [projectRef, storageSupported, isDataApiEnabled, isUnifiedLogsEnabled] ) const enabledServices = serviceBase.filter((s) => s.enabled) @@ -143,7 +157,8 @@ export const ObservabilityOverview = () => { const end = dayjs.utc(datum.timestamp).add(1, unit).toISOString() const queryParams = new URLSearchParams({ its: start, ite: end }) - router.push(`${logsUrl}?${queryParams.toString()}`) + const separator = logsUrl.includes('?') ? '&' : '?' + router.push(`${logsUrl}${separator}${queryParams.toString()}`) }, [router, interval] ) diff --git a/apps/studio/components/interfaces/ProjectHome/ServiceStatus.tsx b/apps/studio/components/interfaces/ProjectHome/ServiceStatus.tsx index e706e00ee5c..079ba9eaf09 100644 --- a/apps/studio/components/interfaces/ProjectHome/ServiceStatus.tsx +++ b/apps/studio/components/interfaces/ProjectHome/ServiceStatus.tsx @@ -1,26 +1,85 @@ import { useParams } from 'common' import dayjs from 'dayjs' -import { ChevronRight, Loader2 } from 'lucide-react' +import { AlertTriangle, CheckCircle2, ChevronRight, Loader2 } from 'lucide-react' import Link from 'next/link' import { cn, HoverCard, HoverCardContent, HoverCardTrigger, InfoIcon } from 'ui' -import { - extractDbSchema, - ProjectServiceStatus, - StatusIcon, - StatusMessage, -} from '../Home/ServiceStatus' +import { useUnifiedLogsPreview } from '../App/FeaturePreview/FeaturePreviewContext' import { InlineLink } from '@/components/ui/InlineLink' import { SingleStat } from '@/components/ui/SingleStat' import { useBranchesQuery } from '@/data/branches/branches-query' import { useEdgeFunctionServiceStatusQuery } from '@/data/service-status/edge-functions-status-query' -import { useProjectServiceStatusQuery } from '@/data/service-status/service-status-query' +import { + useProjectServiceStatusQuery, + type ProjectServiceStatus as APIProjectServiceStatus, + type ServiceHealthResponse, +} from '@/data/service-status/service-status-query' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { DOCS_URL } from '@/lib/constants' const SERVICE_STATUS_THRESHOLD = 5 // minutes +type ProjectServiceStatus = APIProjectServiceStatus | 'DISABLED' + +const iconProps = { + size: 18, + strokeWidth: 1.5, +} +const LoaderIcon = () => +const AlertIcon = () => +const CheckIcon = () => + +export const StatusIcon = ({ + isLoading, + isProjectNew, + projectStatus, +}: { + isLoading: boolean + isProjectNew: boolean + projectStatus?: ProjectServiceStatus +}) => { + // + if (projectStatus === 'ACTIVE_HEALTHY') return + if (projectStatus === 'DISABLED') return + if (projectStatus === 'COMING_UP') return + if (isLoading) return + // isProjectNew has to be above UNHEALTHY because in the first few minutes, some services might be starting up and show as UNHEALTHY + if (isProjectNew) return + if (projectStatus === 'UNHEALTHY') return + return +} + +const StatusMessage = ({ + status, + isLoading, + isProjectNew, +}: { + isLoading: boolean + isProjectNew: boolean + status?: ProjectServiceStatus +}) => { + if (isLoading) return 'Checking status' + if (status === 'DISABLED') return 'Disabled' + if (status === 'UNHEALTHY') return 'Unhealthy' + if (status === 'COMING_UP') return 'Coming up...' + if (status === 'ACTIVE_HEALTHY') return 'Healthy' + // isProjectNew has to be after all other statuses + if (isProjectNew) return 'Coming up...' + if (status) return status + return 'Unable to connect' +} + +/* + * Extract the db_schema from the response.info object + */ +const extractDbSchema = (response: ServiceHealthResponse | undefined) => { + if (response?.info && 'db_schema' in response.info) { + return response.info.db_schema + } + return undefined +} + /** * [Joshen] JFYI before we go live with this, we need to revisit the migrations section * as I don't think it should live in the ServiceStatus component since its not indicative @@ -42,6 +101,7 @@ const SERVICE_STATUS_THRESHOLD = 5 // minutes export const ServiceStatus = () => { const { ref } = useParams() const { data: project } = useSelectedProjectQuery() + const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview() const { projectAuthAll: authEnabled, @@ -123,7 +183,7 @@ export const ServiceStatus = () => { docsUrl: undefined, isLoading: isLoading, status: dbStatus?.status ?? 'UNHEALTHY', - logsUrl: '/logs/postgres-logs', + logsUrl: isUnifiedLogsEnabled ? '/logs?filter=log_type:eq:postgres' : '/logs/postgres-logs', }, { name: 'PostgREST', @@ -132,7 +192,7 @@ export const ServiceStatus = () => { isLoading, // If PostgREST has an empty schema, it means it's been disabled status: extractDbSchema(restStatus) === '' ? 'DISABLED' : (restStatus?.status ?? 'UNHEALTHY'), - logsUrl: '/logs/postgrest-logs', + logsUrl: isUnifiedLogsEnabled ? '/logs?filter=log_type:eq:postgrest' : '/logs/postgrest-logs', }, ...(authEnabled ? [ @@ -142,7 +202,7 @@ export const ServiceStatus = () => { docsUrl: undefined, isLoading, status: authStatus?.status ?? 'UNHEALTHY', - logsUrl: '/logs/auth-logs', + logsUrl: isUnifiedLogsEnabled ? '/logs?filter=log_type:eq:auth' : '/logs/auth-logs', }, ] : []), @@ -154,7 +214,9 @@ export const ServiceStatus = () => { docsUrl: undefined, isLoading, status: realtimeStatus?.status ?? 'UNHEALTHY', - logsUrl: '/logs/realtime-logs', + logsUrl: isUnifiedLogsEnabled + ? '/logs?filter=log_type:eq:realtime' + : '/logs/realtime-logs', }, ] : []), @@ -166,7 +228,9 @@ export const ServiceStatus = () => { docsUrl: undefined, isLoading, status: storageStatus?.status ?? 'UNHEALTHY', - logsUrl: '/logs/storage-logs', + logsUrl: isUnifiedLogsEnabled + ? '/logs?filter=log_type:eq:storage' + : '/logs/storage-logs', }, ] : []), @@ -182,7 +246,9 @@ export const ServiceStatus = () => { : isLoading ? ('COMING_UP' as const) : ('UNHEALTHY' as const), - logsUrl: '/logs/edge-functions-logs', + logsUrl: isUnifiedLogsEnabled + ? '/logs?filter=log_type:eq:edge+function' + : '/logs/edge-functions-logs', }, ] : []),