diff --git a/frontend/src/container/SpanList/SpanList.tsx b/frontend/src/container/SpanList/SpanList.tsx index f6a771e75a72..a8d0cd5aee6e 100644 --- a/frontend/src/container/SpanList/SpanList.tsx +++ b/frontend/src/container/SpanList/SpanList.tsx @@ -1,11 +1,14 @@ import './SpanList.styles.scss'; +import { ENTITY_VERSION_V5 } from 'constants/app'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; import { useMemo } from 'react'; import { Span } from 'types/api/trace/getTraceV2'; -import { mockEntrySpanData } from './mockData'; import SearchFilters from './SearchFilters'; import SpanTable from './SpanTable'; +import { SpanDataRow } from './types'; import { transformEntrySpansToHierarchy } from './utils'; interface SpanListProps { @@ -14,9 +17,90 @@ interface SpanListProps { } function SpanList({ traceId, setSelectedSpan }: SpanListProps): JSX.Element { + const payload = initialQueriesMap.traces; + + const { data, isLoading, isFetching } = useGetQueryRange( + { + graphType: PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + query: { + ...payload, + builder: { + ...payload.builder, + queryData: [ + { + ...payload.builder.queryData[0], + ...{ + name: 'A', + signal: 'traces', + stepInterval: null, + disabled: false, + filter: { + expression: `trace_id = '${traceId}' isEntryPoint = 'true'`, + }, + limit: 10, + offset: 0, + order: [ + { + key: { + name: 'timestamp', + }, + direction: 'desc', + }, + ], + having: { + expression: '', + }, + selectFields: [ + { + name: 'service.name', + fieldDataType: 'string', + signal: 'traces', + fieldContext: 'resource', + }, + { + name: 'name', + fieldDataType: 'string', + signal: 'traces', + }, + { + name: 'duration_nano', + fieldDataType: '', + signal: 'traces', + fieldContext: 'span', + }, + { + name: 'http_method', + fieldDataType: '', + signal: 'traces', + fieldContext: 'span', + }, + { + name: 'response_status_code', + fieldDataType: '', + signal: 'traces', + fieldContext: 'span', + }, + ], + }, + }, + ], + }, + }, + }, + // version, + ENTITY_VERSION_V5, + ); + const hierarchicalData = useMemo( - () => transformEntrySpansToHierarchy(mockEntrySpanData), - [], + () => + // TODO(shaheer): properly fix the type + transformEntrySpansToHierarchy( + (data?.payload.data.newResult.data.result[0].list as unknown) as + | SpanDataRow[] + | undefined, + ), + [data?.payload.data.newResult.data.result], ); return ( @@ -27,6 +111,7 @@ function SpanList({ traceId, setSelectedSpan }: SpanListProps): JSX.Element {
diff --git a/frontend/src/container/SpanList/SpanTable.tsx b/frontend/src/container/SpanList/SpanTable.tsx index 98f0dde3a43d..d05491b8e57a 100644 --- a/frontend/src/container/SpanList/SpanTable.tsx +++ b/frontend/src/container/SpanList/SpanTable.tsx @@ -1,11 +1,12 @@ import { Button } from '@signozhq/button'; import { ColumnDef, DataTable, Row } from '@signozhq/table'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import { ChevronDownIcon, ChevronRightIcon } from 'lucide-react'; import { useCallback, useMemo, useState } from 'react'; import { Span } from 'types/api/trace/getTraceV2'; import { HierarchicalSpanData, ServiceEntrySpan, SpanDataRow } from './types'; -import { fetchServiceSpans, formatDuration } from './utils'; +import { fetchServiceSpans } from './utils'; // Constants const SPAN_TYPE_ENTRY = 'entry-span'; @@ -15,6 +16,7 @@ interface SpanTableProps { data: HierarchicalSpanData; traceId?: string; setSelectedSpan?: (span: Span) => void; + isLoading?: boolean; } interface TableRowData { @@ -36,6 +38,7 @@ function SpanTable({ data, traceId, setSelectedSpan, + isLoading, }: SpanTableProps): JSX.Element { const [expandedEntrySpans, setExpandedEntrySpans] = useState< Record @@ -156,17 +159,6 @@ function SpanTable({ [], ); - const renderSpanCountCell = useCallback( - ({ row }: { row: Row }): JSX.Element => { - const { original } = row; - if (original.type === SPAN_TYPE_ENTRY && original.spanCount !== undefined) { - return {original.spanCount}; - } - return -; - }, - [], - ); - const renderDurationCell = useCallback( ({ row }: { row: Row }): JSX.Element => { const { original } = row; @@ -190,7 +182,15 @@ function SpanTable({ const renderStatusCodeCell = useCallback( ({ row }: { row: Row }): JSX.Element => { const { original } = row; - return {original.statusCode}; + return {original.statusCode || '-'}; + }, + [], + ); + + const renderSpanIdCell = useCallback( + ({ row }: { row: Row }): JSX.Element => { + const { original } = row; + return {original.spanId}; }, [], ); @@ -218,6 +218,13 @@ function SpanTable({ size: 180, cell: renderTimestampCell, }, + { + id: 'spanId', + header: 'Span ID', + accessorKey: 'spanId', + size: 150, + cell: renderSpanIdCell, + }, { id: 'httpMethod', header: 'Method', @@ -239,13 +246,6 @@ function SpanTable({ size: 120, cell: renderServiceCell, }, - { - id: 'spans', - header: 'Spans', - accessorKey: 'spanCount', - size: 80, - cell: renderSpanCountCell, - }, { id: 'duration', header: 'Duration', @@ -272,7 +272,10 @@ function SpanTable({ spanName: entrySpan.spanData.data.name, serviceName: entrySpan.serviceName, spanCount: spanCount > 0 ? spanCount : undefined, - duration: formatDuration(entrySpan.spanData.data.duration_nano), + duration: getYAxisFormattedValue( + entrySpan.spanData.data.duration_nano.toString(), + 'ns', + ), timestamp: entrySpan.spanData.timestamp, statusCode: entrySpan.spanData.data.response_status_code, httpMethod: entrySpan.spanData.data.http_method, @@ -288,7 +291,10 @@ function SpanTable({ type: SPAN_TYPE_SERVICE, spanName: serviceSpan.data.name, serviceName: serviceSpan.data['service.name'], - duration: formatDuration(serviceSpan.data.duration_nano), + duration: getYAxisFormattedValue( + serviceSpan.data.duration_nano.toString(), + 'ns', + ), timestamp: serviceSpan.timestamp, statusCode: serviceSpan.data.response_status_code, httpMethod: serviceSpan.data.http_method, @@ -354,6 +360,7 @@ function SpanTable({ fixedHeight={args.fixedHeight} data={flattenedData} onRowClick={handleRowClick} + isLoading={isLoading} />
); @@ -362,6 +369,7 @@ function SpanTable({ SpanTable.defaultProps = { traceId: undefined, setSelectedSpan: undefined, + isLoading: false, }; export default SpanTable; diff --git a/frontend/src/container/SpanList/mockData.ts b/frontend/src/container/SpanList/mockData.ts deleted file mode 100644 index 4fb4dbf96ef0..000000000000 --- a/frontend/src/container/SpanList/mockData.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { SpanDataRow } from './types'; - -export const mockEntrySpanData: SpanDataRow[] = [ - { - data: { - duration_nano: 3878813, - http_method: '', - // eslint-disable-next-line sonarjs/no-duplicate-string - name: 'CurrencyService/Convert', - response_status_code: '0', - 'service.name': 'currencyservice', - span_id: '8439d7461ab457a2', - timestamp: '2025-09-03T11:33:46.060209146Z', - trace_id: '5ad9c01671f0e38582efe03bbf81360a', - }, - timestamp: '2025-09-03T11:33:46.060209146Z', - }, - { - data: { - duration_nano: 12485759, - http_method: '', - name: 'CurrencyService/Convert', - response_status_code: '0', - 'service.name': 'currencyservice', - span_id: '7dbf7811106b1049', - timestamp: '2025-09-03T11:33:46.058715482Z', - trace_id: '5ad9c01671f0e38582efe03bbf81360a', - }, - timestamp: '2025-09-03T11:33:46.058715482Z', - }, - { - data: { - duration_nano: 6950595, - http_method: '', - name: 'CurrencyService/Convert', - response_status_code: '0', - 'service.name': 'currencyservice', - span_id: '9adcdf1baa56d23a', - timestamp: '2025-09-03T11:33:46.058319328Z', - trace_id: '5ad9c01671f0e38582efe03bbf81360a', - }, - timestamp: '2025-09-03T11:33:46.058319328Z', - }, - { - data: { - duration_nano: 5323696, - http_method: '', - name: 'CurrencyService/Convert', - response_status_code: '0', - 'service.name': 'currencyservice', - span_id: '1a1b01a750dfe4d6', - timestamp: '2025-09-03T11:33:46.057323233Z', - trace_id: '5ad9c01671f0e38582efe03bbf81360a', - }, - timestamp: '2025-09-03T11:33:46.057323233Z', - }, - { - data: { - duration_nano: 133659, - http_method: '', - name: 'oteldemo.ProductCatalogService/GetProduct', - response_status_code: '0', - 'service.name': 'productcatalogservice', - span_id: '485a987e9dbf8ddd', - timestamp: '2025-09-03T11:33:45.963838319Z', - trace_id: '5ad9c01671f0e38582efe03bbf81360a', - }, - timestamp: '2025-09-03T11:33:45.963838319Z', - }, - { - data: { - duration_nano: 245000000, - http_method: 'GET', - name: '/api/checkout', - response_status_code: '200', - 'service.name': 'checkoutservice', - span_id: 'abc123def456', - timestamp: '2025-09-03T11:33:45.800000000Z', - trace_id: '5ad9c01671f0e38582efe03bbf81360a', - }, - timestamp: '2025-09-03T11:33:45.800000000Z', - }, - { - data: { - duration_nano: 150000000, - http_method: 'POST', - name: '/api/payment', - response_status_code: '200', - 'service.name': 'paymentservice', - span_id: 'def456ghi789', - timestamp: '2025-09-03T11:33:45.750000000Z', - trace_id: '5ad9c01671f0e38582efe03bbf81360a', - }, - timestamp: '2025-09-03T11:33:45.750000000Z', - }, -]; diff --git a/frontend/src/container/SpanList/utils.ts b/frontend/src/container/SpanList/utils.ts index 402c6f2975d8..95398884fe61 100644 --- a/frontend/src/container/SpanList/utils.ts +++ b/frontend/src/container/SpanList/utils.ts @@ -1,30 +1,28 @@ +import { ENTITY_VERSION_V5 } from 'constants/app'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults'; +import { uniqBy } from 'lodash-es'; + import { HierarchicalSpanData, ServiceEntrySpan, SpanDataRow } from './types'; -export function formatDuration(durationNano: number): string { - if (durationNano < 1000) { - return `${durationNano}ns`; - } - if (durationNano < 1000000) { - return `${(durationNano / 1000).toFixed(2)}μs`; - } - if (durationNano < 1000000000) { - return `${(durationNano / 1000000).toFixed(2)}ms`; - } - return `${(durationNano / 1000000000).toFixed(2)}s`; -} - export function transformEntrySpansToHierarchy( - entrySpans: SpanDataRow[], + entrySpans?: SpanDataRow[], ): HierarchicalSpanData { let totalTraceTime = 0; + if (!entrySpans) { + return { entrySpans: [], totalTraceTime: 0 }; + } + + const uniqueEntrySpans = uniqBy(entrySpans, 'data.span_id'); + // Calculate total trace time from all entry spans - entrySpans.forEach((span) => { + uniqueEntrySpans.forEach((span) => { totalTraceTime += span.data.duration_nano; }); // Transform entry spans to ServiceEntrySpan structure - const entrySpansList: ServiceEntrySpan[] = entrySpans.map((span) => ({ + const entrySpansList: ServiceEntrySpan[] = uniqueEntrySpans.map((span) => ({ spanData: span, serviceName: span.data['service.name'], isExpanded: false, @@ -32,58 +30,101 @@ export function transformEntrySpansToHierarchy( isLoadingServiceSpans: false, })); - // Sort by timestamp (most recent first) - entrySpansList.sort( - (a, b) => - new Date(b.spanData.timestamp).getTime() - - new Date(a.spanData.timestamp).getTime(), - ); - return { entrySpans: entrySpansList, totalTraceTime, }; } -// Mock function to simulate fetching service spans -export function fetchServiceSpans( +export async function fetchServiceSpans( traceId: string, serviceName: string, ): Promise { - // This would normally make an API call to get spans for the specific service - // For now, return mock data filtered by service name - return new Promise((resolve) => { - setTimeout(() => { - // Mock response - in real implementation, this would call the API - const mockServiceSpans: SpanDataRow[] = [ - { - data: { - duration_nano: 1500000, - http_method: 'GET', - name: `${serviceName}/internal-call-1`, - response_status_code: '200', - 'service.name': serviceName, - span_id: `${serviceName}-span-1`, - timestamp: '2025-09-03T11:33:46.100000000Z', - trace_id: traceId, + // Use the same payload structure as in SpanList component but with service-specific filter + const payload = initialQueriesMap.traces; + + try { + const response = await GetMetricQueryRange( + { + graphType: PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + query: { + ...payload, + builder: { + ...payload.builder, + queryData: [ + { + ...payload.builder.queryData[0], + ...{ + name: 'A', + signal: 'traces', + stepInterval: null, + disabled: false, + filter: { + expression: `trace_id = '${traceId}' and service.name = '${serviceName}'`, + }, + limit: 10, + offset: 0, + order: [ + { + key: { + name: 'timestamp', + }, + direction: 'desc', + }, + ], + having: { + expression: '', + }, + selectFields: [ + { + name: 'service.name', + fieldDataType: 'string', + signal: 'traces', + fieldContext: 'resource', + }, + { + name: 'name', + fieldDataType: 'string', + signal: 'traces', + }, + { + name: 'duration_nano', + fieldDataType: '', + signal: 'traces', + fieldContext: 'span', + }, + { + name: 'http_method', + fieldDataType: '', + signal: 'traces', + fieldContext: 'span', + }, + { + name: 'response_status_code', + fieldDataType: '', + signal: 'traces', + fieldContext: 'span', + }, + ], + }, + }, + ], }, - timestamp: '2025-09-03T11:33:46.100000000Z', }, - { - data: { - duration_nano: 2500000, - http_method: 'POST', - name: `${serviceName}/internal-call-2`, - response_status_code: '200', - 'service.name': serviceName, - span_id: `${serviceName}-span-2`, - timestamp: '2025-09-03T11:33:46.200000000Z', - trace_id: traceId, - }, - timestamp: '2025-09-03T11:33:46.200000000Z', - }, - ]; - resolve(mockServiceSpans); - }, 500); // Simulate network delay - }); + }, + ENTITY_VERSION_V5, + ); + + // Extract spans from the API response using the same path as SpanList component + const spans = + response?.payload?.data?.newResult?.data?.result?.[0]?.list || []; + + // Transform the API response to SpanDataRow format if needed + // The API should return the correct format for traces, but we'll handle any potential transformation + return (spans as unknown) as SpanDataRow[]; + } catch (error) { + console.error('Failed to fetch service spans:', error); + return []; + } }