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:
Pranshu Chittora 2022-07-08 16:18:08 +05:30 committed by GitHub
parent da368ab5e8
commit 80c80b2180
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 441 additions and 227 deletions

View File

@ -39,6 +39,7 @@ function Trace(props: TraceProps): JSX.Element {
isExpandAll, isExpandAll,
intervalUnit, intervalUnit,
children, children,
isMissing,
} = props; } = props;
const { isDarkMode } = useThemeMode(); const { isDarkMode } = useThemeMode();
@ -125,7 +126,7 @@ function Trace(props: TraceProps): JSX.Element {
isDarkMode={isDarkMode} isDarkMode={isDarkMode}
/> />
<CardContainer onClick={onClick}> <CardContainer isMissing={isMissing} onClick={onClick}>
<StyledCol flex={`${panelWidth}px`} styledclass={[styles.overFlowHidden]}> <StyledCol flex={`${panelWidth}px`} styledclass={[styles.overFlowHidden]}>
<StyledRow styledclass={[styles.flexNoWrap]}> <StyledRow styledclass={[styles.flexNoWrap]}>
<Col> <Col>
@ -174,6 +175,7 @@ function Trace(props: TraceProps): JSX.Element {
activeSpanPath={activeSpanPath} activeSpanPath={activeSpanPath}
isExpandAll={isExpandAll} isExpandAll={isExpandAll}
intervalUnit={intervalUnit} intervalUnit={intervalUnit}
isMissing={child.isMissing}
/> />
))} ))}
</> </>
@ -182,6 +184,10 @@ function Trace(props: TraceProps): JSX.Element {
); );
} }
Trace.defaultProps = {
isMissing: false,
};
interface ITraceGlobal { interface ITraceGlobal {
globalSpread: ITraceMetaData['spread']; globalSpread: ITraceMetaData['spread'];
globalStart: ITraceMetaData['globalStart']; globalStart: ITraceMetaData['globalStart'];
@ -196,6 +202,7 @@ interface TraceProps extends ITraceTree, ITraceGlobal {
activeSpanPath: string[]; activeSpanPath: string[];
isExpandAll: boolean; isExpandAll: boolean;
intervalUnit: IIntervalUnit; intervalUnit: IIntervalUnit;
isMissing?: boolean;
} }
export default Trace; export default Trace;

View File

@ -1,3 +1,4 @@
import { volcano } from '@ant-design/colors';
import styled, { import styled, {
css, css,
DefaultTheme, DefaultTheme,
@ -15,7 +16,6 @@ export const Wrapper = styled.ul<Props>`
padding-top: 0.5rem; padding-top: 0.5rem;
position: relative; position: relative;
z-index: 1; z-index: 1;
ul { ul {
border-left: ${({ isOnlyChild }): StyledCSS => border-left: ${({ isOnlyChild }): StyledCSS =>
isOnlyChild && 'none'} !important; 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; display: flex;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
border-radius: 0.25rem;
${({ isMissing }): string =>
isMissing ? `border: 1px dashed ${volcano[6]};` : ''}
`; `;
interface Props { interface Props {

View File

@ -3,7 +3,7 @@ import { IIntervalUnit } from 'container/TraceDetail/utils';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { ITraceTree } from 'types/api/trace/getTraceItem'; 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 Trace from './Trace';
import { getSpanPath } from './utils'; import { getSpanPath } from './utils';
@ -36,35 +36,33 @@ function GanttChart(props: GanttChartProps): JSX.Element {
setIsExpandAll((prev) => !prev); setIsExpandAll((prev) => !prev);
}; };
return ( return (
<Wrapper> <CardContainer>
<CardContainer> <CollapseButton
<CollapseButton onClick={handleCollapse}
onClick={handleCollapse} title={isExpandAll ? 'Collapse All' : 'Expand All'}
title={isExpandAll ? 'Collapse All' : 'Expand All'} >
> {isExpandAll ? <MinusSquareOutlined /> : <PlusSquareOutlined />}
{isExpandAll ? <MinusSquareOutlined /> : <PlusSquareOutlined />} </CollapseButton>
</CollapseButton> <CardWrapper>
<CardWrapper> <Trace
<Trace activeHoverId={activeHoverId}
activeHoverId={activeHoverId} activeSpanPath={activeSpanPath}
activeSpanPath={activeSpanPath} setActiveHoverId={setActiveHoverId}
setActiveHoverId={setActiveHoverId} key={data.id}
key={data.id} // eslint-disable-next-line react/jsx-props-no-spreading
// eslint-disable-next-line react/jsx-props-no-spreading {...{
{...{ ...data,
...data, globalSpread,
globalSpread, globalStart,
globalStart, setActiveSelectedId,
setActiveSelectedId, activeSelectedId,
activeSelectedId, }}
}} level={0}
level={0} isExpandAll={isExpandAll}
isExpandAll={isExpandAll} intervalUnit={intervalUnit}
intervalUnit={intervalUnit} />
/> </CardWrapper>
</CardWrapper> </CardContainer>
</CardContainer>
</Wrapper>
); );
} }

View File

@ -38,6 +38,7 @@ export const CardWrapper = styled.div`
export const CardContainer = styled.li` export const CardContainer = styled.li`
display: flex; display: flex;
width: 100%; width: 100%;
position: relative;
`; `;
export const CollapseButton = styled.div` export const CollapseButton = styled.div`

View File

@ -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 { interface GetTraceMetaData {
globalStart: number; globalStart: number;
@ -65,25 +66,48 @@ export function getTopLeftFromBody(
export const getNodeById = ( export const getNodeById = (
searchingId: string, searchingId: string,
treeData: ITraceTree, treesData: ITraceForest | undefined,
): ITraceTree | undefined => { ): ITraceForest => {
let foundNode: ITraceTree | undefined; const newtreeData: ITraceForest = {} as ITraceForest;
const traverse = (treeNode: ITraceTree, level = 0): void => {
const traverse = (
treeNode: ITraceTree,
setCallBack: (arg0: ITraceTree) => void,
level = 0,
): void => {
if (!treeNode) { if (!treeNode) {
return; return;
} }
if (searchingId === treeNode.id) { if (searchingId === treeNode.id) {
foundNode = treeNode; setCallBack(treeNode);
} }
treeNode.children.forEach((childNode) => { 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 = ( const getSpanWithoutChildren = (

View 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;

View File

@ -18,6 +18,7 @@ const { TabPane } = Tabs;
function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element { function SelectedSpanDetails(props: SelectedSpanDetailsProps): JSX.Element {
const { tree } = props; const { tree } = props;
const { isDarkMode } = useThemeMode(); const { isDarkMode } = useThemeMode();
const OverLayComponentName = useMemo(() => tree?.name, [tree?.name]); const OverLayComponentName = useMemo(() => tree?.name, [tree?.name]);

View File

@ -17,15 +17,23 @@ import dayjs from 'dayjs';
import useUrlQuery from 'hooks/useUrlQuery'; import useUrlQuery from 'hooks/useUrlQuery';
import { spanServiceNameToColorMapping } from 'lib/getRandomColor'; import { spanServiceNameToColorMapping } from 'lib/getRandomColor';
import history from 'lib/history'; import history from 'lib/history';
import { map } from 'lodash-es';
import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants'; import { SPAN_DETAILS_LEFT_COL_WIDTH } from 'pages/TraceDetail/constants';
import React, { useEffect, useMemo, useState } from 'react'; 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 { getSpanTreeMetadata } from 'utils/getSpanTreeMetadata';
import { spanToTreeUtil } from 'utils/spanToTree'; import { spanToTreeUtil } from 'utils/spanToTree';
import MissingSpansMessage from './Missingtrace';
import SelectedSpanDetails from './SelectedSpanDetails'; import SelectedSpanDetails from './SelectedSpanDetails';
import * as styles from './styles'; 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 { function TraceDetail({ response }: TraceDetailProps): JSX.Element {
const spanServiceColors = useMemo( const spanServiceColors = useMemo(
@ -43,17 +51,23 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
const [activeHoverId, setActiveHoverId] = useState<string>(''); const [activeHoverId, setActiveHoverId] = useState<string>('');
const [activeSelectedId, setActiveSelectedId] = useState<string>(spanId || ''); const [activeSelectedId, setActiveSelectedId] = useState<string>(spanId || '');
const [treeData, setTreeData] = useState<ITraceTree>( const [treesData, setTreesData] = useState<ITraceForest>(
spanToTreeUtil(response[0].events), spanToTreeUtil(response[0].events),
); );
const { treeData: tree, ...traceMetaData } = useMemo(() => { const { treesData: tree, ...traceMetaData } = useMemo(() => {
const tree = getSortedData(treeData); const sortedTreesData: ITraceForest = {
spanTree: map(treesData.spanTree, (tree) => getSortedData(tree)),
missingSpanTree: map(
treesData.missingSpanTree,
(tree) => getSortedData(tree) || [],
),
};
// Note: Handle undefined // Note: Handle undefined
/*eslint-disable */ /*eslint-disable */
return getSpanTreeMetadata(tree as ITraceTree, spanServiceColors); return getSpanTreeMetadata(sortedTreesData, spanServiceColors);
/* eslint-enable */ /* eslint-enable */
}, [treeData, spanServiceColors]); }, [treesData, spanServiceColors]);
const [globalTraceMetadata] = useState<ITraceMetaData>({ const [globalTraceMetadata] = useState<ITraceMetaData>({
...traceMetaData, ...traceMetaData,
@ -69,24 +83,34 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
}, [activeSelectedId]); }, [activeSelectedId]);
const getSelectedNode = useMemo(() => { const getSelectedNode = useMemo(() => {
return getNodeById(activeSelectedId, treeData); return getNodeById(activeSelectedId, treesData);
}, [activeSelectedId, treeData]); }, [activeSelectedId, treesData]);
// const onSearchHandler = (value: string) => { // const onSearchHandler = (value: string) => {
// setSearchSpanString(value); // setSearchSpanString(value);
// setTreeData(spanToTreeUtil(response[0].events)); // setTreeData(spanToTreeUtil(response[0].events));
// }; // };
const onFocusSelectedSpanHandler = (): void => { const onFocusSelectedSpanHandler = (): void => {
const treeNode = getNodeById(activeSelectedId, tree); const treeNode = getNodeById(activeSelectedId, tree);
if (treeNode) { if (treeNode) {
setTreeData(treeNode); setTreesData(treeNode);
} }
}; };
const onResetHandler = (): void => { 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 ( return (
<StyledRow styledclass={[StyledStyles.Flex({ flex: 1 })]}> <StyledRow styledclass={[StyledStyles.Flex({ flex: 1 })]}>
<StyledCol flex="auto" styledclass={styles.leftContainer}> <StyledCol flex="auto" styledclass={styles.leftContainer}>
@ -101,16 +125,45 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
<StyledTypography.Text styledclass={[styles.removeMargin]}> <StyledTypography.Text styledclass={[styles.removeMargin]}>
{traceMetaData.totalSpans} Span {traceMetaData.totalSpans} Span
</StyledTypography.Text> </StyledTypography.Text>
{hasMissingSpans && <MissingSpansMessage />}
</StyledCol> </StyledCol>
<Col flex="auto"> <Col flex="auto">
<TraceFlameGraph {map(tree.spanTree, (tree) => {
treeData={tree} return (
traceMetaData={traceMetaData} <TraceFlameGraph
hoveredSpanId={activeHoverId} key={tree as never}
selectedSpanId={activeSelectedId} treeData={tree}
onSpanHover={setActiveHoverId} traceMetaData={traceMetaData}
onSpanSelect={setActiveSelectedId} 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> </Col>
</StyledRow> </StyledRow>
<StyledRow styledclass={[styles.traceDateAndTimelineContainer]}> <StyledRow styledclass={[styles.traceDateAndTimelineContainer]}>
@ -122,7 +175,9 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
justifyContent: 'center', 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>
<StyledCol flex="auto" styledclass={[styles.timelineContainer]}> <StyledCol flex="auto" styledclass={[styles.timelineContainer]}>
<Timeline <Timeline
@ -141,14 +196,7 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
}), }),
]} ]}
> >
<Col flex={`${SPAN_DETAILS_LEFT_COL_WIDTH}px`}> <Col flex={`${SPAN_DETAILS_LEFT_COL_WIDTH}px`} />
{/* <Search
placeholder="Type to filter.."
allowClear
onSearch={onSearchHandler}
style={{ width: 200 }}
/> */}
</Col>
<Col flex="auto"> <Col flex="auto">
<StyledSpace styledclass={[styles.floatRight]}> <StyledSpace styledclass={[styles.floatRight]}>
<Button onClick={onFocusSelectedSpanHandler} icon={<FilterOutlined />}> <Button onClick={onFocusSelectedSpanHandler} icon={<FilterOutlined />}>
@ -161,23 +209,50 @@ function TraceDetail({ response }: TraceDetailProps): JSX.Element {
</Col> </Col>
</StyledRow> </StyledRow>
<StyledDiv styledclass={[styles.ganttChartContainer]}> <StyledDiv styledclass={[styles.ganttChartContainer]}>
<GanttChart <GanttChartWrapper>
traceMetaData={traceMetaData} {map([...tree.spanTree, ...tree.missingSpanTree], (tree) => (
data={tree} <GanttChart
activeSelectedId={activeSelectedId} key={tree as never}
activeHoverId={activeHoverId} traceMetaData={traceMetaData}
setActiveHoverId={setActiveHoverId} data={tree}
setActiveSelectedId={setActiveSelectedId} activeSelectedId={activeSelectedId}
spanId={spanId || ''} activeHoverId={activeHoverId}
intervalUnit={intervalUnit} 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> </StyledDiv>
</StyledCol> </StyledCol>
<Col> <Col>
<StyledDivider styledclass={[styles.verticalSeparator]} type="vertical" /> <StyledDivider styledclass={[styles.verticalSeparator]} type="vertical" />
</Col> </Col>
<StyledCol md={5} sm={5} styledclass={[styles.selectedSpanDetailContainer]}> <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> </StyledCol>
</StyledRow> </StyledRow>
); );

View File

@ -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 * Styles for the left container. Containers flamegraph, timeline and gantt chart
@ -76,3 +77,38 @@ export const floatRight = css`
export const removeMargin = css` export const removeMargin = css`
margin: 0; 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;
`;

View File

@ -62,7 +62,7 @@ export const convertTimeToRelevantUnit = (
return relevantTime; return relevantTime;
}; };
export const getSortedData = (treeData: ITraceTree): undefined | ITraceTree => { export const getSortedData = (treeData: ITraceTree): ITraceTree => {
const traverse = (treeNode: ITraceTree, level = 0): void => { const traverse = (treeNode: ITraceTree, level = 0): void => {
if (!treeNode) { if (!treeNode) {
return; return;
@ -80,3 +80,21 @@ export const getSortedData = (treeData: ITraceTree): undefined | ITraceTree => {
return treeData; 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;
};

View File

@ -28,6 +28,7 @@ test('loads and displays greeting', () => {
spread: 0, spread: 0,
totalSpans: 0, totalSpans: 0,
}, },
missingSpanTree: false,
treeData: { treeData: {
children: [], children: [],
id: '', id: '',

View File

@ -93,8 +93,9 @@ function TraceFlameGraph(props: {
onSpanSelect: SpanItemProps['onSpanSelect']; onSpanSelect: SpanItemProps['onSpanSelect'];
hoveredSpanId: string; hoveredSpanId: string;
selectedSpanId: string; selectedSpanId: string;
missingSpanTree: boolean;
}): JSX.Element { }): JSX.Element {
const { treeData, traceMetaData, onSpanHover } = props; const { treeData, traceMetaData, onSpanHover, missingSpanTree } = props;
if (!treeData || treeData.id === 'empty' || !traceMetaData) { if (!treeData || treeData.id === 'empty' || !traceMetaData) {
return <div />; return <div />;
@ -140,6 +141,7 @@ function TraceFlameGraph(props: {
hoveredSpanId={hoveredSpanId} hoveredSpanId={hoveredSpanId}
selectedSpanId={selectedSpanId} selectedSpanId={selectedSpanId}
/> />
{spanData.children.map((childData) => ( {spanData.children.map((childData) => (
<RenderSpanRecursive <RenderSpanRecursive
level={level + 1} level={level + 1}
@ -164,7 +166,7 @@ function TraceFlameGraph(props: {
onSpanSelect={onSpanSelect} onSpanSelect={onSpanSelect}
hoveredSpanId={hoveredSpanId} hoveredSpanId={hoveredSpanId}
selectedSpanId={selectedSpanId} selectedSpanId={selectedSpanId}
level={0} level={missingSpanTree ? -1 : 0}
parentLeftOffset={0} parentLeftOffset={0}
/> />
</TraceFlameGraphContainer> </TraceFlameGraphContainer>

View File

@ -21,7 +21,8 @@ export type Span = [
string | string[], string | string[],
string | string[], string | string[],
string | string[], string | string[],
ITraceTree[], Record<string, unknown>[],
boolean,
]; ];
export interface ITraceTree { export interface ITraceTree {
@ -37,6 +38,10 @@ export interface ITraceTree {
serviceColour: string; serviceColour: string;
hasError?: boolean; hasError?: boolean;
event?: ITraceEvents[]; event?: ITraceEvents[];
isMissing?: boolean;
// For internal use
isProcessed?: boolean;
references?: Record<string, string>[];
} }
export interface ITraceTag { export interface ITraceTag {
@ -48,3 +53,8 @@ interface ITraceEvents {
attributeMap: { event: string; [key: string]: string }; attributeMap: { event: string; [key: string]: string };
name?: string; name?: string;
} }
export interface ITraceForest {
spanTree: ITraceTree[];
missingSpanTree: ITraceTree[];
}

View File

@ -1,28 +1,31 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { errorColor } from 'lib/getRandomColor'; 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. * Traverses the Span Tree data and returns the relevant meta data.
* Metadata includes globalStart, globalEnd, * Metadata includes globalStart, globalEnd,
*/ */
export const getSpanTreeMetadata = ( export const getSpanTreeMetadata = (
treeData: ITraceTree, treesData: ITraceForest,
spanServiceColours: { [key: string]: string }, spanServiceColours: { [key: string]: string },
): GetSpanTreeMetaData => { ): GetSpanTreeMetaData => {
let globalStart = Number.POSITIVE_INFINITY; let globalStart = Number.POSITIVE_INFINITY;
let globalEnd = Number.NEGATIVE_INFINITY; let globalEnd = Number.NEGATIVE_INFINITY;
let totalSpans = 0; let totalSpans = 0;
let levels = 1; let levels = 1;
const traverse = (treeNode: ITraceTree, level = 0): void => { const traverse = (treeNode: ITraceTree, level = 0): void => {
if (!treeNode) { if (!treeNode) {
return; return;
} }
totalSpans += 1; totalSpans += 1;
levels = Math.max(levels, level); levels = Math.max(levels, level);
const { startTime } = treeNode; const { startTime, value } = treeNode;
const endTime = startTime + treeNode.value / 1e6; if (startTime !== null && value !== null) {
globalStart = Math.min(globalStart, startTime); const endTime = startTime + value / 1e6;
globalEnd = Math.max(globalEnd, endTime); globalStart = Math.min(globalStart, startTime);
globalEnd = Math.max(globalEnd, endTime);
}
if (treeNode.hasError) { if (treeNode.hasError) {
treeNode.serviceColour = errorColor; treeNode.serviceColour = errorColor;
} else treeNode.serviceColour = spanServiceColours[treeNode.serviceName]; } else treeNode.serviceColour = spanServiceColours[treeNode.serviceName];
@ -30,7 +33,12 @@ export const getSpanTreeMetadata = (
traverse(childNode, level + 1); traverse(childNode, level + 1);
}); });
}; };
traverse(treeData, 1); treesData.spanTree.forEach((treeData) => {
traverse(treeData, 1);
});
treesData.missingSpanTree.forEach((treeData) => {
traverse(treeData, 1);
});
return { return {
globalStart, globalStart,
@ -38,7 +46,7 @@ export const getSpanTreeMetadata = (
spread: globalEnd - globalStart, spread: globalEnd - globalStart,
totalSpans, totalSpans,
levels, levels,
treeData, treesData,
}; };
}; };
@ -48,5 +56,5 @@ interface GetSpanTreeMetaData {
spread: number; spread: number;
totalSpans: number; totalSpans: number;
levels: number; levels: number;
treeData: ITraceTree; treesData: ITraceForest;
} }

View File

@ -1,137 +1,125 @@
/* eslint-disable */ /* eslint-disable no-restricted-syntax */
// @ts-nocheck
import { cloneDeep } from 'lodash-es'; 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 => { const getSpanReferences = (
// Initializing tree. What should be returned is trace is empty? We should have better error handling rawReferences: string[] = [],
let tree: ITraceTree = { ): Record<string, string>[] => {
id: 'empty', return rawReferences.map((rawRef) => {
name: 'default', const refObject: Record<string, string> = {};
value: 0, rawRef
time: 0, .replaceAll('{', '')
startTime: 0, .replaceAll('}', '')
tags: [], .replaceAll(' ', '')
children: [], .split(',')
serviceColour: '', .forEach((rawRefKeyPair) => {
serviceName: '', const [key, value] = rawRefKeyPair.split('=');
}; refObject[key] = value;
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);
}); });
if (references.length !== 0 && references[0].spanID.length !== 0) { return refObject;
if (references[0].refType === 'CHILD_OF') { });
const parentID = references[0].spanID; };
// console.log(`In SpanTreeUtil: mapped_array[parentID] is ${mapped_array[parentID]}`);
// This getSpanTags is migrated from the previous implementation.
if (typeof mapped_array[parentID] !== 'undefined') { const getSpanTags = (spanData: Span): { key: string; value: string }[] => {
// checking for undefined [10] issue const tags = [];
mapped_array[parentID][10].push(push_object); if (spanData[7] !== null && spanData[8] !== null) {
} else { if (typeof spanData[7] === 'string' && typeof spanData[8] === 'string') {
// console.log( tags.push({ key: spanData[7], value: spanData[8] });
// `In SpanTreeUtil: mapped_array[parentID] is undefined, parentID is ${parentID}`, } else if (spanData[7].length > 0 && spanData[8].length > 0) {
// ); for (let j = 0; j < spanData[7].length; j += 1) {
// console.log( tags.push({ key: spanData[7][j], value: spanData[8][j] });
// `In SpanTreeUtil: mapped_array[parentID] is undefined, mapped_array[parentID] is ${mapped_array[parentID]}`, }
// ); }
} }
} return tags;
} else { };
tree = push_object;
} // eslint-disable-next-line sonarjs/cognitive-complexity
} // end of for loop export const spanToTreeUtil = (inputSpanList: Span[]): ITraceForest => {
} // end of if(spans) const spanList = cloneDeep(inputSpanList);
const traceIdSet: Set<string> = new Set();
return { ...tree }; 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,
};
}; };