mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-18 07:56:56 +00:00
feat: span list basic UI using static data
This commit is contained in:
parent
144e866afc
commit
da841650f5
@ -44,6 +44,7 @@
|
|||||||
"@sentry/react": "8.41.0",
|
"@sentry/react": "8.41.0",
|
||||||
"@sentry/webpack-plugin": "2.22.6",
|
"@sentry/webpack-plugin": "2.22.6",
|
||||||
"@signozhq/badge": "0.0.2",
|
"@signozhq/badge": "0.0.2",
|
||||||
|
"@signozhq/button": "0.0.2",
|
||||||
"@signozhq/calendar": "0.0.0",
|
"@signozhq/calendar": "0.0.0",
|
||||||
"@signozhq/callout": "0.0.2",
|
"@signozhq/callout": "0.0.2",
|
||||||
"@signozhq/design-tokens": "1.1.4",
|
"@signozhq/design-tokens": "1.1.4",
|
||||||
|
|||||||
19
frontend/src/container/SpanList/SearchFilters.tsx
Normal file
19
frontend/src/container/SpanList/SearchFilters.tsx
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import { SearchOutlined } from '@ant-design/icons';
|
||||||
|
import { Input } from 'antd';
|
||||||
|
|
||||||
|
function SearchFilters(): JSX.Element {
|
||||||
|
return (
|
||||||
|
<div className="search-filters">
|
||||||
|
<div className="search-filters__input">
|
||||||
|
<Input
|
||||||
|
placeholder="Search Spans..."
|
||||||
|
prefix={<SearchOutlined />}
|
||||||
|
size="middle"
|
||||||
|
className="search-filters__search-input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SearchFilters;
|
||||||
81
frontend/src/container/SpanList/SpanList.styles.scss
Normal file
81
frontend/src/container/SpanList/SpanList.styles.scss
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
.span-list {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
padding: 16px;
|
||||||
|
border-bottom: 1px solid var(--bg-slate-400);
|
||||||
|
// background: var(--bg-vanilla-100);
|
||||||
|
}
|
||||||
|
|
||||||
|
&__content {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-filters {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
|
||||||
|
&__input {
|
||||||
|
flex: 1;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__search-input {
|
||||||
|
.ant-input-prefix {
|
||||||
|
color: var(--text-slate-400);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-table {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.span-name-with-expand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
.expand-button {
|
||||||
|
min-width: auto;
|
||||||
|
padding: 0;
|
||||||
|
height: auto;
|
||||||
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-slate-100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.span-name {
|
||||||
|
padding-left: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-slot='table-cell'] {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
thead > [data-slot='table-row'] {
|
||||||
|
background: var(--bg-slate-400);
|
||||||
|
[data-slot='table-head'] {
|
||||||
|
color: var(--bg-vanilla-400);
|
||||||
|
font-size: 11px;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 18px; /* 163.636% */
|
||||||
|
letter-spacing: 0.44px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
[data-slot='table-row'] {
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-slate-500);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
frontend/src/container/SpanList/SpanList.tsx
Normal file
43
frontend/src/container/SpanList/SpanList.tsx
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
import './SpanList.styles.scss';
|
||||||
|
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { Span } from 'types/api/trace/getTraceV2';
|
||||||
|
|
||||||
|
import { mockEntrySpanData } from './mockData';
|
||||||
|
import SearchFilters from './SearchFilters';
|
||||||
|
import SpanTable from './SpanTable';
|
||||||
|
import { transformEntrySpansToHierarchy } from './utils';
|
||||||
|
|
||||||
|
interface SpanListProps {
|
||||||
|
traceId?: string;
|
||||||
|
setSelectedSpan?: (span: Span) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SpanList({ traceId, setSelectedSpan }: SpanListProps): JSX.Element {
|
||||||
|
const hierarchicalData = useMemo(
|
||||||
|
() => transformEntrySpansToHierarchy(mockEntrySpanData),
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="span-list">
|
||||||
|
<div className="span-list__header">
|
||||||
|
<SearchFilters />
|
||||||
|
</div>
|
||||||
|
<div className="span-list__content">
|
||||||
|
<SpanTable
|
||||||
|
data={hierarchicalData}
|
||||||
|
traceId={traceId}
|
||||||
|
setSelectedSpan={setSelectedSpan}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpanList.defaultProps = {
|
||||||
|
traceId: undefined,
|
||||||
|
setSelectedSpan: (): void => {},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SpanList;
|
||||||
367
frontend/src/container/SpanList/SpanTable.tsx
Normal file
367
frontend/src/container/SpanList/SpanTable.tsx
Normal file
@ -0,0 +1,367 @@
|
|||||||
|
import { Button } from '@signozhq/button';
|
||||||
|
import { ColumnDef, DataTable, Row } from '@signozhq/table';
|
||||||
|
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';
|
||||||
|
|
||||||
|
// Constants
|
||||||
|
const SPAN_TYPE_ENTRY = 'entry-span';
|
||||||
|
const SPAN_TYPE_SERVICE = 'service-span';
|
||||||
|
|
||||||
|
interface SpanTableProps {
|
||||||
|
data: HierarchicalSpanData;
|
||||||
|
traceId?: string;
|
||||||
|
setSelectedSpan?: (span: Span) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TableRowData {
|
||||||
|
id: string;
|
||||||
|
type: typeof SPAN_TYPE_ENTRY | typeof SPAN_TYPE_SERVICE;
|
||||||
|
spanName?: string;
|
||||||
|
serviceName?: string;
|
||||||
|
spanCount?: number;
|
||||||
|
duration?: string;
|
||||||
|
timestamp?: string;
|
||||||
|
statusCode?: string;
|
||||||
|
httpMethod?: string;
|
||||||
|
spanId?: string;
|
||||||
|
originalData?: ServiceEntrySpan | SpanDataRow;
|
||||||
|
isLoading?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function SpanTable({
|
||||||
|
data,
|
||||||
|
traceId,
|
||||||
|
setSelectedSpan,
|
||||||
|
}: SpanTableProps): JSX.Element {
|
||||||
|
const [expandedEntrySpans, setExpandedEntrySpans] = useState<
|
||||||
|
Record<string, ServiceEntrySpan>
|
||||||
|
>({});
|
||||||
|
const [loadingSpans, setLoadingSpans] = useState<Record<string, boolean>>({});
|
||||||
|
|
||||||
|
const handleEntrySpanClick = useCallback(
|
||||||
|
async (entrySpan: ServiceEntrySpan) => {
|
||||||
|
const spanId = entrySpan.spanData.data.span_id;
|
||||||
|
|
||||||
|
if (expandedEntrySpans[spanId]) {
|
||||||
|
// Collapse - remove from expanded spans
|
||||||
|
const { [spanId]: removed, ...rest } = expandedEntrySpans;
|
||||||
|
setExpandedEntrySpans(rest);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expand - fetch service spans
|
||||||
|
if (!entrySpan.serviceSpans && traceId) {
|
||||||
|
setLoadingSpans((prev) => ({ ...prev, [spanId]: true }));
|
||||||
|
|
||||||
|
try {
|
||||||
|
const serviceSpans = await fetchServiceSpans(
|
||||||
|
traceId,
|
||||||
|
entrySpan.serviceName,
|
||||||
|
);
|
||||||
|
const updatedEntrySpan = {
|
||||||
|
...entrySpan,
|
||||||
|
serviceSpans,
|
||||||
|
isExpanded: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
setExpandedEntrySpans((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[spanId]: updatedEntrySpan,
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch service spans:', error);
|
||||||
|
} finally {
|
||||||
|
setLoadingSpans((prev) => ({ ...prev, [spanId]: false }));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Already have service spans, just toggle expansion
|
||||||
|
setExpandedEntrySpans((prev) => ({
|
||||||
|
...prev,
|
||||||
|
[spanId]: { ...entrySpan, isExpanded: true },
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[expandedEntrySpans, traceId],
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleSpanClick = useCallback(
|
||||||
|
(span: SpanDataRow) => {
|
||||||
|
if (setSelectedSpan) {
|
||||||
|
// Convert span data to the format expected by SpanDetailsDrawer
|
||||||
|
const convertedSpan = ({
|
||||||
|
id: span.data.span_id,
|
||||||
|
traceID: span.data.trace_id,
|
||||||
|
spanID: span.data.span_id,
|
||||||
|
parentSpanID: '',
|
||||||
|
operationName: span.data.name,
|
||||||
|
startTime: new Date(span.timestamp).getTime() * 1000000, // Convert to nanoseconds
|
||||||
|
duration: span.data.duration_nano,
|
||||||
|
tags: [],
|
||||||
|
logs: [],
|
||||||
|
process: {
|
||||||
|
serviceName: span.data['service.name'],
|
||||||
|
tags: [],
|
||||||
|
},
|
||||||
|
} as unknown) as Span;
|
||||||
|
setSelectedSpan(convertedSpan);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[setSelectedSpan],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderNameCell = useCallback(
|
||||||
|
({ row }: { row: Row<TableRowData> }): JSX.Element => {
|
||||||
|
const { original } = row;
|
||||||
|
if (original.type === SPAN_TYPE_ENTRY) {
|
||||||
|
const entrySpan = original.originalData as ServiceEntrySpan;
|
||||||
|
const spanId = entrySpan.spanData.data.span_id;
|
||||||
|
const isExpanded = !!expandedEntrySpans[spanId];
|
||||||
|
const isLoading = loadingSpans[spanId];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="span-name-with-expand">
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
loading={isLoading}
|
||||||
|
prefixIcon={
|
||||||
|
!isLoading && isExpanded ? (
|
||||||
|
<ChevronDownIcon size={16} />
|
||||||
|
) : (
|
||||||
|
<ChevronRightIcon size={16} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onClick={(): Promise<void> => handleEntrySpanClick(entrySpan)}
|
||||||
|
className="expand-button"
|
||||||
|
/>
|
||||||
|
<span className="span-name-text">{original.spanName}</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Service span (nested)
|
||||||
|
return <div className="span-name">{original.spanName}</div>;
|
||||||
|
},
|
||||||
|
[expandedEntrySpans, loadingSpans, handleEntrySpanClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderServiceCell = useCallback(
|
||||||
|
({ row }: { row: Row<TableRowData> }): JSX.Element => {
|
||||||
|
const { original } = row;
|
||||||
|
return <span>{original.serviceName}</span>;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
({ row }: { row: Row<TableRowData> }): JSX.Element => {
|
||||||
|
const { original } = row;
|
||||||
|
return <span>{original.duration}</span>;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderTimestampCell = useCallback(
|
||||||
|
({ row }: { row: Row<TableRowData> }): JSX.Element => {
|
||||||
|
const { original } = row;
|
||||||
|
return (
|
||||||
|
<span className="timestamp">
|
||||||
|
{new Date(original.timestamp || '').toLocaleString()}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderStatusCodeCell = useCallback(
|
||||||
|
({ row }: { row: Row<TableRowData> }): JSX.Element => {
|
||||||
|
const { original } = row;
|
||||||
|
return <span>{original.statusCode}</span>;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const renderHttpMethodCell = useCallback(
|
||||||
|
// eslint-disable-next-line react/no-unused-prop-types
|
||||||
|
({ row }: { row: Row<TableRowData> }): JSX.Element => {
|
||||||
|
const { original } = row;
|
||||||
|
return <span>{original.httpMethod || '-'}</span>;
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const columns: ColumnDef<TableRowData>[] = [
|
||||||
|
{
|
||||||
|
id: 'name',
|
||||||
|
header: 'Span Name',
|
||||||
|
accessorKey: 'spanName',
|
||||||
|
cell: renderNameCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'timestamp',
|
||||||
|
header: 'Timestamp',
|
||||||
|
accessorKey: 'timestamp',
|
||||||
|
size: 180,
|
||||||
|
cell: renderTimestampCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'httpMethod',
|
||||||
|
header: 'Method',
|
||||||
|
accessorKey: 'httpMethod',
|
||||||
|
size: 80,
|
||||||
|
cell: renderHttpMethodCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'statusCode',
|
||||||
|
header: 'Status',
|
||||||
|
accessorKey: 'statusCode',
|
||||||
|
size: 80,
|
||||||
|
cell: renderStatusCodeCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'service',
|
||||||
|
header: 'Service',
|
||||||
|
accessorKey: 'serviceName',
|
||||||
|
size: 120,
|
||||||
|
cell: renderServiceCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'spans',
|
||||||
|
header: 'Spans',
|
||||||
|
accessorKey: 'spanCount',
|
||||||
|
size: 80,
|
||||||
|
cell: renderSpanCountCell,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'duration',
|
||||||
|
header: 'Duration',
|
||||||
|
accessorKey: 'duration',
|
||||||
|
size: 120,
|
||||||
|
cell: renderDurationCell,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const flattenedData = useMemo(() => {
|
||||||
|
const result: TableRowData[] = [];
|
||||||
|
|
||||||
|
data.entrySpans.forEach((entrySpan) => {
|
||||||
|
const spanId = entrySpan.spanData.data.span_id;
|
||||||
|
|
||||||
|
// Calculate span count for this service
|
||||||
|
const expandedSpan = expandedEntrySpans[spanId];
|
||||||
|
const spanCount = expandedSpan?.serviceSpans?.length || 0;
|
||||||
|
|
||||||
|
// Add entry span row
|
||||||
|
result.push({
|
||||||
|
id: spanId,
|
||||||
|
type: SPAN_TYPE_ENTRY,
|
||||||
|
spanName: entrySpan.spanData.data.name,
|
||||||
|
serviceName: entrySpan.serviceName,
|
||||||
|
spanCount: spanCount > 0 ? spanCount : undefined,
|
||||||
|
duration: formatDuration(entrySpan.spanData.data.duration_nano),
|
||||||
|
timestamp: entrySpan.spanData.timestamp,
|
||||||
|
statusCode: entrySpan.spanData.data.response_status_code,
|
||||||
|
httpMethod: entrySpan.spanData.data.http_method,
|
||||||
|
spanId,
|
||||||
|
originalData: entrySpan,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add service spans if expanded
|
||||||
|
if (expandedSpan?.serviceSpans) {
|
||||||
|
expandedSpan.serviceSpans.forEach((serviceSpan) => {
|
||||||
|
result.push({
|
||||||
|
id: serviceSpan.data.span_id,
|
||||||
|
type: SPAN_TYPE_SERVICE,
|
||||||
|
spanName: serviceSpan.data.name,
|
||||||
|
serviceName: serviceSpan.data['service.name'],
|
||||||
|
duration: formatDuration(serviceSpan.data.duration_nano),
|
||||||
|
timestamp: serviceSpan.timestamp,
|
||||||
|
statusCode: serviceSpan.data.response_status_code,
|
||||||
|
httpMethod: serviceSpan.data.http_method,
|
||||||
|
spanId: serviceSpan.data.span_id,
|
||||||
|
originalData: serviceSpan,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}, [data.entrySpans, expandedEntrySpans]);
|
||||||
|
|
||||||
|
const handleRowClick = useCallback(
|
||||||
|
(row: Row<TableRowData>) => {
|
||||||
|
const { original } = row;
|
||||||
|
if (original.type === SPAN_TYPE_ENTRY) {
|
||||||
|
handleEntrySpanClick(original.originalData as ServiceEntrySpan);
|
||||||
|
} else if (original.type === SPAN_TYPE_SERVICE) {
|
||||||
|
handleSpanClick(original.originalData as SpanDataRow);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[handleEntrySpanClick, handleSpanClick],
|
||||||
|
);
|
||||||
|
|
||||||
|
const args = {
|
||||||
|
columns,
|
||||||
|
tableId: 'span-list-table',
|
||||||
|
enableSorting: false,
|
||||||
|
enableFiltering: false,
|
||||||
|
enableGlobalFilter: false,
|
||||||
|
enableColumnReordering: false,
|
||||||
|
enableColumnResizing: false,
|
||||||
|
enableColumnPinning: false,
|
||||||
|
enableRowSelection: false,
|
||||||
|
enablePagination: false,
|
||||||
|
showHeaders: true,
|
||||||
|
defaultColumnWidth: 150,
|
||||||
|
minColumnWidth: 80,
|
||||||
|
maxColumnWidth: 300,
|
||||||
|
enableVirtualization: false,
|
||||||
|
fixedHeight: 600,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="span-table">
|
||||||
|
<DataTable
|
||||||
|
columns={columns}
|
||||||
|
tableId={args.tableId}
|
||||||
|
enableSorting={args.enableSorting}
|
||||||
|
enableFiltering={args.enableFiltering}
|
||||||
|
enableGlobalFilter={args.enableGlobalFilter}
|
||||||
|
enableColumnReordering={args.enableColumnReordering}
|
||||||
|
enableColumnResizing={args.enableColumnResizing}
|
||||||
|
enableColumnPinning={args.enableColumnPinning}
|
||||||
|
enableRowSelection={args.enableRowSelection}
|
||||||
|
enablePagination={args.enablePagination}
|
||||||
|
showHeaders={args.showHeaders}
|
||||||
|
defaultColumnWidth={args.defaultColumnWidth}
|
||||||
|
minColumnWidth={args.minColumnWidth}
|
||||||
|
maxColumnWidth={args.maxColumnWidth}
|
||||||
|
enableVirtualization={args.enableVirtualization}
|
||||||
|
fixedHeight={args.fixedHeight}
|
||||||
|
data={flattenedData}
|
||||||
|
onRowClick={handleRowClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
SpanTable.defaultProps = {
|
||||||
|
traceId: undefined,
|
||||||
|
setSelectedSpan: undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SpanTable;
|
||||||
96
frontend/src/container/SpanList/mockData.ts
Normal file
96
frontend/src/container/SpanList/mockData.ts
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
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',
|
||||||
|
},
|
||||||
|
];
|
||||||
47
frontend/src/container/SpanList/types.ts
Normal file
47
frontend/src/container/SpanList/types.ts
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
export interface SpanData {
|
||||||
|
duration_nano: number;
|
||||||
|
http_method: string;
|
||||||
|
name: string;
|
||||||
|
response_status_code: string;
|
||||||
|
'service.name': string;
|
||||||
|
span_id: string;
|
||||||
|
timestamp: string;
|
||||||
|
trace_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpanDataRow {
|
||||||
|
data: SpanData;
|
||||||
|
timestamp: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse {
|
||||||
|
status: string;
|
||||||
|
data: {
|
||||||
|
type: string;
|
||||||
|
meta: {
|
||||||
|
rowsScanned: number;
|
||||||
|
bytesScanned: number;
|
||||||
|
durationMs: number;
|
||||||
|
};
|
||||||
|
data: {
|
||||||
|
results: Array<{
|
||||||
|
queryName: string;
|
||||||
|
nextCursor: string;
|
||||||
|
rows: SpanDataRow[];
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ServiceEntrySpan {
|
||||||
|
spanData: SpanDataRow;
|
||||||
|
serviceName: string;
|
||||||
|
isExpanded?: boolean;
|
||||||
|
serviceSpans?: SpanDataRow[]; // All spans for this service when expanded
|
||||||
|
isLoadingServiceSpans?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HierarchicalSpanData {
|
||||||
|
entrySpans: ServiceEntrySpan[];
|
||||||
|
totalTraceTime: number;
|
||||||
|
}
|
||||||
89
frontend/src/container/SpanList/utils.ts
Normal file
89
frontend/src/container/SpanList/utils.ts
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
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[],
|
||||||
|
): HierarchicalSpanData {
|
||||||
|
let totalTraceTime = 0;
|
||||||
|
|
||||||
|
// Calculate total trace time from all entry spans
|
||||||
|
entrySpans.forEach((span) => {
|
||||||
|
totalTraceTime += span.data.duration_nano;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Transform entry spans to ServiceEntrySpan structure
|
||||||
|
const entrySpansList: ServiceEntrySpan[] = entrySpans.map((span) => ({
|
||||||
|
spanData: span,
|
||||||
|
serviceName: span.data['service.name'],
|
||||||
|
isExpanded: false,
|
||||||
|
serviceSpans: undefined,
|
||||||
|
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(
|
||||||
|
traceId: string,
|
||||||
|
serviceName: string,
|
||||||
|
): Promise<SpanDataRow[]> {
|
||||||
|
// 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,
|
||||||
|
},
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -1,9 +1,11 @@
|
|||||||
import './TraceDetailV2.styles.scss';
|
import './TraceDetailV2.styles.scss';
|
||||||
|
|
||||||
|
import { UnorderedListOutlined } from '@ant-design/icons';
|
||||||
import { Button, Tabs } from 'antd';
|
import { Button, Tabs } from 'antd';
|
||||||
import FlamegraphImg from 'assets/TraceDetail/Flamegraph';
|
import FlamegraphImg from 'assets/TraceDetail/Flamegraph';
|
||||||
import TraceFlamegraph from 'container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph';
|
import TraceFlamegraph from 'container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph';
|
||||||
import SpanDetailsDrawer from 'container/SpanDetailsDrawer/SpanDetailsDrawer';
|
import SpanDetailsDrawer from 'container/SpanDetailsDrawer/SpanDetailsDrawer';
|
||||||
|
import SpanList from 'container/SpanList/SpanList';
|
||||||
import TraceMetadata from 'container/TraceMetadata/TraceMetadata';
|
import TraceMetadata from 'container/TraceMetadata/TraceMetadata';
|
||||||
import TraceWaterfall, {
|
import TraceWaterfall, {
|
||||||
IInterestedSpan,
|
IInterestedSpan,
|
||||||
@ -118,6 +120,19 @@ function TraceDetailsV2(): JSX.Element {
|
|||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
label: (
|
||||||
|
<Button
|
||||||
|
type="text"
|
||||||
|
icon={<UnorderedListOutlined />}
|
||||||
|
className="flamegraph-waterfall-toggle"
|
||||||
|
>
|
||||||
|
Span list
|
||||||
|
</Button>
|
||||||
|
),
|
||||||
|
key: 'span-list',
|
||||||
|
children: <SpanList traceId={traceId} setSelectedSpan={setSelectedSpan} />,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -4247,7 +4247,7 @@
|
|||||||
tailwind-merge "^2.5.2"
|
tailwind-merge "^2.5.2"
|
||||||
tailwindcss-animate "^1.0.7"
|
tailwindcss-animate "^1.0.7"
|
||||||
|
|
||||||
"@signozhq/button@^0.0.2":
|
"@signozhq/button@0.0.2", "@signozhq/button@^0.0.2":
|
||||||
version "0.0.2"
|
version "0.0.2"
|
||||||
resolved "https://registry.yarnpkg.com/@signozhq/button/-/button-0.0.2.tgz#c13edef1e735134b784a41f874b60a14bc16993f"
|
resolved "https://registry.yarnpkg.com/@signozhq/button/-/button-0.0.2.tgz#c13edef1e735134b784a41f874b60a14bc16993f"
|
||||||
integrity sha512-434/gbTykC00LrnzFPp7c33QPWZkf9n+8+SToLZFTB0rzcaS/xoB4b7QKhvk+8xLCj4zpw6BxfeRAL+gSoOUJw==
|
integrity sha512-434/gbTykC00LrnzFPp7c33QPWZkf9n+8+SToLZFTB0rzcaS/xoB4b7QKhvk+8xLCj4zpw6BxfeRAL+gSoOUJw==
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user