From 4d264f4fd7815096176dbb1107d3eea0586e2ee6 Mon Sep 17 00:00:00 2001 From: Benjamin Chastanier Date: Fri, 23 Jan 2026 16:58:01 +0100 Subject: [PATCH] chore(service): allow to retrieve envoy gateway logs Ticket: QOV-1430 --- .../domains-service-logs-data-access.spec.ts | 216 ++++++++++++++++++ .../lib/domains-service-logs-data-access.ts | 31 ++- .../use-service-history-logs.ts | 56 ++++- .../use-service-live-logs.ts | 70 +++++- .../row-service-logs.spec.tsx | 84 +++++++ .../row-service-logs/row-service-logs.tsx | 13 +- .../service-logs-context.tsx | 1 + .../search-service-logs-helpers.spec.ts | 169 ++++++++++++++ .../search-service-logs.spec.tsx | 6 +- .../search-service-logs.tsx | 19 +- 10 files changed, 628 insertions(+), 37 deletions(-) create mode 100644 libs/domains/service-logs/data-access/src/lib/domains-service-logs-data-access.spec.ts create mode 100644 libs/domains/service-logs/feature/src/lib/search-service-logs/search-service-logs-helpers.spec.ts diff --git a/libs/domains/service-logs/data-access/src/lib/domains-service-logs-data-access.spec.ts b/libs/domains/service-logs/data-access/src/lib/domains-service-logs-data-access.spec.ts new file mode 100644 index 00000000000..ce97596ea0c --- /dev/null +++ b/libs/domains/service-logs/data-access/src/lib/domains-service-logs-data-access.spec.ts @@ -0,0 +1,216 @@ +import { type LogFilters, buildLokiQuery } from './domains-service-logs-data-access' + +describe('buildLokiQuery', () => { + const baseFilters: LogFilters = { + serviceId: 'test-service-id', + } + + describe('service logs (default)', () => { + it('should build query for service logs with only serviceId', () => { + const query = buildLokiQuery(baseFilters) + expect(query).toBe('{qovery_com_service_id="test-service-id"}') + }) + + it('should build query for service logs with explicit logType', () => { + const query = buildLokiQuery(baseFilters, 'service') + expect(query).toBe('{qovery_com_service_id="test-service-id"}') + }) + + it('should build query with level filter', () => { + const query = buildLokiQuery({ ...baseFilters, level: 'error' }) + expect(query).toBe('{qovery_com_service_id="test-service-id",level="error"}') + }) + + it('should build query with instance filter', () => { + const query = buildLokiQuery({ ...baseFilters, instance: 'pod-123' }) + expect(query).toBe('{qovery_com_service_id="test-service-id",pod="pod-123"}') + }) + + it('should build query with container filter', () => { + const query = buildLokiQuery({ ...baseFilters, container: 'main' }) + expect(query).toBe('{qovery_com_service_id="test-service-id",container="main"}') + }) + + it('should build query with namespace filter', () => { + const query = buildLokiQuery({ ...baseFilters, namespace: 'production' }) + expect(query).toBe('{qovery_com_service_id="test-service-id",namespace="production"}') + }) + + it('should build query with version filter', () => { + const query = buildLokiQuery({ ...baseFilters, version: 'v1.0.0' }) + expect(query).toBe('{qovery_com_service_id="test-service-id",app="v1.0.0"}') + }) + + it('should build query with deploymentId filter', () => { + const query = buildLokiQuery({ ...baseFilters, deploymentId: 'deploy-123' }) + expect(query).toBe('{qovery_com_service_id="test-service-id",qovery_com_deployment_id="deploy-123"}') + }) + + it('should build query with message search', () => { + const query = buildLokiQuery({ ...baseFilters, message: 'error occurred' }) + expect(query).toBe('{qovery_com_service_id="test-service-id"} |= "error occurred"') + }) + + it('should build query with search text', () => { + const query = buildLokiQuery({ ...baseFilters, search: 'exception' }) + expect(query).toBe('{qovery_com_service_id="test-service-id"} |= "exception"') + }) + + it('should build query with multiple filters', () => { + const query = buildLokiQuery({ + ...baseFilters, + level: 'error', + instance: 'pod-123', + container: 'main', + message: 'failed', + }) + expect(query).toBe( + '{qovery_com_service_id="test-service-id",level="error",pod="pod-123",container="main"} |= "failed"' + ) + }) + + it('should combine message and search filters', () => { + const query = buildLokiQuery({ + ...baseFilters, + message: 'error', + search: 'timeout', + }) + expect(query).toBe('{qovery_com_service_id="test-service-id"} |= "errortimeout"') + }) + }) + + describe('nginx logs', () => { + it('should build query for nginx logs', () => { + const query = buildLokiQuery(baseFilters, 'nginx') + expect(query).toBe( + '({app="ingress-nginx"} | level=~"error|warn" or (qovery_com_associated_service_id="test-service-id" and level!~"error|warn"))' + ) + }) + + it('should build nginx query with level filter', () => { + const query = buildLokiQuery({ ...baseFilters, level: 'error' }, 'nginx') + expect(query).toBe( + '({app="ingress-nginx"} | level=~"error|warn" or (qovery_com_associated_service_id="test-service-id" and level!~"error|warn"),level="error")' + ) + }) + + it('should build nginx query with instance filter', () => { + const query = buildLokiQuery({ ...baseFilters, instance: 'nginx-pod-123' }, 'nginx') + expect(query).toBe( + '({app="ingress-nginx"} | level=~"error|warn" or (qovery_com_associated_service_id="test-service-id" and level!~"error|warn"),pod="nginx-pod-123")' + ) + }) + + it('should build nginx query with message search', () => { + const query = buildLokiQuery({ ...baseFilters, message: '404' }, 'nginx') + expect(query).toBe( + '({app="ingress-nginx"} | level=~"error|warn" or (qovery_com_associated_service_id="test-service-id" and level!~"error|warn")) |= "404"' + ) + }) + + it('should build nginx query with multiple filters', () => { + const query = buildLokiQuery( + { + ...baseFilters, + level: 'error', + instance: 'nginx-pod-123', + message: '500', + }, + 'nginx' + ) + expect(query).toBe( + '({app="ingress-nginx"} | level=~"error|warn" or (qovery_com_associated_service_id="test-service-id" and level!~"error|warn"),level="error",pod="nginx-pod-123") |= "500"' + ) + }) + }) + + describe('envoy logs', () => { + it('should build query for envoy logs', () => { + const query = buildLokiQuery(baseFilters, 'envoy') + expect(query).toBe( + '({app="envoy"} | level=~"error|warn" or (qovery_com_associated_service_id="test-service-id" and level!~"error|warn"))' + ) + }) + + it('should build envoy query with level filter', () => { + const query = buildLokiQuery({ ...baseFilters, level: 'error' }, 'envoy') + expect(query).toBe( + '({app="envoy"} | level=~"error|warn" or (qovery_com_associated_service_id="test-service-id" and level!~"error|warn"),level="error")' + ) + }) + + it('should build envoy query with instance filter', () => { + const query = buildLokiQuery({ ...baseFilters, instance: 'envoy-pod-123' }, 'envoy') + expect(query).toBe( + '({app="envoy"} | level=~"error|warn" or (qovery_com_associated_service_id="test-service-id" and level!~"error|warn"),pod="envoy-pod-123")' + ) + }) + + it('should build envoy query with message search', () => { + const query = buildLokiQuery({ ...baseFilters, message: 'upstream error' }, 'envoy') + expect(query).toBe( + '({app="envoy"} | level=~"error|warn" or (qovery_com_associated_service_id="test-service-id" and level!~"error|warn")) |= "upstream error"' + ) + }) + + it('should build envoy query with multiple filters', () => { + const query = buildLokiQuery( + { + ...baseFilters, + level: 'warn', + instance: 'envoy-pod-456', + container: 'gateway', + message: 'timeout', + }, + 'envoy' + ) + expect(query).toBe( + '({app="envoy"} | level=~"error|warn" or (qovery_com_associated_service_id="test-service-id" and level!~"error|warn"),level="warn",pod="envoy-pod-456",container="gateway") |= "timeout"' + ) + }) + + it('should build envoy query with deploymentId', () => { + const query = buildLokiQuery({ ...baseFilters, deploymentId: 'deploy-envoy-123' }, 'envoy') + expect(query).toBe( + '({app="envoy"} | level=~"error|warn" or (qovery_com_associated_service_id="test-service-id" and level!~"error|warn"),qovery_com_deployment_id="deploy-envoy-123")' + ) + }) + }) + + describe('log type comparison', () => { + it('should use parentheses for nginx and envoy, curly braces for service', () => { + const serviceQuery = buildLokiQuery(baseFilters, 'service') + const nginxQuery = buildLokiQuery(baseFilters, 'nginx') + const envoyQuery = buildLokiQuery(baseFilters, 'envoy') + + expect(serviceQuery).toMatch(/^\{.*\}$/) + expect(nginxQuery).toMatch(/^\(.*\)$/) + expect(envoyQuery).toMatch(/^\(.*\)$/) + }) + + it('should use different app labels for nginx and envoy', () => { + const nginxQuery = buildLokiQuery(baseFilters, 'nginx') + const envoyQuery = buildLokiQuery(baseFilters, 'envoy') + + expect(nginxQuery).toContain('app="ingress-nginx"') + expect(envoyQuery).toContain('app="envoy"') + expect(nginxQuery).not.toContain('app="envoy"') + expect(envoyQuery).not.toContain('app="ingress-nginx"') + }) + + it('should use qovery_com_associated_service_id for nginx and envoy', () => { + const serviceQuery = buildLokiQuery(baseFilters, 'service') + const nginxQuery = buildLokiQuery(baseFilters, 'nginx') + const envoyQuery = buildLokiQuery(baseFilters, 'envoy') + + expect(serviceQuery).toContain('qovery_com_service_id="test-service-id"') + expect(serviceQuery).not.toContain('qovery_com_associated_service_id') + + expect(nginxQuery).toContain('qovery_com_associated_service_id="test-service-id"') + expect(nginxQuery).not.toContain('qovery_com_service_id="test-service-id"') + + expect(envoyQuery).toContain('qovery_com_associated_service_id="test-service-id"') + expect(envoyQuery).not.toContain('qovery_com_service_id="test-service-id"') + }) + }) +}) diff --git a/libs/domains/service-logs/data-access/src/lib/domains-service-logs-data-access.ts b/libs/domains/service-logs/data-access/src/lib/domains-service-logs-data-access.ts index 4cecd4cd40e..2d50ef0b622 100644 --- a/libs/domains/service-logs/data-access/src/lib/domains-service-logs-data-access.ts +++ b/libs/domains/service-logs/data-access/src/lib/domains-service-logs-data-access.ts @@ -1,5 +1,6 @@ import { createQueryKeys } from '@lukemorales/query-key-factory' import { ClustersApi } from 'qovery-typescript-axios' +import { match } from 'ts-pattern' const clusterApi = new ClustersApi() @@ -26,12 +27,18 @@ export interface LogFilters { deploymentId?: string } -export function buildLokiQuery(filters: LogFilters, isNginx = false): string { - const labels: string[] = isNginx - ? [ - `{app="ingress-nginx"} | level=~"error|warn" or (qovery_com_associated_service_id="${filters.serviceId}" and level!~"error|warn")`, - ] - : [`qovery_com_service_id="${filters.serviceId}"`] +export type LogType = 'service' | 'nginx' | 'envoy' + +export function buildLokiQuery(filters: LogFilters, logType: LogType = 'service'): string { + const labels: string[] = match(logType) + .with('nginx', () => [ + `{app="ingress-nginx"} | level=~"error|warn" or (qovery_com_associated_service_id="${filters.serviceId}" and level!~"error|warn")`, + ]) + .with('envoy', () => [ + `{app="envoy"} | level=~"error|warn" or (qovery_com_associated_service_id="${filters.serviceId}" and level!~"error|warn")`, + ]) + .with('service', () => [`qovery_com_service_id="${filters.serviceId}"`]) + .exhaustive() if (filters.level) { labels.push(`level="${filters.level}"`) @@ -57,7 +64,9 @@ export function buildLokiQuery(filters: LogFilters, isNginx = false): string { labels.push(`qovery_com_deployment_id="${filters.deploymentId}"`) } - let query = isNginx ? `(${labels.join(',')})` : `{${labels.join(',')}}` + let query = match(logType) + .with('service', () => `{${labels.join(',')}}`) + .otherwise(() => `(${labels.join(',')})`) if (filters.message || filters.search) { query += ` |= "${filters.message ? filters.message : ''}${filters.search ? `${filters.search}` : ''}"` @@ -187,7 +196,7 @@ export const serviceLogs = createQueryKeys('serviceLogs', { filters, limit, direction, - isNginx = false, + logType = 'service', }: { clusterId: string serviceId: string @@ -197,16 +206,16 @@ export const serviceLogs = createQueryKeys('serviceLogs', { filters?: Omit limit?: number direction?: 'forward' | 'backward' - isNginx?: boolean + logType?: LogType }) => ({ - queryKey: [clusterId, timeRange, startDate, endDate, serviceId, filters, limit, direction, isNginx], + queryKey: [clusterId, timeRange, startDate, endDate, serviceId, filters, limit, direction, logType], async queryFn() { // Convert Date objects to nanosecond Unix epoch format for Loki API // https://grafana.com/docs/loki/latest/reference/loki-http-api/#timestamps const startTimestamp = startDate ? (startDate.getTime() * 1000000).toString() : undefined const endTimestamp = endDate ? (endDate.getTime() * 1000000).toString() : undefined - const query = buildLokiQuery({ serviceId, ...filters }, isNginx) + const query = buildLokiQuery({ serviceId, ...filters }, logType) const response = await clusterApi.getClusterLogs( clusterId, diff --git a/libs/domains/service-logs/feature/src/lib/hooks/use-service-history-logs/use-service-history-logs.ts b/libs/domains/service-logs/feature/src/lib/hooks/use-service-history-logs/use-service-history-logs.ts index 9e42b59389f..d4027bff57a 100644 --- a/libs/domains/service-logs/feature/src/lib/hooks/use-service-history-logs/use-service-history-logs.ts +++ b/libs/domains/service-logs/feature/src/lib/hooks/use-service-history-logs/use-service-history-logs.ts @@ -97,52 +97,79 @@ export function useServiceHistoryLogs({ clusterId, serviceId, enabled = false }: filters, direction: 'backward', limit: LOGS_PER_BATCH, - isNginx: Boolean(queryParams.nginx) ?? false, + logType: 'nginx', }), - enabled: Boolean(clusterId) && Boolean(serviceId) && isHistoryMode && enabled, + enabled: Boolean(clusterId) && Boolean(serviceId) && isHistoryMode && Boolean(queryParams.nginx) && enabled, + }) + + const { + data: envoyLogs = [], + isFetched: isEnvoyFetched, + isFetching: isEnvoyLoading, + refetch: refetchEnvoyLogs, + } = useQuery({ + ...serviceLogs.serviceLogs({ + clusterId, + serviceId, + startDate, + endDate: currentEndDate ?? undefined, + filters, + direction: 'backward', + limit: LOGS_PER_BATCH, + logType: 'envoy', + }), + enabled: Boolean(clusterId) && Boolean(serviceId) && isHistoryMode && Boolean(queryParams.envoy) && enabled, }) - const isFetched = isFetchedLogs || isNginxFetched + const isFetched = useMemo( + () => isFetchedLogs || isNginxFetched || isEnvoyFetched, + [isFetchedLogs, isNginxFetched, isEnvoyFetched] + ) useEffect(() => { - if (isFetched && (logs.length > 0 || nginxLogs.length > 0)) { + if (isFetched && (logs.length > 0 || nginxLogs.length > 0 || envoyLogs.length > 0)) { setAccumulatedLogs((prev) => { const existingKeys = new Set(prev.map((log) => `${log.timestamp}|${log.message}`)) const newLogs = logs.filter((log) => !existingKeys.has(`${log.timestamp}|${log.message}`)) const newNginxLogs = nginxLogs.filter((log) => !existingKeys.has(`${log.timestamp}|${log.message}`)) - return [...newLogs, ...newNginxLogs, ...prev] + const newEnvoyLogs = envoyLogs.filter((log) => !existingKeys.has(`${log.timestamp}|${log.message}`)) + return [...newLogs, ...newNginxLogs, ...newEnvoyLogs, ...prev] }) setIsPaginationLoading(false) } - }, [isFetched, logs, nginxLogs, resetCounter]) + }, [isFetched, logs, nginxLogs, envoyLogs, resetCounter]) useEffect(() => { if (isFetched && isPaginationLoading) { - if (logs.length === 0 && nginxLogs.length === 0) { + if (logs.length === 0 && nginxLogs.length === 0 && envoyLogs.length === 0) { setHasMoreLogs(false) setIsPaginationLoading(false) return } - if (logs.length > 0 || nginxLogs.length > 0) { + if (logs.length > 0 || nginxLogs.length > 0 || envoyLogs.length > 0) { const existingKeys = new Set(accumulatedLogs.map((log) => `${log.timestamp}|${log.message}`)) const hasNewAppLogs = logs.some((log) => !existingKeys.has(`${log.timestamp}|${log.message}`)) const hasNewNginxLogs = nginxLogs.some((log) => !existingKeys.has(`${log.timestamp}|${log.message}`)) + const hasNewEnvoyLogs = envoyLogs.some((log) => !existingKeys.has(`${log.timestamp}|${log.message}`)) - if (!hasNewAppLogs && !hasNewNginxLogs) { + if (!hasNewAppLogs && !hasNewNginxLogs && !hasNewEnvoyLogs) { setHasMoreLogs(false) setIsPaginationLoading(false) } } } - }, [isFetched, logs, nginxLogs, accumulatedLogs, isPaginationLoading]) + }, [isFetched, logs, nginxLogs, envoyLogs, accumulatedLogs, isPaginationLoading]) const refetch = useCallback(() => { refetchLogs() if (queryParams.nginx) { refetchNginxLogs() } - }, [refetchLogs, refetchNginxLogs, queryParams.nginx]) + if (queryParams.envoy) { + refetchEnvoyLogs() + } + }, [refetchLogs, refetchNginxLogs, refetchEnvoyLogs, queryParams.nginx, queryParams.envoy]) const loadPreviousLogs = useCallback(async () => { if (accumulatedLogs.length === 0 || !hasMoreLogs || isPaginationLoading) return @@ -202,11 +229,16 @@ export function useServiceHistoryLogs({ clusterId, serviceId, enabled = false }: } }, [isFetched, logs.length, accumulatedLogs.length]) + const isLoading = useMemo( + () => isLoadingLogs || isNginxLoading || isEnvoyLoading || isPaginationLoading, + [isLoadingLogs, isNginxLoading, isEnvoyLoading, isPaginationLoading] + ) + return { data: normalizedLogs, refetch, isFetched, - isLoading: isLoadingLogs || isNginxLoading || isPaginationLoading, + isLoading, loadPreviousLogs, hasMoreLogs, } diff --git a/libs/domains/service-logs/feature/src/lib/hooks/use-service-live-logs/use-service-live-logs.ts b/libs/domains/service-logs/feature/src/lib/hooks/use-service-live-logs/use-service-live-logs.ts index b21f5f570df..54a4a3e404f 100644 --- a/libs/domains/service-logs/feature/src/lib/hooks/use-service-live-logs/use-service-live-logs.ts +++ b/libs/domains/service-logs/feature/src/lib/hooks/use-service-live-logs/use-service-live-logs.ts @@ -32,7 +32,6 @@ export function useServiceLiveLogs({ clusterId, serviceId, enabled = false }: Us const [isFetched, setIsFetched] = useState(false) const [isLoading, setIsLoading] = useState(false) - const [enabledNginx, setEnabledNginx] = useState(false) const [debouncedLogs, setDebouncedLogs] = useState([]) const flushBufferedLogs = useCallback(() => { @@ -103,6 +102,27 @@ export function useServiceLiveLogs({ clusterId, serviceId, enabled = false }: Us [scheduleFlush] ) + const onLogHandlerEnvoy = useCallback( + ( + _: QueryClient, + log: { + created_at?: number + message?: string + pod_name?: string + container_name?: string + version?: string + } + ) => { + setNewLogsAvailable(true) + const normalizedLog = normalizeWebSocketLog(log) + + // Temporary fix to show Envoy logs as unknown to avoid UI issues + serviceLogsBuffer.current.push({ ...normalizedLog, level: 'unknown' }) + scheduleFlush() + }, + [scheduleFlush] + ) + const onCloseHandler = useCallback((_: QueryClient) => { setDebouncedLogs([]) serviceLogsBuffer.current = [] @@ -152,7 +172,32 @@ export function useServiceLiveLogs({ clusterId, serviceId, enabled = false }: Us search: queryParams.search || undefined, version: queryParams.version || undefined, }, - Boolean(queryParams.nginx) + 'nginx' + ) + }, [ + serviceId, + queryParams.level, + queryParams.instance, + queryParams.container, + queryParams.message, + queryParams.search, + queryParams.version, + ]) + + const dynamicQueryEnvoy = useMemo(() => { + if (!serviceId) return '' + + return buildLokiQuery( + { + serviceId, + level: queryParams.level || undefined, + instance: queryParams.instance || undefined, + container: queryParams.container || undefined, + message: queryParams.message || undefined, + search: queryParams.search || undefined, + version: queryParams.version || undefined, + }, + 'envoy' ) }, [ serviceId, @@ -162,7 +207,6 @@ export function useServiceLiveLogs({ clusterId, serviceId, enabled = false }: Us queryParams.message, queryParams.search, queryParams.version, - queryParams.nginx, ]) useReactQueryWsSubscription({ @@ -200,6 +244,24 @@ export function useServiceLiveLogs({ clusterId, serviceId, enabled = false }: Us onOpen: onOpenHandler, }) + useReactQueryWsSubscription({ + url: QOVERY_WS + '/service/logs', + urlSearchParams: { + organization: organizationId, + project: projectId, + environment: environmentId, + service: serviceId, + cluster: clusterId, + query: dynamicQueryEnvoy, + limit: LIMIT.toString(), + }, + enabled: + Boolean(clusterId) && Boolean(serviceId) && Boolean(dynamicQueryEnvoy) && Boolean(queryParams.envoy) && enabled, + onMessage: onLogHandlerEnvoy, + onClose: onCloseHandler, + onOpen: onOpenHandler, + }) + const pausedDataLogs = useMemo( () => debouncedLogs.sort((a, b) => Number(a?.timestamp) - Number(b?.timestamp)), [debouncedLogs] @@ -211,8 +273,6 @@ export function useServiceLiveLogs({ clusterId, serviceId, enabled = false }: Us setPauseLogs, setNewLogsAvailable, newLogsAvailable, - enabledNginx, - setEnabledNginx, isFetched, isLoading, } diff --git a/libs/domains/service-logs/feature/src/lib/list-service-logs/row-service-logs/row-service-logs.spec.tsx b/libs/domains/service-logs/feature/src/lib/list-service-logs/row-service-logs/row-service-logs.spec.tsx index 03043f87e9f..76a9bed511d 100644 --- a/libs/domains/service-logs/feature/src/lib/list-service-logs/row-service-logs/row-service-logs.spec.tsx +++ b/libs/domains/service-logs/feature/src/lib/list-service-logs/row-service-logs/row-service-logs.spec.tsx @@ -63,4 +63,88 @@ describe('RowServiceLogs', () => { expect(screen.getAllByText('test-container')).toHaveLength(1) }) + + describe('nginx logs', () => { + const nginxLog = { + ...mockLog, + app: 'ingress-nginx', + } + + it('should render NGINX badge for nginx logs', () => { + renderRowServiceLogs(nginxLog) + + expect(screen.getByText('NGINX')).toBeInTheDocument() + expect(screen.queryByText('12345')).not.toBeInTheDocument() + }) + + it('should not expand nginx log row on click', async () => { + const { userEvent } = renderRowServiceLogs(nginxLog) + + await userEvent.click(screen.getByText('Test log message')) + expect(screen.queryByText('Instance')).not.toBeInTheDocument() + expect(screen.queryByText('Container')).not.toBeInTheDocument() + }) + + it('should not show container cell for nginx logs when hasMultipleContainers is true', () => { + renderRowServiceLogs(nginxLog, true) + + expect(screen.getByText('NGINX')).toBeInTheDocument() + expect(screen.queryByText('test-container')).not.toBeInTheDocument() + }) + }) + + describe('envoy logs', () => { + const envoyLog = { + ...mockLog, + app: 'envoy', + } + + it('should render ENVOY badge for envoy logs', () => { + renderRowServiceLogs(envoyLog) + + expect(screen.getByText('ENVOY')).toBeInTheDocument() + expect(screen.queryByText('12345')).not.toBeInTheDocument() + }) + + it('should not expand envoy log row on click', async () => { + const { userEvent } = renderRowServiceLogs(envoyLog) + + await userEvent.click(screen.getByText('Test log message')) + expect(screen.queryByText('Instance')).not.toBeInTheDocument() + expect(screen.queryByText('Container')).not.toBeInTheDocument() + }) + + it('should not show container cell for envoy logs when hasMultipleContainers is true', () => { + renderRowServiceLogs(envoyLog, true) + + expect(screen.getByText('ENVOY')).toBeInTheDocument() + expect(screen.queryByText('test-container')).not.toBeInTheDocument() + }) + }) + + describe('regular service logs', () => { + it('should render instance button for regular service logs', () => { + renderRowServiceLogs(mockLog) + + expect(screen.getByText('12345')).toBeInTheDocument() + expect(screen.queryByText('NGINX')).not.toBeInTheDocument() + expect(screen.queryByText('ENVOY')).not.toBeInTheDocument() + }) + + it('should expand regular service log row on click', async () => { + const { userEvent } = renderRowServiceLogs(mockLog) + + await userEvent.click(screen.getByText('Test log message')) + expect(screen.getByText('Instance')).toBeInTheDocument() + expect(screen.getByText('Container')).toBeInTheDocument() + }) + + it('should show container cell for regular logs when hasMultipleContainers is true', () => { + renderRowServiceLogs(mockLog, true) + + expect(screen.getAllByText('test-container')).toHaveLength(1) + expect(screen.queryByText('NGINX')).not.toBeInTheDocument() + expect(screen.queryByText('ENVOY')).not.toBeInTheDocument() + }) + }) }) diff --git a/libs/domains/service-logs/feature/src/lib/list-service-logs/row-service-logs/row-service-logs.tsx b/libs/domains/service-logs/feature/src/lib/list-service-logs/row-service-logs/row-service-logs.tsx index d9a72e2fc23..6e29818a4fd 100644 --- a/libs/domains/service-logs/feature/src/lib/list-service-logs/row-service-logs/row-service-logs.tsx +++ b/libs/domains/service-logs/feature/src/lib/list-service-logs/row-service-logs/row-service-logs.tsx @@ -38,6 +38,7 @@ export function RowServiceLogs({ log, hasMultipleContainers, highlightedText, se const serviceType = service?.serviceType const isNginx = log.app === 'ingress-nginx' + const isEnvoy = log.app === 'envoy' const { updateTimeContextValue } = useServiceLogsContext() const { utc } = updateTimeContextValue @@ -84,7 +85,7 @@ export function RowServiceLogs({ log, hasMultipleContainers, highlightedText, se return ( <> !isNginx && setIsExpanded(!isExpanded)} + onClick={() => !isNginx && !isEnvoy && setIsExpanded(!isExpanded)} className={twMerge( clsx('sl-row sl-row-appear group relative mt-0.5 cursor-pointer text-xs', { 'bg-red-500/10': isErrorOrCritical, @@ -104,7 +105,7 @@ export function RowServiceLogs({ log, hasMultipleContainers, highlightedText, se )} /> - {!isNginx && ( + {!isNginx && !isEnvoy && ( @@ -113,6 +114,10 @@ export function RowServiceLogs({ log, hasMultipleContainers, highlightedText, se NGINX + ) : isEnvoy ? ( + + ENVOY + ) : (