mirror of
https://git.mirrors.martin98.com/https://github.com/SigNoz/signoz
synced 2025-08-12 21:58:59 +08:00
feat: missing spans handling by returning a forest of trees (#1365)
* feat: spanToTree 2.0 * feat: spanToTree EPIFI data * feat: missing spans multiple trees * chore: migrated to popoverss Co-authored-by: Palash <palashgdev@gmail.com>
This commit is contained in:
parent
da368ab5e8
commit
80c80b2180
@ -39,6 +39,7 @@ function Trace(props: TraceProps): JSX.Element {
|
||||
isExpandAll,
|
||||
intervalUnit,
|
||||
children,
|
||||
isMissing,
|
||||
} = props;
|
||||
|
||||
const { isDarkMode } = useThemeMode();
|
||||
@ -125,7 +126,7 @@ function Trace(props: TraceProps): JSX.Element {
|
||||
isDarkMode={isDarkMode}
|
||||
/>
|
||||
|
||||
<CardContainer onClick={onClick}>
|
||||
<CardContainer isMissing={isMissing} onClick={onClick}>
|
||||
<StyledCol flex={`${panelWidth}px`} styledclass={[styles.overFlowHidden]}>
|
||||
<StyledRow styledclass={[styles.flexNoWrap]}>
|
||||
<Col>
|
||||
@ -174,6 +175,7 @@ function Trace(props: TraceProps): JSX.Element {
|
||||
activeSpanPath={activeSpanPath}
|
||||
isExpandAll={isExpandAll}
|
||||
intervalUnit={intervalUnit}
|
||||
isMissing={child.isMissing}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
@ -182,6 +184,10 @@ function Trace(props: TraceProps): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
Trace.defaultProps = {
|
||||
isMissing: false,
|
||||
};
|
||||
|
||||
interface ITraceGlobal {
|
||||
globalSpread: ITraceMetaData['spread'];
|
||||
globalStart: ITraceMetaData['globalStart'];
|
||||
@ -196,6 +202,7 @@ interface TraceProps extends ITraceTree, ITraceGlobal {
|
||||
activeSpanPath: string[];
|
||||
isExpandAll: boolean;
|
||||
intervalUnit: IIntervalUnit;
|
||||
isMissing?: boolean;
|
||||
}
|
||||
|
||||
export default Trace;
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { volcano } from '@ant-design/colors';
|
||||
import styled, {
|
||||
css,
|
||||
DefaultTheme,
|
||||
@ -15,7 +16,6 @@ export const Wrapper = styled.ul<Props>`
|
||||
padding-top: 0.5rem;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
ul {
|
||||
border-left: ${({ isOnlyChild }): StyledCSS =>
|
||||
isOnlyChild && 'none'} !important;
|
||||
@ -36,10 +36,13 @@ export const Wrapper = styled.ul<Props>`
|
||||
}
|
||||
`;
|
||||
|
||||
export const CardContainer = styled.li`
|
||||
export const CardContainer = styled.li<{ isMissing?: boolean }>`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border-radius: 0.25rem;
|
||||
${({ isMissing }): string =>
|
||||
isMissing ? `border: 1px dashed ${volcano[6]};` : ''}
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
|
@ -3,7 +3,7 @@ import { IIntervalUnit } from 'container/TraceDetail/utils';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
||||
|
||||
import { CardContainer, CardWrapper, CollapseButton, Wrapper } from './styles';
|
||||
import { CardContainer, CardWrapper, CollapseButton } from './styles';
|
||||
import Trace from './Trace';
|
||||
import { getSpanPath } from './utils';
|
||||
|
||||
@ -36,35 +36,33 @@ function GanttChart(props: GanttChartProps): JSX.Element {
|
||||
setIsExpandAll((prev) => !prev);
|
||||
};
|
||||
return (
|
||||
<Wrapper>
|
||||
<CardContainer>
|
||||
<CollapseButton
|
||||
onClick={handleCollapse}
|
||||
title={isExpandAll ? 'Collapse All' : 'Expand All'}
|
||||
>
|
||||
{isExpandAll ? <MinusSquareOutlined /> : <PlusSquareOutlined />}
|
||||
</CollapseButton>
|
||||
<CardWrapper>
|
||||
<Trace
|
||||
activeHoverId={activeHoverId}
|
||||
activeSpanPath={activeSpanPath}
|
||||
setActiveHoverId={setActiveHoverId}
|
||||
key={data.id}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...{
|
||||
...data,
|
||||
globalSpread,
|
||||
globalStart,
|
||||
setActiveSelectedId,
|
||||
activeSelectedId,
|
||||
}}
|
||||
level={0}
|
||||
isExpandAll={isExpandAll}
|
||||
intervalUnit={intervalUnit}
|
||||
/>
|
||||
</CardWrapper>
|
||||
</CardContainer>
|
||||
</Wrapper>
|
||||
<CardContainer>
|
||||
<CollapseButton
|
||||
onClick={handleCollapse}
|
||||
title={isExpandAll ? 'Collapse All' : 'Expand All'}
|
||||
>
|
||||
{isExpandAll ? <MinusSquareOutlined /> : <PlusSquareOutlined />}
|
||||
</CollapseButton>
|
||||
<CardWrapper>
|
||||
<Trace
|
||||
activeHoverId={activeHoverId}
|
||||
activeSpanPath={activeSpanPath}
|
||||
setActiveHoverId={setActiveHoverId}
|
||||
key={data.id}
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...{
|
||||
...data,
|
||||
globalSpread,
|
||||
globalStart,
|
||||
setActiveSelectedId,
|
||||
activeSelectedId,
|
||||
}}
|
||||
level={0}
|
||||
isExpandAll={isExpandAll}
|
||||
intervalUnit={intervalUnit}
|
||||
/>
|
||||
</CardWrapper>
|
||||
</CardContainer>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -38,6 +38,7 @@ export const CardWrapper = styled.div`
|
||||
export const CardContainer = styled.li`
|
||||
display: flex;
|
||||
width: 100%;
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const CollapseButton = styled.div`
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
||||
import { set } from 'lodash-es';
|
||||
import { ITraceForest, ITraceTree } from 'types/api/trace/getTraceItem';
|
||||
|
||||
interface GetTraceMetaData {
|
||||
globalStart: number;
|
||||
@ -65,25 +66,48 @@ export function getTopLeftFromBody(
|
||||
|
||||
export const getNodeById = (
|
||||
searchingId: string,
|
||||
treeData: ITraceTree,
|
||||
): ITraceTree | undefined => {
|
||||
let foundNode: ITraceTree | undefined;
|
||||
const traverse = (treeNode: ITraceTree, level = 0): void => {
|
||||
treesData: ITraceForest | undefined,
|
||||
): ITraceForest => {
|
||||
const newtreeData: ITraceForest = {} as ITraceForest;
|
||||
|
||||
const traverse = (
|
||||
treeNode: ITraceTree,
|
||||
setCallBack: (arg0: ITraceTree) => void,
|
||||
level = 0,
|
||||
): void => {
|
||||
if (!treeNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (searchingId === treeNode.id) {
|
||||
foundNode = treeNode;
|
||||
setCallBack(treeNode);
|
||||
}
|
||||
|
||||
treeNode.children.forEach((childNode) => {
|
||||
traverse(childNode, level + 1);
|
||||
traverse(childNode, setCallBack, level + 1);
|
||||
});
|
||||
};
|
||||
traverse(treeData, 1);
|
||||
|
||||
return foundNode;
|
||||
const spanTreeSetCallback = (
|
||||
path: (keyof ITraceForest)[],
|
||||
value: ITraceTree,
|
||||
): ITraceForest => set(newtreeData, path, [value]);
|
||||
|
||||
if (treesData?.spanTree)
|
||||
treesData.spanTree.forEach((tree) => {
|
||||
traverse(tree, (value) => spanTreeSetCallback(['spanTree'], value), 1);
|
||||
});
|
||||
|
||||
if (treesData?.missingSpanTree)
|
||||
treesData.missingSpanTree.forEach((tree) => {
|
||||
traverse(
|
||||
tree,
|
||||
(value) => spanTreeSetCallback(['missingSpanTree'], value),
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
return newtreeData;
|
||||
};
|
||||
|
||||
const getSpanWithoutChildren = (
|
||||
|
42
frontend/src/container/TraceDetail/Missingtrace.tsx
Normal file
42
frontend/src/container/TraceDetail/Missingtrace.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { volcano } from '@ant-design/colors';
|
||||
import { InfoCircleOutlined } from '@ant-design/icons';
|
||||
import { Popover } from 'antd';
|
||||
import React from 'react';
|
||||
|
||||
function PopOverContent(): JSX.Element {
|
||||
return (
|
||||
<div>
|
||||
More details on missing spans{' '}
|
||||
<a
|
||||
href="https://signoz.io/docs/userguide/traces/#missing-spans"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function MissingSpansMessage(): JSX.Element {
|
||||
return (
|
||||
<Popover content={PopOverContent} trigger="hover" placement="bottom">
|
||||
<div
|
||||
style={{
|
||||
textAlign: 'center',
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
margin: '1rem 0',
|
||||
}}
|
||||
>
|
||||
<InfoCircleOutlined
|
||||
style={{ color: volcano[6], fontSize: '1.4rem', marginRight: '0.3rem' }}
|
||||
/>{' '}
|
||||
This trace has missing spans
|
||||
</div>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
|
||||
export default MissingSpansMessage;
|
@ -18,6 +18,7 @@ const { TabPane } = Tabs;
|
||||
|
||||
function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
|
||||
const { tree } = props;
|
||||
|
||||
const { isDarkMode } = useThemeMode();
|
||||
|
||||
const OverLayComponentName = useMemo(() => tree?.name, [tree?.name]);
|
||||
|
@ -17,15 +17,23 @@ import dayjs from 'dayjs';
|
||||
import useUrlQuery from 'hooks/useUrlQuery';
|
||||
import { spanServiceNameToColorMapping } from 'lib/getRandomColor';
|
||||
import history from 'lib/history';
|
||||
import { map } from 'lodash-es';
|
||||
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { ITraceTree, PayloadProps } from 'types/api/trace/getTraceItem';
|
||||
import { ITraceForest, PayloadProps } from 'types/api/trace/getTraceItem';
|
||||
import { getSpanTreeMetadata } from 'utils/getSpanTreeMetadata';
|
||||
import { spanToTreeUtil } from 'utils/spanToTree';
|
||||
|
||||
import MissingSpansMessage from './Missingtrace';
|
||||
import SelectedSpanDetails from './SelectedSpanDetails';
|
||||
import * as styles from './styles';
|
||||
import { getSortedData, IIntervalUnit, INTERVAL_UNITS } from './utils';
|
||||
import { FlameGraphMissingSpansContainer, GanttChartWrapper } from './styles';
|
||||
import {
|
||||
getSortedData,
|
||||
getTreeLevelsCount,
|
||||
IIntervalUnit,
|
||||
INTERVAL_UNITS,
|
||||
} from './utils';
|
||||
|
||||
function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
||||
const spanServiceColors = useMemo(
|
||||
@ -43,17 +51,23 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
||||
const [activeHoverId, setActiveHoverId] = useState<string>('');
|
||||
const [activeSelectedId, setActiveSelectedId] = useState<string>(spanId || '');
|
||||
|
||||
const [treeData, setTreeData] = useState<ITraceTree>(
|
||||
const [treesData, setTreesData] = useState<ITraceForest>(
|
||||
spanToTreeUtil(response[0].events),
|
||||
);
|
||||
|
||||
const { treeData: tree, ...traceMetaData } = useMemo(() => {
|
||||
const tree = getSortedData(treeData);
|
||||
const { treesData: tree, ...traceMetaData } = useMemo(() => {
|
||||
const sortedTreesData: ITraceForest = {
|
||||
spanTree: map(treesData.spanTree, (tree) => getSortedData(tree)),
|
||||
missingSpanTree: map(
|
||||
treesData.missingSpanTree,
|
||||
(tree) => getSortedData(tree) || [],
|
||||
),
|
||||
};
|
||||
// Note: Handle undefined
|
||||
/*eslint-disable */
|
||||
return getSpanTreeMetadata(tree as ITraceTree, spanServiceColors);
|
||||
return getSpanTreeMetadata(sortedTreesData, spanServiceColors);
|
||||
/* eslint-enable */
|
||||
}, [treeData, spanServiceColors]);
|
||||
}, [treesData, spanServiceColors]);
|
||||
|
||||
const [globalTraceMetadata] = useState<ITraceMetaData>({
|
||||
...traceMetaData,
|
||||
@ -69,24 +83,34 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
||||
}, [activeSelectedId]);
|
||||
|
||||
const getSelectedNode = useMemo(() => {
|
||||
return getNodeById(activeSelectedId, treeData);
|
||||
}, [activeSelectedId, treeData]);
|
||||
return getNodeById(activeSelectedId, treesData);
|
||||
}, [activeSelectedId, treesData]);
|
||||
|
||||
// const onSearchHandler = (value: string) => {
|
||||
// setSearchSpanString(value);
|
||||
// setTreeData(spanToTreeUtil(response[0].events));
|
||||
// };
|
||||
|
||||
const onFocusSelectedSpanHandler = (): void => {
|
||||
const treeNode = getNodeById(activeSelectedId, tree);
|
||||
|
||||
if (treeNode) {
|
||||
setTreeData(treeNode);
|
||||
setTreesData(treeNode);
|
||||
}
|
||||
};
|
||||
|
||||
const onResetHandler = (): void => {
|
||||
setTreeData(spanToTreeUtil(response[0].events));
|
||||
setTreesData(spanToTreeUtil(response[0].events));
|
||||
};
|
||||
|
||||
const hasMissingSpans = useMemo(
|
||||
(): boolean =>
|
||||
tree.missingSpanTree &&
|
||||
Array.isArray(tree.missingSpanTree) &&
|
||||
tree.missingSpanTree.length > 0,
|
||||
[tree],
|
||||
);
|
||||
|
||||
return (
|
||||
<StyledRow styledclass={[StyledStyles.Flex({ flex: 1 })]}>
|
||||
<StyledCol flex="auto" styledclass={styles.leftContainer}>
|
||||
@ -101,16 +125,45 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
||||
<StyledTypography.Text styledclass={[styles.removeMargin]}>
|
||||
{traceMetaData.totalSpans} Span
|
||||
</StyledTypography.Text>
|
||||
{hasMissingSpans && <MissingSpansMessage />}
|
||||
</StyledCol>
|
||||
<Col flex="auto">
|
||||
<TraceFlameGraph
|
||||
treeData={tree}
|
||||
traceMetaData={traceMetaData}
|
||||
hoveredSpanId={activeHoverId}
|
||||
selectedSpanId={activeSelectedId}
|
||||
onSpanHover={setActiveHoverId}
|
||||
onSpanSelect={setActiveSelectedId}
|
||||
/>
|
||||
{map(tree.spanTree, (tree) => {
|
||||
return (
|
||||
<TraceFlameGraph
|
||||
key={tree as never}
|
||||
treeData={tree}
|
||||
traceMetaData={traceMetaData}
|
||||
hoveredSpanId={activeHoverId}
|
||||
selectedSpanId={activeSelectedId}
|
||||
onSpanHover={setActiveHoverId}
|
||||
onSpanSelect={setActiveSelectedId}
|
||||
missingSpanTree={false}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
||||
{hasMissingSpans && (
|
||||
<FlameGraphMissingSpansContainer>
|
||||
{map(tree.missingSpanTree, (tree) => {
|
||||
return (
|
||||
<TraceFlameGraph
|
||||
key={tree as never}
|
||||
treeData={tree}
|
||||
traceMetaData={{
|
||||
...traceMetaData,
|
||||
levels: getTreeLevelsCount(tree),
|
||||
}}
|
||||
hoveredSpanId={activeHoverId}
|
||||
selectedSpanId={activeSelectedId}
|
||||
onSpanHover={setActiveHoverId}
|
||||
onSpanSelect={setActiveSelectedId}
|
||||
missingSpanTree
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</FlameGraphMissingSpansContainer>
|
||||
)}
|
||||
</Col>
|
||||
</StyledRow>
|
||||
<StyledRow styledclass={[styles.traceDateAndTimelineContainer]}>
|
||||
@ -122,7 +175,9 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
>
|
||||
{tree && dayjs(tree.startTime).format('hh:mm:ss a MM/DD')}
|
||||
{tree &&
|
||||
traceMetaData.globalStart &&
|
||||
dayjs(traceMetaData.globalStart).format('hh:mm:ss a MM/DD')}
|
||||
</StyledCol>
|
||||
<StyledCol flex="auto" styledclass={[styles.timelineContainer]}>
|
||||
<Timeline
|
||||
@ -141,14 +196,7 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
||||
}),
|
||||
]}
|
||||
>
|
||||
<Col flex={`${SPAN_DETAILS_LEFT_COL_WIDTH}px`}>
|
||||
{/* <Search
|
||||
placeholder="Type to filter.."
|
||||
allowClear
|
||||
onSearch={onSearchHandler}
|
||||
style={{ width: 200 }}
|
||||
/> */}
|
||||
</Col>
|
||||
<Col flex={`${SPAN_DETAILS_LEFT_COL_WIDTH}px`} />
|
||||
<Col flex="auto">
|
||||
<StyledSpace styledclass={[styles.floatRight]}>
|
||||
<Button onClick={onFocusSelectedSpanHandler} icon={<FilterOutlined />}>
|
||||
@ -161,23 +209,50 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
|
||||
</Col>
|
||||
</StyledRow>
|
||||
<StyledDiv styledclass={[styles.ganttChartContainer]}>
|
||||
<GanttChart
|
||||
traceMetaData={traceMetaData}
|
||||
data={tree}
|
||||
activeSelectedId={activeSelectedId}
|
||||
activeHoverId={activeHoverId}
|
||||
setActiveHoverId={setActiveHoverId}
|
||||
setActiveSelectedId={setActiveSelectedId}
|
||||
spanId={spanId || ''}
|
||||
intervalUnit={intervalUnit}
|
||||
/>
|
||||
<GanttChartWrapper>
|
||||
{map([...tree.spanTree, ...tree.missingSpanTree], (tree) => (
|
||||
<GanttChart
|
||||
key={tree as never}
|
||||
traceMetaData={traceMetaData}
|
||||
data={tree}
|
||||
activeSelectedId={activeSelectedId}
|
||||
activeHoverId={activeHoverId}
|
||||
setActiveHoverId={setActiveHoverId}
|
||||
setActiveSelectedId={setActiveSelectedId}
|
||||
spanId={spanId || ''}
|
||||
intervalUnit={intervalUnit}
|
||||
/>
|
||||
))}
|
||||
{/* {map(tree.missingSpanTree, (tree) => (
|
||||
<GanttChart
|
||||
key={tree as never}
|
||||
traceMetaData={traceMetaData}
|
||||
data={tree}
|
||||
activeSelectedId={activeSelectedId}
|
||||
activeHoverId={activeHoverId}
|
||||
setActiveHoverId={setActiveHoverId}
|
||||
setActiveSelectedId={setActiveSelectedId}
|
||||
spanId={spanId || ''}
|
||||
intervalUnit={intervalUnit}
|
||||
/>
|
||||
))} */}
|
||||
</GanttChartWrapper>
|
||||
</StyledDiv>
|
||||
</StyledCol>
|
||||
<Col>
|
||||
<StyledDivider styledclass={[styles.verticalSeparator]} type="vertical" />
|
||||
</Col>
|
||||
<StyledCol md={5} sm={5} styledclass={[styles.selectedSpanDetailContainer]}>
|
||||
<SelectedSpanDetails tree={getSelectedNode} />
|
||||
<SelectedSpanDetails
|
||||
tree={[
|
||||
...(getSelectedNode.spanTree ? getSelectedNode.spanTree : []),
|
||||
...(getSelectedNode.missingSpanTree
|
||||
? getSelectedNode.missingSpanTree
|
||||
: []),
|
||||
]
|
||||
.filter(Boolean)
|
||||
.find((tree) => tree)}
|
||||
/>
|
||||
</StyledCol>
|
||||
</StyledRow>
|
||||
);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { css } from 'styled-components';
|
||||
import { volcano } from '@ant-design/colors';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
/**
|
||||
* Styles for the left container. Containers flamegraph, timeline and gantt chart
|
||||
@ -76,3 +77,38 @@ export const floatRight = css`
|
||||
export const removeMargin = css`
|
||||
margin: 0;
|
||||
`;
|
||||
|
||||
export const GanttChartWrapper = styled.ul`
|
||||
padding-left: 0;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
border-left: 1px solid #434343;
|
||||
padding-left: 1rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ul li {
|
||||
position: relative;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
left: -1rem;
|
||||
top: 10px;
|
||||
content: '';
|
||||
height: 1px;
|
||||
width: 1rem;
|
||||
background-color: #434343;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const FlameGraphMissingSpansContainer = styled.div`
|
||||
border: 1px dashed ${volcano[6]};
|
||||
padding: 0.5rem 0;
|
||||
margin-top: 1rem;
|
||||
border-radius: 0.25rem;
|
||||
`;
|
||||
|
@ -62,7 +62,7 @@ export const convertTimeToRelevantUnit = (
|
||||
return relevantTime;
|
||||
};
|
||||
|
||||
export const getSortedData = (treeData: ITraceTree): undefined | ITraceTree => {
|
||||
export const getSortedData = (treeData: ITraceTree): ITraceTree => {
|
||||
const traverse = (treeNode: ITraceTree, level = 0): void => {
|
||||
if (!treeNode) {
|
||||
return;
|
||||
@ -80,3 +80,21 @@ export const getSortedData = (treeData: ITraceTree): undefined | ITraceTree => {
|
||||
|
||||
return treeData;
|
||||
};
|
||||
|
||||
export const getTreeLevelsCount = (tree: ITraceTree): number => {
|
||||
let levels = 0;
|
||||
const traverse = (treeNode: ITraceTree, level: number): void => {
|
||||
if (!treeNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
levels = Math.max(level, levels);
|
||||
|
||||
treeNode.children.forEach((childNode) => {
|
||||
traverse(childNode, level + 1);
|
||||
});
|
||||
};
|
||||
traverse(tree, levels);
|
||||
|
||||
return levels;
|
||||
};
|
||||
|
@ -28,6 +28,7 @@ test('loads and displays greeting', () => {
|
||||
spread: 0,
|
||||
totalSpans: 0,
|
||||
},
|
||||
missingSpanTree: false,
|
||||
treeData: {
|
||||
children: [],
|
||||
id: '',
|
||||
|
@ -93,8 +93,9 @@ function TraceFlameGraph(props: {
|
||||
onSpanSelect: SpanItemProps['onSpanSelect'];
|
||||
hoveredSpanId: string;
|
||||
selectedSpanId: string;
|
||||
missingSpanTree: boolean;
|
||||
}): JSX.Element {
|
||||
const { treeData, traceMetaData, onSpanHover } = props;
|
||||
const { treeData, traceMetaData, onSpanHover, missingSpanTree } = props;
|
||||
|
||||
if (!treeData || treeData.id === 'empty' || !traceMetaData) {
|
||||
return <div />;
|
||||
@ -140,6 +141,7 @@ function TraceFlameGraph(props: {
|
||||
hoveredSpanId={hoveredSpanId}
|
||||
selectedSpanId={selectedSpanId}
|
||||
/>
|
||||
|
||||
{spanData.children.map((childData) => (
|
||||
<RenderSpanRecursive
|
||||
level={level + 1}
|
||||
@ -164,7 +166,7 @@ function TraceFlameGraph(props: {
|
||||
onSpanSelect={onSpanSelect}
|
||||
hoveredSpanId={hoveredSpanId}
|
||||
selectedSpanId={selectedSpanId}
|
||||
level={0}
|
||||
level={missingSpanTree ? -1 : 0}
|
||||
parentLeftOffset={0}
|
||||
/>
|
||||
</TraceFlameGraphContainer>
|
||||
|
@ -21,7 +21,8 @@ export type Span = [
|
||||
string | string[],
|
||||
string | string[],
|
||||
string | string[],
|
||||
ITraceTree[],
|
||||
Record<string, unknown>[],
|
||||
boolean,
|
||||
];
|
||||
|
||||
export interface ITraceTree {
|
||||
@ -37,6 +38,10 @@ export interface ITraceTree {
|
||||
serviceColour: string;
|
||||
hasError?: boolean;
|
||||
event?: ITraceEvents[];
|
||||
isMissing?: boolean;
|
||||
// For internal use
|
||||
isProcessed?: boolean;
|
||||
references?: Record<string, string>[];
|
||||
}
|
||||
|
||||
export interface ITraceTag {
|
||||
@ -48,3 +53,8 @@ interface ITraceEvents {
|
||||
attributeMap: { event: string; [key: string]: string };
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export interface ITraceForest {
|
||||
spanTree: ITraceTree[];
|
||||
missingSpanTree: ITraceTree[];
|
||||
}
|
||||
|
@ -1,28 +1,31 @@
|
||||
/* eslint-disable no-param-reassign */
|
||||
import { errorColor } from 'lib/getRandomColor';
|
||||
import { ITraceTree } from 'types/api/trace/getTraceItem';
|
||||
import { ITraceForest, ITraceTree } from 'types/api/trace/getTraceItem';
|
||||
/**
|
||||
* Traverses the Span Tree data and returns the relevant meta data.
|
||||
* Metadata includes globalStart, globalEnd,
|
||||
*/
|
||||
export const getSpanTreeMetadata = (
|
||||
treeData: ITraceTree,
|
||||
treesData: ITraceForest,
|
||||
spanServiceColours: { [key: string]: string },
|
||||
): GetSpanTreeMetaData => {
|
||||
let globalStart = Number.POSITIVE_INFINITY;
|
||||
let globalEnd = Number.NEGATIVE_INFINITY;
|
||||
let totalSpans = 0;
|
||||
let levels = 1;
|
||||
|
||||
const traverse = (treeNode: ITraceTree, level = 0): void => {
|
||||
if (!treeNode) {
|
||||
return;
|
||||
}
|
||||
totalSpans += 1;
|
||||
levels = Math.max(levels, level);
|
||||
const { startTime } = treeNode;
|
||||
const endTime = startTime + treeNode.value / 1e6;
|
||||
globalStart = Math.min(globalStart, startTime);
|
||||
globalEnd = Math.max(globalEnd, endTime);
|
||||
const { startTime, value } = treeNode;
|
||||
if (startTime !== null && value !== null) {
|
||||
const endTime = startTime + value / 1e6;
|
||||
globalStart = Math.min(globalStart, startTime);
|
||||
globalEnd = Math.max(globalEnd, endTime);
|
||||
}
|
||||
if (treeNode.hasError) {
|
||||
treeNode.serviceColour = errorColor;
|
||||
} else treeNode.serviceColour = spanServiceColours[treeNode.serviceName];
|
||||
@ -30,7 +33,12 @@ export const getSpanTreeMetadata = (
|
||||
traverse(childNode, level + 1);
|
||||
});
|
||||
};
|
||||
traverse(treeData, 1);
|
||||
treesData.spanTree.forEach((treeData) => {
|
||||
traverse(treeData, 1);
|
||||
});
|
||||
treesData.missingSpanTree.forEach((treeData) => {
|
||||
traverse(treeData, 1);
|
||||
});
|
||||
|
||||
return {
|
||||
globalStart,
|
||||
@ -38,7 +46,7 @@ export const getSpanTreeMetadata = (
|
||||
spread: globalEnd - globalStart,
|
||||
totalSpans,
|
||||
levels,
|
||||
treeData,
|
||||
treesData,
|
||||
};
|
||||
};
|
||||
|
||||
@ -48,5 +56,5 @@ interface GetSpanTreeMetaData {
|
||||
spread: number;
|
||||
totalSpans: number;
|
||||
levels: number;
|
||||
treeData: ITraceTree;
|
||||
treesData: ITraceForest;
|
||||
}
|
||||
|
@ -1,137 +1,125 @@
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
|
||||
/* eslint-disable no-restricted-syntax */
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { ITraceTree, Span } from 'types/api/trace/getTraceItem';
|
||||
import { ITraceForest, ITraceTree, Span } from 'types/api/trace/getTraceItem';
|
||||
|
||||
export const spanToTreeUtil = (originalList: Span[]): ITraceTree => {
|
||||
// Initializing tree. What should be returned is trace is empty? We should have better error handling
|
||||
let tree: ITraceTree = {
|
||||
id: 'empty',
|
||||
name: 'default',
|
||||
value: 0,
|
||||
time: 0,
|
||||
startTime: 0,
|
||||
tags: [],
|
||||
children: [],
|
||||
serviceColour: '',
|
||||
serviceName: '',
|
||||
};
|
||||
|
||||
const spanlist = cloneDeep(originalList);
|
||||
|
||||
// let spans :spanItem[]= trace.spans;
|
||||
|
||||
if (spanlist) {
|
||||
// Create a dict with spanIDs as keys
|
||||
// PNOTE
|
||||
// Can we now assign different strings as id - Yes
|
||||
// https://stackoverflow.com/questions/15877362/declare-and-initialize-a-dictionary-in-typescript
|
||||
|
||||
// May1
|
||||
// https://stackoverflow.com/questions/13315131/enforcing-the-type-of-the-indexed-members-of-a-typescript-object
|
||||
|
||||
const mapped_array: { [id: string]: Span } = {};
|
||||
const originalListArray: { [id: string]: Span } = {};
|
||||
|
||||
for (let i = 0; i < spanlist.length; i++) {
|
||||
originalListArray[spanlist[i][1]] = originalList[i];
|
||||
|
||||
mapped_array[spanlist[i][1]] = spanlist[i];
|
||||
mapped_array[spanlist[i][1]][10] = []; // initialising the 10th element in the Span data structure which is array
|
||||
// of type ITraceTree
|
||||
// console.log('IDs while creating mapped array')
|
||||
// console.log(`SpanID is ${spanlist[i][1]}\n`);
|
||||
}
|
||||
|
||||
// console.log(`In SpanTreeUtil: mapped_arrayis ${mapped_array}`);
|
||||
|
||||
for (const id in mapped_array) {
|
||||
const child_span = mapped_array[id];
|
||||
|
||||
// mapping tags to new structure
|
||||
const tags_temp = [];
|
||||
if (child_span[7] !== null && child_span[8] !== null) {
|
||||
if (
|
||||
typeof child_span[7] === 'string' &&
|
||||
typeof child_span[8] === 'string'
|
||||
) {
|
||||
tags_temp.push({ key: child_span[7], value: child_span[8] });
|
||||
} else if (child_span[7].length > 0 && child_span[8].length > 0) {
|
||||
for (let j = 0; j < child_span[7].length; j++) {
|
||||
tags_temp.push({ key: child_span[7][j], value: child_span[8][j] });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const push_object: ITraceTree = {
|
||||
id: child_span[1],
|
||||
name: child_span[4],
|
||||
value: parseInt(child_span[6]),
|
||||
time: parseInt(child_span[6]),
|
||||
startTime: child_span[0],
|
||||
tags: tags_temp,
|
||||
children: mapped_array[id][10],
|
||||
serviceName: child_span[3],
|
||||
hasError: !!child_span[11],
|
||||
serviceColour: '',
|
||||
event: originalListArray[id][10].map((e) => {
|
||||
return JSON.parse(decodeURIComponent(e || '{}')) || {};
|
||||
}),
|
||||
};
|
||||
|
||||
const referencesArr = mapped_array[id][9];
|
||||
let refArray = [];
|
||||
if (typeof referencesArr === 'string') {
|
||||
refArray.push(referencesArr);
|
||||
} else {
|
||||
refArray = referencesArr;
|
||||
}
|
||||
const references = [];
|
||||
|
||||
refArray.forEach((element) => {
|
||||
element = element
|
||||
.replaceAll('{', '')
|
||||
.replaceAll('}', '')
|
||||
.replaceAll(' ', '');
|
||||
const arr = element.split(',');
|
||||
const refItem = { traceID: '', spanID: '', refType: '' };
|
||||
arr.forEach((obj) => {
|
||||
const arr2 = obj.split('=');
|
||||
if (arr2[0] === 'TraceId') {
|
||||
refItem.traceID = arr2[1];
|
||||
} else if (arr2[0] === 'SpanId') {
|
||||
refItem.spanID = arr2[1];
|
||||
} else if (arr2[0] === 'RefType') {
|
||||
refItem.refType = arr2[1];
|
||||
}
|
||||
});
|
||||
|
||||
references.push(refItem);
|
||||
const getSpanReferences = (
|
||||
rawReferences: string[] = [],
|
||||
): Record<string, string>[] => {
|
||||
return rawReferences.map((rawRef) => {
|
||||
const refObject: Record<string, string> = {};
|
||||
rawRef
|
||||
.replaceAll('{', '')
|
||||
.replaceAll('}', '')
|
||||
.replaceAll(' ', '')
|
||||
.split(',')
|
||||
.forEach((rawRefKeyPair) => {
|
||||
const [key, value] = rawRefKeyPair.split('=');
|
||||
refObject[key] = value;
|
||||
});
|
||||
|
||||
if (references.length !== 0 && references[0].spanID.length !== 0) {
|
||||
if (references[0].refType === 'CHILD_OF') {
|
||||
const parentID = references[0].spanID;
|
||||
// console.log(`In SpanTreeUtil: mapped_array[parentID] is ${mapped_array[parentID]}`);
|
||||
|
||||
if (typeof mapped_array[parentID] !== 'undefined') {
|
||||
// checking for undefined [10] issue
|
||||
mapped_array[parentID][10].push(push_object);
|
||||
} else {
|
||||
// console.log(
|
||||
// `In SpanTreeUtil: mapped_array[parentID] is undefined, parentID is ${parentID}`,
|
||||
// );
|
||||
// console.log(
|
||||
// `In SpanTreeUtil: mapped_array[parentID] is undefined, mapped_array[parentID] is ${mapped_array[parentID]}`,
|
||||
// );
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tree = push_object;
|
||||
}
|
||||
} // end of for loop
|
||||
} // end of if(spans)
|
||||
|
||||
return { ...tree };
|
||||
return refObject;
|
||||
});
|
||||
};
|
||||
|
||||
// This getSpanTags is migrated from the previous implementation.
|
||||
const getSpanTags = (spanData: Span): { key: string; value: string }[] => {
|
||||
const tags = [];
|
||||
if (spanData[7] !== null && spanData[8] !== null) {
|
||||
if (typeof spanData[7] === 'string' && typeof spanData[8] === 'string') {
|
||||
tags.push({ key: spanData[7], value: spanData[8] });
|
||||
} else if (spanData[7].length > 0 && spanData[8].length > 0) {
|
||||
for (let j = 0; j < spanData[7].length; j += 1) {
|
||||
tags.push({ key: spanData[7][j], value: spanData[8][j] });
|
||||
}
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
};
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export const spanToTreeUtil = (inputSpanList: Span[]): ITraceForest => {
|
||||
const spanList = cloneDeep(inputSpanList);
|
||||
const traceIdSet: Set<string> = new Set();
|
||||
const spanMap: Record<string, ITraceTree> = {};
|
||||
|
||||
const createTarceRootSpan = (
|
||||
spanReferences: Record<string, string>[],
|
||||
): void => {
|
||||
spanReferences.forEach(({ SpanId, TraceId }) => {
|
||||
traceIdSet.add(TraceId);
|
||||
if (SpanId && !spanMap[SpanId]) {
|
||||
spanMap[SpanId] = {
|
||||
id: SpanId,
|
||||
name: `Missing Span (${SpanId})`,
|
||||
children: [],
|
||||
serviceColour: '',
|
||||
serviceName: '',
|
||||
startTime: null as never,
|
||||
tags: [],
|
||||
time: null as never,
|
||||
value: null as never,
|
||||
isMissing: true,
|
||||
};
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
spanList.forEach((span) => {
|
||||
const spanReferences = getSpanReferences(span[9] as string[]);
|
||||
const spanObject = {
|
||||
id: span[1],
|
||||
name: span[4],
|
||||
value: parseInt(span[6], 10),
|
||||
time: parseInt(span[6], 10),
|
||||
startTime: span[0],
|
||||
tags: getSpanTags(span),
|
||||
children: [],
|
||||
serviceName: span[3],
|
||||
hasError: !!span[11],
|
||||
serviceColour: '',
|
||||
event: span[10].map((e) => {
|
||||
return (
|
||||
JSON.parse(decodeURIComponent((e as never) || ('{}' as never))) ||
|
||||
({} as Record<string, unknown>)
|
||||
);
|
||||
}),
|
||||
references: spanReferences,
|
||||
};
|
||||
spanMap[span[1]] = spanObject;
|
||||
});
|
||||
|
||||
for (const [, spanData] of Object.entries(spanMap)) {
|
||||
if (spanData.references) {
|
||||
createTarceRootSpan(spanData.references);
|
||||
spanData.references.forEach(({ SpanId: parentSpanId }) => {
|
||||
if (spanMap[parentSpanId]) {
|
||||
spanData.isProcessed = true;
|
||||
spanMap[parentSpanId].children.push(spanData);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const [spanId, spanData] of Object.entries(spanMap)) {
|
||||
if (spanData.isProcessed) {
|
||||
delete spanMap[spanId];
|
||||
}
|
||||
}
|
||||
|
||||
const spanTree: ITraceTree[] = [];
|
||||
const missingSpanTree: ITraceTree[] = [];
|
||||
const referencedTraceIds: string[] = Array.from(traceIdSet);
|
||||
Object.keys(spanMap).forEach((spanId) => {
|
||||
for (const traceId of referencedTraceIds) {
|
||||
if (traceId.includes(spanId)) {
|
||||
spanTree.push(spanMap[spanId]);
|
||||
} else {
|
||||
missingSpanTree.push(spanMap[spanId]);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
spanTree,
|
||||
missingSpanTree,
|
||||
};
|
||||
};
|
||||
|
Loading…
x
Reference in New Issue
Block a user