fix: get span list data from API

This commit is contained in:
ahmadshaheer 2025-09-14 20:18:49 +04:30
parent 3d4c6eda71
commit 0976a572e3
4 changed files with 218 additions and 180 deletions

View File

@ -1,11 +1,14 @@
import './SpanList.styles.scss'; 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 { useMemo } from 'react';
import { Span } from 'types/api/trace/getTraceV2'; import { Span } from 'types/api/trace/getTraceV2';
import { mockEntrySpanData } from './mockData';
import SearchFilters from './SearchFilters'; import SearchFilters from './SearchFilters';
import SpanTable from './SpanTable'; import SpanTable from './SpanTable';
import { SpanDataRow } from './types';
import { transformEntrySpansToHierarchy } from './utils'; import { transformEntrySpansToHierarchy } from './utils';
interface SpanListProps { interface SpanListProps {
@ -14,9 +17,90 @@ interface SpanListProps {
} }
function SpanList({ traceId, setSelectedSpan }: SpanListProps): JSX.Element { 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( 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 ( return (
@ -27,6 +111,7 @@ function SpanList({ traceId, setSelectedSpan }: SpanListProps): JSX.Element {
<div className="span-list__content"> <div className="span-list__content">
<SpanTable <SpanTable
data={hierarchicalData} data={hierarchicalData}
isLoading={isLoading || isFetching}
traceId={traceId} traceId={traceId}
setSelectedSpan={setSelectedSpan} setSelectedSpan={setSelectedSpan}
/> />

View File

@ -1,11 +1,12 @@
import { Button } from '@signozhq/button'; import { Button } from '@signozhq/button';
import { ColumnDef, DataTable, Row } from '@signozhq/table'; import { ColumnDef, DataTable, Row } from '@signozhq/table';
import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig';
import { ChevronDownIcon, ChevronRightIcon } from 'lucide-react'; import { ChevronDownIcon, ChevronRightIcon } from 'lucide-react';
import { useCallback, useMemo, useState } from 'react'; import { useCallback, useMemo, useState } from 'react';
import { Span } from 'types/api/trace/getTraceV2'; import { Span } from 'types/api/trace/getTraceV2';
import { HierarchicalSpanData, ServiceEntrySpan, SpanDataRow } from './types'; import { HierarchicalSpanData, ServiceEntrySpan, SpanDataRow } from './types';
import { fetchServiceSpans, formatDuration } from './utils'; import { fetchServiceSpans } from './utils';
// Constants // Constants
const SPAN_TYPE_ENTRY = 'entry-span'; const SPAN_TYPE_ENTRY = 'entry-span';
@ -15,6 +16,7 @@ interface SpanTableProps {
data: HierarchicalSpanData; data: HierarchicalSpanData;
traceId?: string; traceId?: string;
setSelectedSpan?: (span: Span) => void; setSelectedSpan?: (span: Span) => void;
isLoading?: boolean;
} }
interface TableRowData { interface TableRowData {
@ -36,6 +38,7 @@ function SpanTable({
data, data,
traceId, traceId,
setSelectedSpan, setSelectedSpan,
isLoading,
}: SpanTableProps): JSX.Element { }: SpanTableProps): JSX.Element {
const [expandedEntrySpans, setExpandedEntrySpans] = useState< const [expandedEntrySpans, setExpandedEntrySpans] = useState<
Record<string, ServiceEntrySpan> Record<string, ServiceEntrySpan>
@ -156,17 +159,6 @@ function SpanTable({
[], [],
); );
const renderSpanCountCell = useCallback(
({ row }: { row: Row<TableRowData> }): JSX.Element => {
const { original } = row;
if (original.type === SPAN_TYPE_ENTRY && original.spanCount !== undefined) {
return <span>{original.spanCount}</span>;
}
return <span>-</span>;
},
[],
);
const renderDurationCell = useCallback( const renderDurationCell = useCallback(
({ row }: { row: Row<TableRowData> }): JSX.Element => { ({ row }: { row: Row<TableRowData> }): JSX.Element => {
const { original } = row; const { original } = row;
@ -190,7 +182,15 @@ function SpanTable({
const renderStatusCodeCell = useCallback( const renderStatusCodeCell = useCallback(
({ row }: { row: Row<TableRowData> }): JSX.Element => { ({ row }: { row: Row<TableRowData> }): JSX.Element => {
const { original } = row; const { original } = row;
return <span>{original.statusCode}</span>; return <span>{original.statusCode || '-'}</span>;
},
[],
);
const renderSpanIdCell = useCallback(
({ row }: { row: Row<TableRowData> }): JSX.Element => {
const { original } = row;
return <span className="span-id">{original.spanId}</span>;
}, },
[], [],
); );
@ -218,6 +218,13 @@ function SpanTable({
size: 180, size: 180,
cell: renderTimestampCell, cell: renderTimestampCell,
}, },
{
id: 'spanId',
header: 'Span ID',
accessorKey: 'spanId',
size: 150,
cell: renderSpanIdCell,
},
{ {
id: 'httpMethod', id: 'httpMethod',
header: 'Method', header: 'Method',
@ -239,13 +246,6 @@ function SpanTable({
size: 120, size: 120,
cell: renderServiceCell, cell: renderServiceCell,
}, },
{
id: 'spans',
header: 'Spans',
accessorKey: 'spanCount',
size: 80,
cell: renderSpanCountCell,
},
{ {
id: 'duration', id: 'duration',
header: 'Duration', header: 'Duration',
@ -272,7 +272,10 @@ function SpanTable({
spanName: entrySpan.spanData.data.name, spanName: entrySpan.spanData.data.name,
serviceName: entrySpan.serviceName, serviceName: entrySpan.serviceName,
spanCount: spanCount > 0 ? spanCount : undefined, 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, timestamp: entrySpan.spanData.timestamp,
statusCode: entrySpan.spanData.data.response_status_code, statusCode: entrySpan.spanData.data.response_status_code,
httpMethod: entrySpan.spanData.data.http_method, httpMethod: entrySpan.spanData.data.http_method,
@ -288,7 +291,10 @@ function SpanTable({
type: SPAN_TYPE_SERVICE, type: SPAN_TYPE_SERVICE,
spanName: serviceSpan.data.name, spanName: serviceSpan.data.name,
serviceName: serviceSpan.data['service.name'], serviceName: serviceSpan.data['service.name'],
duration: formatDuration(serviceSpan.data.duration_nano), duration: getYAxisFormattedValue(
serviceSpan.data.duration_nano.toString(),
'ns',
),
timestamp: serviceSpan.timestamp, timestamp: serviceSpan.timestamp,
statusCode: serviceSpan.data.response_status_code, statusCode: serviceSpan.data.response_status_code,
httpMethod: serviceSpan.data.http_method, httpMethod: serviceSpan.data.http_method,
@ -354,6 +360,7 @@ function SpanTable({
fixedHeight={args.fixedHeight} fixedHeight={args.fixedHeight}
data={flattenedData} data={flattenedData}
onRowClick={handleRowClick} onRowClick={handleRowClick}
isLoading={isLoading}
/> />
</div> </div>
); );
@ -362,6 +369,7 @@ function SpanTable({
SpanTable.defaultProps = { SpanTable.defaultProps = {
traceId: undefined, traceId: undefined,
setSelectedSpan: undefined, setSelectedSpan: undefined,
isLoading: false,
}; };
export default SpanTable; export default SpanTable;

View File

@ -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',
},
];

View File

@ -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'; 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( export function transformEntrySpansToHierarchy(
entrySpans: SpanDataRow[], entrySpans?: SpanDataRow[],
): HierarchicalSpanData { ): HierarchicalSpanData {
let totalTraceTime = 0; let totalTraceTime = 0;
if (!entrySpans) {
return { entrySpans: [], totalTraceTime: 0 };
}
const uniqueEntrySpans = uniqBy(entrySpans, 'data.span_id');
// Calculate total trace time from all entry spans // Calculate total trace time from all entry spans
entrySpans.forEach((span) => { uniqueEntrySpans.forEach((span) => {
totalTraceTime += span.data.duration_nano; totalTraceTime += span.data.duration_nano;
}); });
// Transform entry spans to ServiceEntrySpan structure // Transform entry spans to ServiceEntrySpan structure
const entrySpansList: ServiceEntrySpan[] = entrySpans.map((span) => ({ const entrySpansList: ServiceEntrySpan[] = uniqueEntrySpans.map((span) => ({
spanData: span, spanData: span,
serviceName: span.data['service.name'], serviceName: span.data['service.name'],
isExpanded: false, isExpanded: false,
@ -32,58 +30,101 @@ export function transformEntrySpansToHierarchy(
isLoadingServiceSpans: false, 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 { return {
entrySpans: entrySpansList, entrySpans: entrySpansList,
totalTraceTime, totalTraceTime,
}; };
} }
// Mock function to simulate fetching service spans export async function fetchServiceSpans(
export function fetchServiceSpans(
traceId: string, traceId: string,
serviceName: string, serviceName: string,
): Promise<SpanDataRow[]> { ): Promise<SpanDataRow[]> {
// This would normally make an API call to get spans for the specific service // Use the same payload structure as in SpanList component but with service-specific filter
// For now, return mock data filtered by service name const payload = initialQueriesMap.traces;
return new Promise((resolve) => {
setTimeout(() => { try {
// Mock response - in real implementation, this would call the API const response = await GetMetricQueryRange(
const mockServiceSpans: SpanDataRow[] = [
{ {
data: { graphType: PANEL_TYPES.LIST,
duration_nano: 1500000, selectedTime: 'GLOBAL_TIME',
http_method: 'GET', query: {
name: `${serviceName}/internal-call-1`, ...payload,
response_status_code: '200', builder: {
'service.name': serviceName, ...payload.builder,
span_id: `${serviceName}-span-1`, queryData: [
timestamp: '2025-09-03T11:33:46.100000000Z', {
trace_id: traceId, ...payload.builder.queryData[0],
...{
name: 'A',
signal: 'traces',
stepInterval: null,
disabled: false,
filter: {
expression: `trace_id = '${traceId}' and service.name = '${serviceName}'`,
}, },
timestamp: '2025-09-03T11:33:46.100000000Z', limit: 10,
offset: 0,
order: [
{
key: {
name: 'timestamp',
},
direction: 'desc',
},
],
having: {
expression: '',
},
selectFields: [
{
name: 'service.name',
fieldDataType: 'string',
signal: 'traces',
fieldContext: 'resource',
}, },
{ {
data: { name: 'name',
duration_nano: 2500000, fieldDataType: 'string',
http_method: 'POST', signal: 'traces',
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', {
name: 'duration_nano',
fieldDataType: '',
signal: 'traces',
fieldContext: 'span',
}, },
]; {
resolve(mockServiceSpans); name: 'http_method',
}, 500); // Simulate network delay fieldDataType: '',
}); signal: 'traces',
fieldContext: 'span',
},
{
name: 'response_status_code',
fieldDataType: '',
signal: 'traces',
fieldContext: 'span',
},
],
},
},
],
},
},
},
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 [];
}
} }