Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,30 @@ import {
const clusterApi = new ClustersApi()
const alertRulesApi = new AlertRulesApi()
const alertReceiversApi = new AlertReceiversApi()
const organizationApi = new OrganizationMainCallsApi()

export const observability = createQueryKeys('observability', {
containerName: ({
clusterId,
serviceId,
resourceType = 'deployment',
useContainerLabel = false,
startDate,
endDate,
}: {
clusterId: string
serviceId: string
resourceType?: 'deployment' | 'statefulset'
useContainerLabel?: boolean
startDate: string
endDate: string
}) => ({
queryKey: ['containerName', clusterId, serviceId, resourceType],
queryKey: ['containerName', clusterId, serviceId, resourceType, useContainerLabel],
async queryFn() {
const endpoints = {
deployment: `api/v1/label/deployment/values?match[]=kube_deployment_labels{label_qovery_com_service_id="${serviceId}"}`,
statefulset: `api/v1/label/statefulset/values?match[]=kube_statefulset_labels{label_qovery_com_service_id="${serviceId}"}`,
statefulset: useContainerLabel
? `api/v1/label/container/values?match[]=kube_pod_labels{label_qovery_com_service_id="${serviceId}"}`
: `api/v1/label/statefulset/values?match[]=kube_statefulset_labels{label_qovery_com_service_id="${serviceId}"}`,
}

const endpoint = endpoints[resourceType]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ export interface UseContainerNameProps {
clusterId: string
serviceId: string
resourceType: 'deployment' | 'statefulset'
useContainerLabel?: boolean
startDate: string
endDate: string
}

// Retrieves the container name associated with a specific service
export function useContainerName({ clusterId, serviceId, resourceType, startDate, endDate }: UseContainerNameProps) {
export function useContainerName({
clusterId,
serviceId,
resourceType,
useContainerLabel = false,
startDate,
endDate,
}: UseContainerNameProps) {
return useQuery({
...observability.containerName({ clusterId, serviceId, resourceType, startDate, endDate }),
...observability.containerName({ clusterId, serviceId, resourceType, useContainerLabel, startDate, endDate }),
enabled: Boolean(clusterId && serviceId),
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function CardStorage({ serviceId, clusterId }: { serviceId: string; clust
})

const rawValue = Number(metricsPercentage?.data?.result[0]?.value[1])
const value = Number.isFinite(rawValue) ? Math.round(rawValue) : 0
const value = Number.isFinite(rawValue) ? rawValue : 0

const maxUsageBytes = Number(metricsMaxStorage?.data?.result[0]?.value[1])

Expand All @@ -89,8 +89,14 @@ export function CardStorage({ serviceId, clusterId }: { serviceId: string; clust
const totalStorageGiB = value > 0 ? maxUsageGiB / (value / 100) : 0

const title = `${maxUsageDisplay} ${maxUsageUnit} max storage usage`
const description =
value > 0 ? `${value}% of your ${totalStorageGiB.toFixed(1)} GiB storage allowance` : `No storage usage data`

let description
if (value > 0) {
const displayValue = value < 0.01 ? '< 0.01' : value < 1 ? value.toFixed(2) : Math.round(value).toString()
description = `${displayValue}% of your ${totalStorageGiB.toFixed(1)} GiB storage allowance`
} else {
description = 'No storage usage data'
}

return (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,10 @@ import { PrivateNetworkRequestStatusChart } from './private-network-request-stat
import { SelectTimeRange } from './select-time-range/select-time-range'

function ServiceDashboardContent() {
const { environmentId = '', applicationId = '' } = useParams()
const { environmentId = '', applicationId = '', databaseId = '' } = useParams()

const { data: service } = useService({ serviceId: applicationId })
const serviceId = applicationId || databaseId
const { data: service } = useService({ serviceId })
const { data: environment } = useEnvironment({ environmentId })
const {
expandCharts,
Expand All @@ -55,31 +56,34 @@ function ServiceDashboardContent() {
(service?.serviceType === 'CONTAINER' && (service?.ports || []).some((port) => !port.publicly_accessible)))

const hasStorage =
(service?.serviceType === 'CONTAINER' || service?.serviceType === 'APPLICATION') &&
(service.storage || []).length > 0
service?.serviceType === 'DATABASE' ||
((service?.serviceType === 'CONTAINER' || service?.serviceType === 'APPLICATION') &&
Array.isArray(service.storage) &&
service.storage.length > 0)

const now = new Date()
const oneHourAgo = subHours(now, 1)

const { data: containerName, isFetched: isFetchedContainerName } = useContainerName({
clusterId: environment?.cluster_id ?? '',
serviceId: applicationId,
serviceId: serviceId,
resourceType: hasStorage ? 'statefulset' : 'deployment',
useContainerLabel: service?.serviceType === 'DATABASE',
startDate: oneHourAgo.toISOString(),
endDate: now.toISOString(),
})

const { data: namespace, isFetched: isFetchedNamespace } = useNamespace({
clusterId: environment?.cluster_id ?? '',
serviceId: applicationId,
serviceId: serviceId,
resourceType: hasStorage ? 'statefulset' : 'deployment',
startDate: oneHourAgo.toISOString(),
endDate: now.toISOString(),
})

const { data: ingressName = '' } = useIngressName({
clusterId: environment?.cluster_id ?? '',
serviceId: applicationId,
serviceId: serviceId,
enabled: hasPublicPort,
startDate: oneHourAgo.toISOString(),
endDate: now.toISOString(),
Expand Down Expand Up @@ -190,7 +194,7 @@ function ServiceDashboardContent() {
<div className={clsx('grid h-full gap-3', expandCharts ? 'grid-cols-1' : 'md:grid-cols-1 xl:grid-cols-2')}>
<CardInstanceStatus
clusterId={environment.cluster_id}
serviceId={applicationId}
serviceId={serviceId}
containerName={containerName}
namespace={namespace}
/>
Expand All @@ -199,37 +203,33 @@ function ServiceDashboardContent() {
organizationId={environment.organization.id}
projectId={environment.project.id}
environmentId={environment.id}
serviceId={applicationId}
serviceId={serviceId}
clusterId={environment.cluster_id}
containerName={containerName}
/>
{hasPublicPort && (
<CardHTTPErrors
clusterId={environment.cluster_id}
serviceId={applicationId}
serviceId={serviceId}
containerName={containerName}
ingressName={ingressName}
/>
)}
{hasOnlyPrivatePorts && (
<CardPrivateHTTPErrors
clusterId={environment.cluster_id}
serviceId={applicationId}
serviceId={serviceId}
containerName={containerName}
/>
)}
{hasStorage && <CardStorage clusterId={environment.cluster_id} serviceId={applicationId} />}
{hasStorage && <CardStorage clusterId={environment.cluster_id} serviceId={serviceId} />}
{hasPublicPort && (
<CardPercentile99
clusterId={environment.cluster_id}
serviceId={applicationId}
ingressName={ingressName}
/>
<CardPercentile99 clusterId={environment.cluster_id} serviceId={serviceId} ingressName={ingressName} />
)}
{hasOnlyPrivatePorts && (
<CardPrivatePercentile99
clusterId={environment.cluster_id}
serviceId={applicationId}
serviceId={serviceId}
containerName={containerName}
/>
)}
Expand All @@ -240,14 +240,14 @@ function ServiceDashboardContent() {
<Heading weight="medium">Resources</Heading>
<div className={clsx('grid gap-3', expandCharts ? 'grid-cols-1' : 'md:grid-cols-1 xl:grid-cols-2')}>
<div className="overflow-hidden rounded border border-neutral-250">
<CpuChart clusterId={environment.cluster_id} serviceId={applicationId} containerName={containerName} />
<CpuChart clusterId={environment.cluster_id} serviceId={serviceId} containerName={containerName} />
</div>
<div className="overflow-hidden rounded border border-neutral-250">
<MemoryChart clusterId={environment.cluster_id} serviceId={applicationId} containerName={containerName} />
<MemoryChart clusterId={environment.cluster_id} serviceId={serviceId} containerName={containerName} />
</div>
{hasStorage && (
<div className="overflow-hidden rounded border border-neutral-250">
<DiskChart clusterId={environment.cluster_id} serviceId={applicationId} containerName={containerName} />
<DiskChart clusterId={environment.cluster_id} serviceId={serviceId} containerName={containerName} />
</div>
)}
</div>
Expand All @@ -259,21 +259,21 @@ function ServiceDashboardContent() {
<div className="overflow-hidden rounded border border-neutral-250">
<NetworkRequestStatusChart
clusterId={environment.cluster_id}
serviceId={applicationId}
serviceId={serviceId}
ingressName={ingressName}
/>
</div>
<div className="overflow-hidden rounded border border-neutral-250">
<NetworkRequestDurationChart
clusterId={environment.cluster_id}
serviceId={applicationId}
serviceId={serviceId}
ingressName={ingressName}
/>
</div>
<div className="overflow-hidden rounded border border-neutral-250">
<NetworkRequestSizeChart
clusterId={environment.cluster_id}
serviceId={applicationId}
serviceId={serviceId}
ingressName={ingressName}
/>
</div>
Expand All @@ -287,21 +287,21 @@ function ServiceDashboardContent() {
<div className="overflow-hidden rounded border border-neutral-250">
<PrivateNetworkRequestStatusChart
clusterId={environment.cluster_id}
serviceId={applicationId}
serviceId={serviceId}
containerName={containerName}
/>
</div>
<div className="overflow-hidden rounded border border-neutral-250">
<PrivateNetworkRequestDurationChart
clusterId={environment.cluster_id}
serviceId={applicationId}
serviceId={serviceId}
containerName={containerName}
/>
</div>
<div className="overflow-hidden rounded border border-neutral-250">
<PrivateNetworkRequestSizeChart
clusterId={environment.cluster_id}
serviceId={applicationId}
serviceId={serviceId}
containerName={containerName}
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ jest.mock('@qovery/domains/observability/feature', () => ({
EnableObservabilityContent: () => <div>Enable Content</div>,
EnableObservabilityVideo: () => <div>Enable Video</div>,
DatabaseRdsDashboard: () => <div>Database RDS Dashboard</div>,
ServiceDashboard: () => <div>Service Dashboard</div>,
}))

jest.mock('./placeholder-monitoring', () => ({
Expand Down Expand Up @@ -131,7 +132,7 @@ describe('PageMonitoringFeature', () => {
expect(screen.getByText('Database RDS Dashboard')).toBeInTheDocument()
})

it('should not show monitoring for non-AWS providers', () => {
it('should not show monitoring for managed databases on non-AWS providers', () => {
mockUseService.mockReturnValue({
data: { serviceType: 'DATABASE', mode: DatabaseModeEnum.MANAGED },
isFetched: true,
Expand All @@ -149,7 +150,25 @@ describe('PageMonitoringFeature', () => {
expect(screen.getByText('Enable Content')).toBeInTheDocument()
})

it('should not show monitoring for container mode databases', () => {
it('should show service dashboard for container databases on GCP when metrics are enabled', () => {
mockUseService.mockReturnValue({
data: { serviceType: 'DATABASE', mode: DatabaseModeEnum.CONTAINER },
isFetched: true,
})
mockUseCluster.mockReturnValue({
data: {
cloud_provider: 'GCP',
metrics_parameters: { enabled: true },
},
isFetched: true,
})
mockUseDeploymentStatus.mockReturnValue({ data: { state: 'RUNNING' } })

render(<PageMonitoringFeature />)
expect(screen.getByText('Service Dashboard')).toBeInTheDocument()
})

it('should show service dashboard for container mode databases when metrics are enabled', () => {
mockUseService.mockReturnValue({
data: { serviceType: 'DATABASE', mode: DatabaseModeEnum.CONTAINER },
isFetched: true,
Expand All @@ -159,7 +178,26 @@ describe('PageMonitoringFeature', () => {
cloud_provider: 'AWS',
metrics_parameters: {
enabled: true,
configuration: { cloud_watch_export_config: { enabled: true } },
},
},
isFetched: true,
})
mockUseDeploymentStatus.mockReturnValue({ data: { state: 'RUNNING' } })

render(<PageMonitoringFeature />)
expect(screen.getByText('Service Dashboard')).toBeInTheDocument()
})

it('should show upsell for container mode databases when metrics are disabled', () => {
mockUseService.mockReturnValue({
data: { serviceType: 'DATABASE', mode: DatabaseModeEnum.CONTAINER },
isFetched: true,
})
mockUseCluster.mockReturnValue({
data: {
cloud_provider: 'AWS',
metrics_parameters: {
enabled: false,
},
},
isFetched: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { DatabaseModeEnum } from 'qovery-typescript-axios'
import { useMemo } from 'react'
import { useParams } from 'react-router-dom'
import { match } from 'ts-pattern'
import { useCluster } from '@qovery/domains/clusters/feature'
import { useEnvironment } from '@qovery/domains/environments/feature'
import {
DatabaseRdsDashboard,
EnableObservabilityButtonContactUs,
EnableObservabilityContent,
EnableObservabilityVideo,
ServiceDashboard,
} from '@qovery/domains/observability/feature'
import { type Database } from '@qovery/domains/services/data-access'
import { useDeploymentStatus, useService } from '@qovery/domains/services/feature'
Expand All @@ -30,11 +32,17 @@ export function PageMonitoringFeature() {

const hasMetrics = useMemo(
() =>
cluster?.cloud_provider === 'AWS' &&
cluster?.metrics_parameters?.enabled &&
cluster?.metrics_parameters?.configuration?.cloud_watch_export_config?.enabled &&
service?.serviceType === 'DATABASE' &&
(service as Database)?.mode === DatabaseModeEnum.MANAGED,
cluster?.metrics_parameters?.enabled &&
match((service as Database)?.mode)
.with(DatabaseModeEnum.MANAGED, () => {
return (
cluster?.cloud_provider === 'AWS' &&
cluster?.metrics_parameters?.configuration?.cloud_watch_export_config?.enabled
)
})
.with(DatabaseModeEnum.CONTAINER, () => true)
.otherwise(() => false),
[
cluster?.cloud_provider,
cluster?.metrics_parameters?.configuration?.cloud_watch_export_config?.enabled,
Expand Down Expand Up @@ -73,17 +81,19 @@ export function PageMonitoringFeature() {
</div>
)

return noMetricsAvailable ? (
<div className="px-10 py-7">
<div className="flex flex-col items-center gap-1 rounded border border-neutral-200 bg-neutral-100 py-10 text-sm text-neutral-350">
<Icon className="text-md text-neutral-300" iconStyle="regular" iconName="circle-question" />
<span className="font-medium">Monitoring is not available</span>
<span>Deploy this database to view monitoring data</span>
if (noMetricsAvailable) {
return (
<div className="px-10 py-7">
<div className="flex flex-col items-center gap-1 rounded border border-neutral-200 bg-neutral-100 py-10 text-sm text-neutral-350">
<Icon className="text-md text-neutral-300" iconStyle="regular" iconName="circle-question" />
<span className="font-medium">Monitoring is not available</span>
<span>Deploy this database to view monitoring data</span>
</div>
</div>
</div>
) : (
<DatabaseRdsDashboard />
)
)
}

return (service as Database)?.mode === DatabaseModeEnum.CONTAINER ? <ServiceDashboard /> : <DatabaseRdsDashboard />
}

export default PageMonitoringFeature
Loading