`
sum by(path,status)(nginx:req_rate:5m_by_path_status{ingress="${ingressName}"}) > 0
`
+// ENVOY: Query for envoy metrics
+const queryEnvoy = (httpRouteName: string) => `
+ sum by(envoy_response_code)(envoy_proxy:req_rate:5m_by_status{httproute_name="${httpRouteName}"}) > 0
+`
+
export function NetworkRequestStatusChart({
clusterId,
serviceId,
ingressName,
+ httpRouteName,
}: {
clusterId: string
serviceId: string
ingressName: string
+ httpRouteName: string
}) {
const { startTimestamp, endTimestamp, useLocalTime, timeRange } = useDashboardContext()
@@ -41,6 +49,7 @@ export function NetworkRequestStatusChart({
setLegendSelectedKeys(new Set())
}
+ // NGINX: Fetch nginx metrics (to remove when migrating to envoy)
const { data: metrics, isLoading: isLoadingMetrics } = useMetrics({
clusterId,
startTimestamp,
@@ -51,8 +60,21 @@ export function NetworkRequestStatusChart({
metricShortName: 'network_req_status',
})
+ // ENVOY: Fetch envoy metrics (only if httpRouteName is configured)
+ const { data: metricsEnvoy, isLoading: isLoadingMetricsEnvoy } = useMetrics({
+ clusterId,
+ startTimestamp,
+ endTimestamp,
+ timeRange,
+ query: queryEnvoy(httpRouteName),
+ boardShortName: 'service_overview',
+ metricShortName: 'envoy_req_status',
+ enabled: !!httpRouteName,
+ })
+
const chartData = useMemo(() => {
- if (!metrics?.data?.result) {
+ // Check if we have data from either source
+ if (!metrics?.data?.result && !metricsEnvoy?.data?.result) {
return []
}
@@ -61,31 +83,66 @@ export function NetworkRequestStatusChart({
{ timestamp: number; time: string; fullTime: string; [key: string]: string | number | null }
>()
- // Process network request metrics
- processMetricsData(
- metrics,
- timeSeriesMap,
- (_, index) => JSON.stringify(metrics.data.result[index].metric),
- (value) => parseFloat(value),
- useLocalTime
- )
+ // NGINX: Process nginx metrics (to remove when migrating to envoy)
+ if (metrics?.data?.result) {
+ processMetricsData(
+ metrics,
+ timeSeriesMap,
+ (_, index) => JSON.stringify({ ...metrics.data.result[index].metric, source: 'nginx' }),
+ (value) => parseFloat(value),
+ useLocalTime
+ )
+ }
+
+ // ENVOY: Process envoy metrics
+ if (metricsEnvoy?.data?.result) {
+ processMetricsData(
+ metricsEnvoy,
+ timeSeriesMap,
+ (_, index) => JSON.stringify({ ...metricsEnvoy.data.result[index].metric, source: 'envoy' }),
+ (value) => parseFloat(value),
+ useLocalTime
+ )
+ }
const baseChartData = Array.from(timeSeriesMap.values()).sort((a, b) => a.timestamp - b.timestamp)
return addTimeRangePadding(baseChartData, startTimestamp, endTimestamp, useLocalTime)
- }, [metrics, useLocalTime, startTimestamp, endTimestamp])
+ }, [metrics, metricsEnvoy, useLocalTime, startTimestamp, endTimestamp])
const seriesNames = useMemo(() => {
- if (!metrics?.data?.result) return []
- return metrics.data.result.map((_: unknown, index: number) =>
- JSON.stringify(metrics.data.result[index].metric)
- ) as string[]
- }, [metrics])
+ const names: string[] = []
+
+ // NGINX: Extract nginx series names (to remove when migrating to envoy)
+ if (metrics?.data?.result) {
+ names.push(
+ ...metrics.data.result.map((_: unknown, index: number) =>
+ JSON.stringify({ ...metrics.data.result[index].metric, source: 'nginx' })
+ )
+ )
+ }
+
+ // ENVOY: Extract envoy series names
+ if (metricsEnvoy?.data?.result) {
+ names.push(
+ ...metricsEnvoy.data.result
+ .filter((result: any) => {
+ const code = result.metric?.envoy_response_code
+ return code !== 'undefined' && code !== undefined && code !== ''
+ })
+ .map((result: any) => JSON.stringify({ ...result.metric, source: 'envoy' }))
+ )
+ }
+
+ return names
+ }, [metrics, metricsEnvoy])
+
+ const isLoading = isLoadingMetrics || isLoadingMetricsEnvoy
return (
0 && !legendSelectedKeys.has(name) ? true : false}
+ hide={legendSelectedKeys.size > 0 && !legendSelectedKeys.has(name)}
/>
))}
- {!isLoadingMetrics && chartData.length > 0 && (
+ {!isLoading && chartData.length > 0 && (
(
- {
- const { path, status } = JSON.parse(value)
- return `path: "${path}" status: "${status}"`
- }}
- {...props}
- />
- )}
+ content={(props) => {
+ // Group series by source
+ const formatter = (value: string) => {
+ const metric = JSON.parse(value)
+ const { source } = metric
+
+ if (source === 'nginx') {
+ const { path, status } = metric
+ return `path: "${path}" status: "${status}" (nginx)`
+ } else {
+ const { envoy_response_code } = metric
+ return `status: "${envoy_response_code}" (envoy)`
+ }
+ }
+
+ return
+ }}
/>
)}
diff --git a/libs/domains/observability/feature/src/lib/service/service-dashboard/service-dashboard.tsx b/libs/domains/observability/feature/src/lib/service/service-dashboard/service-dashboard.tsx
index 93ab304933a..55479504807 100644
--- a/libs/domains/observability/feature/src/lib/service/service-dashboard/service-dashboard.tsx
+++ b/libs/domains/observability/feature/src/lib/service/service-dashboard/service-dashboard.tsx
@@ -7,6 +7,7 @@ import { useService } from '@qovery/domains/services/feature'
import { Button, Callout, Chart, Heading, Icon, InputSelectSmall, Section, Tooltip } from '@qovery/shared/ui'
import { useContainerName } from '../../hooks/use-container-name/use-container-name'
import { useEnvironment } from '../../hooks/use-environment/use-environment'
+import useHttpRouteName from '../../hooks/use-http-route-name/use-http-route-name'
import { useIngressName } from '../../hooks/use-ingress-name/use-ingress-name'
import { useNamespace } from '../../hooks/use-namespace/use-namespace'
import { usePodNames } from '../../hooks/use-pod-names/use-pod-names'
@@ -105,6 +106,14 @@ function ServiceDashboardContent() {
endDate: now.toISOString(),
})
+ const { data: httpRouteName = '' } = useHttpRouteName({
+ clusterId: environment?.cluster_id ?? '',
+ serviceId: serviceId,
+ enabled: hasPublicPort,
+ startDate: oneHourAgo.toISOString(),
+ endDate: now.toISOString(),
+ })
+
if ((!containerName && isFetchedContainerName) || (!namespace && isFetchedNamespace)) {
return (
@@ -231,6 +240,7 @@ function ServiceDashboardContent() {
serviceId={serviceId}
containerName={containerName}
ingressName={ingressName}
+ httpRouteName={httpRouteName}
/>
)}
{hasOnlyPrivatePorts && (
@@ -242,7 +252,12 @@ function ServiceDashboardContent() {
)}
{hasStorage && }
{hasPublicPort && (
-
+
)}
{hasOnlyPrivatePorts && (
@@ -301,6 +317,7 @@ function ServiceDashboardContent() {
clusterId={environment.cluster_id}
serviceId={serviceId}
ingressName={ingressName}
+ httpRouteName={httpRouteName}
/>
@@ -308,6 +325,7 @@ function ServiceDashboardContent() {
clusterId={environment.cluster_id}
serviceId={serviceId}
ingressName={ingressName}
+ httpRouteName={httpRouteName}
/>
diff --git a/libs/domains/observability/feature/src/lib/util-filter/dashboard-context.tsx b/libs/domains/observability/feature/src/lib/util-filter/dashboard-context.tsx
index 70c58c4f837..7742d69cd25 100644
--- a/libs/domains/observability/feature/src/lib/util-filter/dashboard-context.tsx
+++ b/libs/domains/observability/feature/src/lib/util-filter/dashboard-context.tsx
@@ -163,11 +163,14 @@ export function DashboardProvider({ children }: PropsWithChildren) {
const startTimestamp = startDate && convertDatetoTimestamp(startDate).toString()
const endTimestamp = endDate && convertDatetoTimestamp(endDate).toString()
- // Calculate the effective duration for Prometheus queries (accounts for zoom)
- const queryTimeRange =
- isAnyChartZoomed && startTimestamp && endTimestamp
- ? `${Math.floor((parseInt(endTimestamp) - parseInt(startTimestamp)) / 60)}m`
- : timeRange
+ // Calculate the effective duration for Prometheus queries (accounts for zoom and custom ranges)
+ const queryTimeRange = useMemo(() => {
+ // For custom time range or zoomed charts, calculate duration from timestamps
+ if ((timeRange === 'custom' || isAnyChartZoomed) && startTimestamp && endTimestamp) {
+ return `${Math.floor((parseInt(endTimestamp) - parseInt(startTimestamp)) / 60)}m`
+ }
+ return timeRange
+ }, [timeRange, isAnyChartZoomed, startTimestamp, endTimestamp])
// Calculate the average over queryTimeRange with a sub-sampling every 5m or 1m
const THREE_DAYS_IN_SECONDS = 3 * 24 * 60 * 60
diff --git a/libs/shared/ui/src/lib/components/chart/chart.tsx b/libs/shared/ui/src/lib/components/chart/chart.tsx
index 3f8bff0191a..6926331da4f 100644
--- a/libs/shared/ui/src/lib/components/chart/chart.tsx
+++ b/libs/shared/ui/src/lib/components/chart/chart.tsx
@@ -218,12 +218,14 @@ export const ChartLegendContent = ({
style.id = styleId
if (key) {
+ // Escape special characters in the key for CSS selector
+ const escapedKey = CSS.escape(key)
// When highlighting, make non-highlighted paths semi-transparent
style.textContent = `
- path[name]:not([name="${key}"]) {
+ path[name]:not([name="${escapedKey}"]) {
opacity: 0.15 !important;
}
- path[name="${key}"] {
+ path[name="${escapedKey}"] {
opacity: 1 !important;
}
`