diff --git a/frontend/package.json b/frontend/package.json index 9ab79225e6a..c5f4a633939 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -44,6 +44,7 @@ "@sentry/webpack-plugin": "2.22.6", "@signozhq/design-tokens": "1.1.4", "@tanstack/react-table": "8.20.6", + "@tanstack/react-virtual": "3.11.2", "@uiw/react-md-editor": "3.23.5", "@visx/group": "3.3.0", "@visx/shape": "3.5.0", diff --git a/frontend/public/Icons/construction.svg b/frontend/public/Icons/construction.svg new file mode 100644 index 00000000000..0ade634ebb4 --- /dev/null +++ b/frontend/public/Icons/construction.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/public/Icons/no-data.svg b/frontend/public/Icons/no-data.svg new file mode 100644 index 00000000000..858fdfe7481 --- /dev/null +++ b/frontend/public/Icons/no-data.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/AppRoutes/pageComponents.ts b/frontend/src/AppRoutes/pageComponents.ts index cbbcbbed735..8bccc4c50fc 100644 --- a/frontend/src/AppRoutes/pageComponents.ts +++ b/frontend/src/AppRoutes/pageComponents.ts @@ -43,7 +43,10 @@ export const TraceFilter = Loadable( ); export const TraceDetail = Loadable( - () => import(/* webpackChunkName: "TraceDetail Page" */ 'pages/TraceDetail'), + () => + import( + /* webpackChunkName: "TraceDetail Page" */ 'pages/TraceDetailV2/index' + ), ); export const UsageExplorerPage = Loadable( diff --git a/frontend/src/api/trace/getTraceFlamegraph.tsx b/frontend/src/api/trace/getTraceFlamegraph.tsx new file mode 100644 index 00000000000..a084507e1f9 --- /dev/null +++ b/frontend/src/api/trace/getTraceFlamegraph.tsx @@ -0,0 +1,33 @@ +import { ApiV2Instance as axios } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { omit } from 'lodash-es'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + GetTraceFlamegraphPayloadProps, + GetTraceFlamegraphSuccessResponse, +} from 'types/api/trace/getTraceFlamegraph'; + +const getTraceFlamegraph = async ( + props: GetTraceFlamegraphPayloadProps, +): Promise< + SuccessResponse | ErrorResponse +> => { + try { + const response = await axios.post( + `/traces/flamegraph/${props.traceId}`, + omit(props, 'traceId'), + ); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getTraceFlamegraph; diff --git a/frontend/src/api/trace/getTraceV2.tsx b/frontend/src/api/trace/getTraceV2.tsx new file mode 100644 index 00000000000..a2a534154ad --- /dev/null +++ b/frontend/src/api/trace/getTraceV2.tsx @@ -0,0 +1,41 @@ +import { ApiV2Instance as axios } from 'api'; +import { ErrorResponseHandler } from 'api/ErrorResponseHandler'; +import { AxiosError } from 'axios'; +import { omit } from 'lodash-es'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + GetTraceV2PayloadProps, + GetTraceV2SuccessResponse, +} from 'types/api/trace/getTraceV2'; + +const getTraceV2 = async ( + props: GetTraceV2PayloadProps, +): Promise | ErrorResponse> => { + try { + let uncollapsedSpans = [...props.uncollapsedSpans]; + if (!props.isSelectedSpanIDUnCollapsed) { + uncollapsedSpans = uncollapsedSpans.filter( + (node) => node !== props.selectedSpanId, + ); + } + const postData: GetTraceV2PayloadProps = { + ...props, + uncollapsedSpans, + }; + const response = await axios.post( + `/traces/waterfall/${props.traceId}`, + omit(postData, 'traceId'), + ); + + return { + statusCode: 200, + error: null, + message: 'Success', + payload: response.data, + }; + } catch (error) { + return ErrorResponseHandler(error as AxiosError); + } +}; + +export default getTraceV2; diff --git a/frontend/src/assets/TraceDetail/Flamegraph.tsx b/frontend/src/assets/TraceDetail/Flamegraph.tsx new file mode 100644 index 00000000000..96fa65cf8bd --- /dev/null +++ b/frontend/src/assets/TraceDetail/Flamegraph.tsx @@ -0,0 +1,25 @@ +import { Color } from '@signozhq/design-tokens'; +import { useIsDarkMode } from 'hooks/useDarkMode'; + +function FlamegraphImg(): JSX.Element { + const isDarkMode = useIsDarkMode(); + return ( + + + + ); +} + +export default FlamegraphImg; diff --git a/frontend/src/components/DetailsDrawer/DetailsDrawer.styles.scss b/frontend/src/components/DetailsDrawer/DetailsDrawer.styles.scss new file mode 100644 index 00000000000..2a92f63a5a6 --- /dev/null +++ b/frontend/src/components/DetailsDrawer/DetailsDrawer.styles.scss @@ -0,0 +1,134 @@ +.details-drawer { + .ant-drawer-wrapper-body { + border-left: 1px solid var(--bg-slate-500); + } + .ant-drawer-header { + background: var(--bg-ink-400); + border-bottom: 1px solid var(--bg-slate-500); + + .ant-drawer-header-title { + display: flex; + align-items: center; + + .ant-drawer-close { + margin-inline-end: 0px; + padding: 0px; + padding-right: 16px; + border-right: 1px solid var(--bg-slate-500); + } + + .ant-drawer-title { + padding-left: 16px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + } + .ant-drawer-body { + padding: 16px; + background: var(--bg-ink-400); + + &::-webkit-scrollbar { + width: 0.1rem; + } + } + + .details-drawer-tabs { + margin-top: 32px; + + .ant-tabs-tab { + display: flex; + align-items: center; + justify-content: center; + width: 114px; + height: 32px; + flex-shrink: 0; + padding: 7px 20px; + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-ink-400); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + + color: #fff; + font-family: Inter; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: -0.06px; + + .ant-btn { + display: flex; + align-items: center; + justify-content: center; + padding: 0px; + } + + .ant-btn:hover { + background: unset; + } + } + + .ant-tabs-tab-active { + background: var(--bg-slate-400); + } + + .ant-tabs-tab + .ant-tabs-tab { + margin-left: 0px; + } + + .ant-tabs-nav::before { + border-bottom: 0px; + } + + .ant-tabs-ink-bar { + background: none; + } + } +} + +.lightMode { + .details-drawer { + .ant-drawer-wrapper-body { + border-left: 1px solid var(--bg-vanilla-300); + } + .ant-drawer-header { + background: var(--bg-vanilla-200); + border-bottom: 1px solid var(--bg-vanilla-300); + + .ant-drawer-header-title { + .ant-drawer-close { + border-right: 1px solid var(--bg-vanilla-300); + } + + .ant-drawer-title { + color: var(--bg-ink-400); + } + } + } + .ant-drawer-body { + background: var(--bg-vanilla-200); + } + + .details-drawer-tabs { + .ant-tabs-tab { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + color: var(--bg-ink-400); + } + + .ant-tabs-tab-active { + background: var(--bg-vanilla-200); + } + + .ant-tabs-tab + .ant-tabs-tab { + border-left: none; + } + } + } +} diff --git a/frontend/src/components/DetailsDrawer/DetailsDrawer.tsx b/frontend/src/components/DetailsDrawer/DetailsDrawer.tsx new file mode 100644 index 00000000000..59776aec13c --- /dev/null +++ b/frontend/src/components/DetailsDrawer/DetailsDrawer.tsx @@ -0,0 +1,57 @@ +import './DetailsDrawer.styles.scss'; + +import { Drawer, Tabs, TabsProps } from 'antd'; +import cx from 'classnames'; +import { Dispatch, SetStateAction } from 'react'; + +interface IDetailsDrawerProps { + open: boolean; + setOpen: Dispatch>; + title: string; + descriptiveContent: JSX.Element; + defaultActiveKey: string; + items: TabsProps['items']; + detailsDrawerClassName?: string; + tabBarExtraContent?: JSX.Element; +} + +function DetailsDrawer(props: IDetailsDrawerProps): JSX.Element { + const { + open, + setOpen, + title, + descriptiveContent, + defaultActiveKey, + detailsDrawerClassName, + items, + tabBarExtraContent, + } = props; + return ( + setOpen(false)} + className="details-drawer" + > +
{descriptiveContent}
+ +
+ ); +} + +DetailsDrawer.defaultProps = { + detailsDrawerClassName: '', + tabBarExtraContent: null, +}; + +export default DetailsDrawer; diff --git a/frontend/src/components/Logs/LogStateIndicator/utils.test.ts b/frontend/src/components/Logs/LogStateIndicator/utils.test.ts index f940ee30460..124689999b2 100644 --- a/frontend/src/components/Logs/LogStateIndicator/utils.test.ts +++ b/frontend/src/components/Logs/LogStateIndicator/utils.test.ts @@ -10,7 +10,7 @@ describe('getLogIndicatorType', () => { timestamp: 1646115296, id: '123456', traceId: '987654', - spanId: '54321', + spanID: '54321', traceFlags: 0, severityText: 'INFO', severityNumber: 2, @@ -34,7 +34,7 @@ describe('getLogIndicatorType', () => { timestamp: 1646115296, id: '123456', traceId: '987654', - spanId: '54321', + spanID: '54321', traceFlags: 0, severityText: 'INFO', severityNumber: 2, @@ -57,7 +57,7 @@ describe('getLogIndicatorType', () => { timestamp: 1646115296, id: '123456', traceId: '987654', - spanId: '54321', + spanID: '54321', traceFlags: 0, severityText: 'INFO', severityNumber: 2, @@ -80,7 +80,7 @@ describe('getLogIndicatorType', () => { timestamp: 1646115296, id: '123456', traceId: '987654', - spanId: '54321', + spanID: '54321', traceFlags: 0, severityNumber: 2, body: 'Sample log', @@ -107,7 +107,7 @@ describe('getLogIndicatorTypeForTable', () => { timestamp: 1646115296, id: '123456', traceId: '987654', - spanId: '54321', + spanID: '54321', traceFlags: 0, severityNumber: 2, severity_number: 2, @@ -129,7 +129,7 @@ describe('getLogIndicatorTypeForTable', () => { timestamp: 1646115296, id: '123456', traceId: '987654', - spanId: '54321', + spanID: '54321', traceFlags: 0, severityNumber: 0, severity_number: 0, @@ -165,7 +165,7 @@ describe('logIndicatorBySeverityNumber', () => { timestamp: 1646115296, id: '123456', traceId: '987654', - spanId: '54321', + spanID: '54321', traceFlags: 0, severityText: sevText, severityNumber: sevNum, diff --git a/frontend/src/components/TableV3/TableV3.tsx b/frontend/src/components/TableV3/TableV3.tsx index 6219297f33d..b54319a22be 100644 --- a/frontend/src/components/TableV3/TableV3.tsx +++ b/frontend/src/components/TableV3/TableV3.tsx @@ -7,28 +7,53 @@ import { Table, useReactTable, } from '@tanstack/react-table'; -import React, { useMemo } from 'react'; +import { useVirtualizer, Virtualizer } from '@tanstack/react-virtual'; +import cx from 'classnames'; +import React, { + Dispatch, + MutableRefObject, + SetStateAction, + useEffect, + useMemo, +} from 'react'; // here we are manually rendering the table body so that we can memoize the same for performant re-renders -function TableBody({ table }: { table: Table }): JSX.Element { +function TableBody({ + table, + virtualizer, +}: { + table: Table; + virtualizer: Virtualizer; +}): JSX.Element { + const { rows } = table.getRowModel(); return (
- {table.getRowModel().rows.map((row) => ( -
- {row.getVisibleCells().map((cell) => ( -
- {cell.renderValue()} -
- ))} -
- ))} + {virtualizer.getVirtualItems().map((virtualRow, index) => { + const row = rows[virtualRow.index]; + return ( +
+ {row.getVisibleCells().map((cell) => ( +
+ {flexRender(cell.column.columnDef.cell, cell.getContext())} +
+ ))} +
+ ); + })}
); } @@ -40,17 +65,32 @@ const MemoizedTableBody = React.memo( ) as typeof TableBody; interface ITableConfig { - defaultColumnMinSize: number; - defaultColumnMaxSize: number; + defaultColumnMinSize?: number; + defaultColumnMaxSize?: number; + handleVirtualizerInstanceChanged?: ( + instance: Virtualizer, + ) => void; } interface ITableV3Props { columns: ColumnDef[]; data: T[]; config: ITableConfig; + customClassName?: string; + setTraceFlamegraphStatsWidth: Dispatch>; + virtualiserRef?: MutableRefObject< + Virtualizer | undefined + >; } export function TableV3(props: ITableV3Props): JSX.Element { - const { data, columns, config } = props; + const { + data, + columns, + config, + customClassName = '', + virtualiserRef, + setTraceFlamegraphStatsWidth, + } = props; const table = useReactTable({ data, @@ -61,11 +101,26 @@ export function TableV3(props: ITableV3Props): JSX.Element { }, columnResizeMode: 'onChange', getCoreRowModel: getCoreRowModel(), - debugTable: true, - debugHeaders: true, - debugColumns: true, + // turn on debug flags to get debug logs from these instances + debugAll: false, + }); + + const tableRef = React.useRef(null); + const { rows } = table.getRowModel(); + const virtualizer = useVirtualizer({ + count: rows.length, + getScrollElement: () => tableRef.current, + estimateSize: () => 54, + overscan: 20, + onChange: config.handleVirtualizerInstanceChanged, }); + useEffect(() => { + if (virtualiserRef) { + virtualiserRef.current = virtualizer; + } + }, [virtualiserRef, virtualizer]); + /** * Instead of calling `column.getSize()` on every render for every header * and especially every data cell (very expensive), @@ -84,14 +139,21 @@ export function TableV3(props: ITableV3Props): JSX.Element { // eslint-disable-next-line react-hooks/exhaustive-deps }, [table.getState().columnSizingInfo, table.getState().columnSizing]); + useEffect(() => { + const headers = table.getFlatHeaders(); + setTraceFlamegraphStatsWidth(headers[0].getSize()); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [table.getState().columnSizingInfo, table.getState().columnSizing]); + return ( -
+
{/* Here in the equivalent element (surrounds all table head and data cells), we will define our CSS variables for column sizes */}
element width: table.getTotalSize(), + height: `${virtualizer.getTotalSize()}px`, }} >
@@ -113,6 +175,9 @@ export function TableV3(props: ITableV3Props): JSX.Element { onDoubleClick: (): void => header.column.resetSize(), onMouseDown: header.getResizeHandler(), onTouchStart: header.getResizeHandler(), + style: { + display: !header.column.getCanResize() ? 'none' : '', + }, className: `resizer ${ header.column.getIsResizing() ? 'isResizing' : '' }`, @@ -125,11 +190,16 @@ export function TableV3(props: ITableV3Props): JSX.Element {
{/* When resizing any column we will render this special memoized version of our table body */} {table.getState().columnSizingInfo.isResizingColumn ? ( - + ) : ( - + )}
); } + +TableV3.defaultProps = { + customClassName: '', + virtualiserRef: null, +}; diff --git a/frontend/src/components/TimelineV2/TimelineV2.tsx b/frontend/src/components/TimelineV2/TimelineV2.tsx index f7c0488bdfb..a52dafc5bc4 100644 --- a/frontend/src/components/TimelineV2/TimelineV2.tsx +++ b/frontend/src/components/TimelineV2/TimelineV2.tsx @@ -42,6 +42,8 @@ function TimelineV2(props: ITimelineV2Props): JSX.Element { return
; } + const strokeColor = isDarkMode ? ' rgb(192,193,195,0.8)' : 'black'; + return (
{intervals && @@ -70,14 +72,14 @@ function TimelineV2(props: ITimelineV2Props): JSX.Element { {interval.label} diff --git a/frontend/src/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index 30da4e13625..65fdbffd684 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -21,6 +21,8 @@ export const REACT_QUERY_KEY = { GET_HOST_LIST: 'GET_HOST_LIST', UPDATE_ALERT_RULE: 'UPDATE_ALERT_RULE', GET_ACTIVE_LICENSE_V3: 'GET_ACTIVE_LICENSE_V3', + GET_TRACE_V2_WATERFALL: 'GET_TRACE_V2_WATERFALL', + GET_TRACE_V2_FLAMEGRAPH: 'GET_TRACE_V2_FLAMEGRAPH', GET_POD_LIST: 'GET_POD_LIST', GET_NODE_LIST: 'GET_NODE_LIST', GET_DEPLOYMENT_LIST: 'GET_DEPLOYMENT_LIST', diff --git a/frontend/src/constants/theme.ts b/frontend/src/constants/theme.ts index edc0eea75f5..8481f0578ba 100644 --- a/frontend/src/constants/theme.ts +++ b/frontend/src/constants/theme.ts @@ -1,4 +1,38 @@ const themeColors = { + traceDetailColors: { + robin: '#3F5ECC', + dodgerBlue: '#2F80ED', + mediumOrchid: '#BB6BD9', + seaBuckthorn: '#F2994A', + turquoiseBlue: '#56CCF2', + festivalOrange: '#F2C94C', + silver: '#BDBDBD', + outrageousOrange: '#FF6633', + roseBud: '#FFB399', + canary: '#FFFF99', + deepSkyBlue: '#00B3E6', + goldTips: '#E6B333', + royalBlue: '#3366E6', + avocado: '#999966', + mintGreen: '#99FF99', + lima: '#80B300', + olive: '#809900', + beautyBush: '#E6B3B3', + danube: '#6680B3', + oliveDrab: '#66991A', + lavenderRose: '#FF99E6', + electricLime: '#CCFF1A', + turquoise: '#33FFCC', + gladeGreen: '#66994D', + hemlock: '#66664D', + vidaLoca: '#4D8000', + mediumAquamarine: '#66CDAA', + lavender: '#E6E6FA', + thistle: '#D8BFD8', + yellow: '#FFFF00', + purple: '#800080', + cyan: '#00FFFF', + }, chartcolors: { robin: '#3F5ECC', dodgerBlue: '#2F80ED', diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 24d9c2b9684..9a47baa8114 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -430,7 +430,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { ? 0 : '0 1rem', - ...(isTraceDetailsView() ? { marginRight: 0 } : {}), + ...(isTraceDetailsView() ? { margin: 0 } : {}), }} > {isToDisplayLayout && !renderFullScreen && } diff --git a/frontend/src/container/LogDetailedView/utils.tsx b/frontend/src/container/LogDetailedView/utils.tsx index da62f97f8e2..05d65259f60 100644 --- a/frontend/src/container/LogDetailedView/utils.tsx +++ b/frontend/src/container/LogDetailedView/utils.tsx @@ -186,7 +186,7 @@ export const aggregateAttributesResourcesToString = (logData: ILog): string => { id: logData.id, severityNumber: logData.severityNumber, severityText: logData.severityText, - spanId: logData.spanId, + spanID: logData.spanID, timestamp: logData.timestamp, traceFlags: logData.traceFlags, traceId: logData.traceId, diff --git a/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.styles.scss b/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.styles.scss new file mode 100644 index 00000000000..504acf4f02b --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.styles.scss @@ -0,0 +1,145 @@ +.flamegraph { + display: flex; + height: 30vh; + border-bottom: 1px solid var(--bg-slate-400); + + .flamegraph-chart { + padding: 15px; + + .loading-skeleton { + justify-content: center; + align-items: center; + } + } + + .flamegraph-stats { + display: flex; + flex-direction: column; + border-right: 1px solid var(--bg-slate-400); + overflow-y: auto; + overflow-x: hidden; + padding: 16px 20px; + + .exec-time-service { + display: flex; + height: 30px; + flex-shrink: 0; + justify-content: center; + align-items: center; + border-radius: 2px 0px 0px 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-slate-400); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + margin-bottom: 16px; + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + letter-spacing: -0.06px; + } + + .stats { + display: flex; + flex-direction: column; + gap: 12px; + overflow-y: auto; + overflow-x: hidden; + + &::-webkit-scrollbar { + width: 0rem; + } + + .value-row { + display: flex; + justify-content: space-between; + + .service-name { + display: flex; + align-items: center; + gap: 8px; + width: 80%; + + .service-text { + color: var(--bg-vanilla-400); + font-family: 'Inter'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: normal; + width: 80%; + } + + .square-box { + height: 8px; + width: 8px; + } + } + + .progress-service { + display: flex; + align-items: center; + width: 100px; + gap: 8px; + justify-content: flex-start; + flex-shrink: 0; + + .service-progress-indicator { + width: fit-content; + margin-inline-end: 0px !important; + margin-bottom: 0px !important; + + .ant-progress-inner { + width: 30px; + } + } + + .percent-value { + color: var(--bg-vanilla-100); + text-align: right; + font-family: 'Inter'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.48px; + font-variant-numeric: lining-nums tabular-nums slashed-zero; + } + } + } + } + } +} + +.lightMode { + .flamegraph { + border-bottom: 1px solid var(--bg-vanilla-300); + + .flamegraph-stats { + border-right: 1px solid var(--bg-vanilla-300); + + .exec-time-service { + border: 1px solid var(--bg-vanilla-400); + background: var(--bg-vanilla-400); + color: var(--bg-ink-100); + } + + .stats { + .value-row { + .service-name { + .service-text { + color: var(--bg-ink-400); + } + } + + .progress-service { + .percent-value { + color: var(--bg-ink-100); + } + } + } + } + } + } +} diff --git a/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.tsx b/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.tsx new file mode 100644 index 00000000000..c01fc274d48 --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.tsx @@ -0,0 +1,177 @@ +import './PaginatedTraceFlamegraph.styles.scss'; + +import { Progress, Skeleton, Tooltip, Typography } from 'antd'; +import { AxiosError } from 'axios'; +import Spinner from 'components/Spinner'; +import { themeColors } from 'constants/theme'; +import useGetTraceFlamegraph from 'hooks/trace/useGetTraceFlamegraph'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { generateColor } from 'lib/uPlotLib/utils/generateColor'; +import { useEffect, useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { TraceDetailFlamegraphURLProps } from 'types/api/trace/getTraceFlamegraph'; +import { Span } from 'types/api/trace/getTraceV2'; + +import { TraceFlamegraphStates } from './constants'; +import Error from './TraceFlamegraphStates/Error/Error'; +import NoData from './TraceFlamegraphStates/NoData/NoData'; +import Success from './TraceFlamegraphStates/Success/Success'; + +interface ITraceFlamegraphProps { + serviceExecTime: Record; + startTime: number; + endTime: number; + traceFlamegraphStatsWidth: number; + selectedSpan: Span | undefined; +} + +function TraceFlamegraph(props: ITraceFlamegraphProps): JSX.Element { + const { + serviceExecTime, + startTime, + endTime, + traceFlamegraphStatsWidth, + selectedSpan, + } = props; + const { id: traceId } = useParams(); + const urlQuery = useUrlQuery(); + const [firstSpanAtFetchLevel, setFirstSpanAtFetchLevel] = useState( + urlQuery.get('spanId') || '', + ); + + useEffect(() => { + setFirstSpanAtFetchLevel(urlQuery.get('spanId') || ''); + }, [urlQuery]); + + const { data, isFetching, error } = useGetTraceFlamegraph({ + traceId, + selectedSpanId: firstSpanAtFetchLevel, + }); + const isDarkMode = useIsDarkMode(); + + // get the current state of trace flamegraph based on the API lifecycle + const traceFlamegraphState = useMemo(() => { + if (isFetching) { + if ( + data && + data.payload && + data.payload.spans && + data.payload.spans.length > 0 + ) { + return TraceFlamegraphStates.FETCHING_WITH_OLD_DATA_PRESENT; + } + return TraceFlamegraphStates.LOADING; + } + if (error) { + return TraceFlamegraphStates.ERROR; + } + if ( + data && + data.payload && + data.payload.spans && + data.payload.spans.length === 0 + ) { + return TraceFlamegraphStates.NO_DATA; + } + + return TraceFlamegraphStates.SUCCESS; + }, [error, isFetching, data]); + + // capture the spans from the response, since we do not need to do any manipulation on the same we will keep this as a simple constant [ memoized ] + const spans = useMemo(() => data?.payload?.spans || [], [ + data?.payload?.spans, + ]); + + // get the content based on the current state of the trace waterfall + const getContent = useMemo(() => { + switch (traceFlamegraphState) { + case TraceFlamegraphStates.LOADING: + return ( +
+ +
+ ); + case TraceFlamegraphStates.ERROR: + return ; + case TraceFlamegraphStates.NO_DATA: + return ; + case TraceFlamegraphStates.SUCCESS: + case TraceFlamegraphStates.FETCHING_WITH_OLD_DATA_PRESENT: + return ( + + ); + default: + return ; + } + }, [ + data?.payload?.endTimestampMillis, + data?.payload?.startTimestampMillis, + error, + firstSpanAtFetchLevel, + selectedSpan, + spans, + traceFlamegraphState, + traceId, + ]); + + return ( +
+
+
% exec time
+
+ {Object.keys(serviceExecTime).map((service) => { + const spread = endTime - startTime; + const value = (serviceExecTime[service] * 100) / spread; + const color = generateColor( + service, + isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor, + ); + return ( +
+
+
+ + + {service} + + +
+
+ + + {parseFloat(value.toFixed(2))}% + +
+
+ ); + })} +
+
+
+ {getContent} +
+
+ ); +} + +export default TraceFlamegraph; diff --git a/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Error/Error.styles.scss b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Error/Error.styles.scss new file mode 100644 index 00000000000..1b837b5ad97 --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Error/Error.styles.scss @@ -0,0 +1,31 @@ +.error-flamegraph { + display: flex; + gap: 4px; + flex-direction: column; + justify-content: center; + align-items: center; + height: 15vh; + + .error-flamegraph-img { + height: 32px; + width: 32px; + } + + .no-data-text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } +} + +.lightMode { + .error-flamegraph { + .no-data-text { + color: var(--bg-ink-400); + } + } +} diff --git a/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Error/Error.tsx b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Error/Error.tsx new file mode 100644 index 00000000000..a9a4d63f94f --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Error/Error.tsx @@ -0,0 +1,29 @@ +import './Error.styles.scss'; + +import { Tooltip, Typography } from 'antd'; +import { AxiosError } from 'axios'; + +interface IErrorProps { + error: AxiosError; +} + +function Error(props: IErrorProps): JSX.Element { + const { error } = props; + + return ( +
+ error-flamegraph + + + {error?.message || 'Something went wrong!'} + + +
+ ); +} + +export default Error; diff --git a/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/NoData/NoData.tsx b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/NoData/NoData.tsx new file mode 100644 index 00000000000..0be04ffc234 --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/NoData/NoData.tsx @@ -0,0 +1,12 @@ +import { Typography } from 'antd'; + +interface INoDataProps { + id: string; +} + +function NoData(props: INoDataProps): JSX.Element { + const { id } = props; + return No Trace found with the id: {id} ; +} + +export default NoData; diff --git a/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.styles.scss b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.styles.scss new file mode 100644 index 00000000000..16501c6b01c --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.styles.scss @@ -0,0 +1,28 @@ +.trace-flamegraph { + height: 90%; + overflow-x: hidden; + overflow-y: auto; + + .trace-flamegraph-virtuoso { + overflow-x: hidden; + + .flamegraph-row { + display: flex; + align-items: center; + height: 18px; + padding-bottom: 6px; + + .span-item { + position: absolute; + height: 12px; + background-color: yellow; + border-radius: 6px; + cursor: pointer; + } + } + + &::-webkit-scrollbar { + width: 0rem; + } + } +} diff --git a/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.tsx b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.tsx new file mode 100644 index 00000000000..bd99cebee64 --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.tsx @@ -0,0 +1,154 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import './Success.styles.scss'; + +import { Tooltip } from 'antd'; +import Color from 'color'; +import TimelineV2 from 'components/TimelineV2/TimelineV2'; +import { themeColors } from 'constants/theme'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { generateColor } from 'lib/uPlotLib/utils/generateColor'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { ListRange, Virtuoso, VirtuosoHandle } from 'react-virtuoso'; +import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph'; +import { Span } from 'types/api/trace/getTraceV2'; + +interface ITraceMetadata { + startTime: number; + endTime: number; +} + +interface ISuccessProps { + spans: FlamegraphSpan[][]; + firstSpanAtFetchLevel: string; + setFirstSpanAtFetchLevel: Dispatch>; + traceMetadata: ITraceMetadata; + selectedSpan: Span | undefined; +} + +function Success(props: ISuccessProps): JSX.Element { + const { + spans, + setFirstSpanAtFetchLevel, + traceMetadata, + firstSpanAtFetchLevel, + selectedSpan, + } = props; + const { search } = useLocation(); + const history = useHistory(); + const isDarkMode = useIsDarkMode(); + const virtuosoRef = useRef(null); + const [hoveredSpanId, setHoveredSpanId] = useState(''); + const renderSpanLevel = useCallback( + (_: number, spans: FlamegraphSpan[]): JSX.Element => ( +
+ {spans.map((span) => { + const spread = traceMetadata.endTime - traceMetadata.startTime; + const leftOffset = + ((span.timestamp - traceMetadata.startTime) * 100) / spread; + let width = ((span.durationNano / 1e6) * 100) / spread; + if (width > 100) { + width = 100; + } + const toolTipText = `${span.name}`; + const searchParams = new URLSearchParams(search); + + let color = generateColor(span.serviceName, themeColors.traceDetailColors); + + const selectedSpanColor = isDarkMode + ? Color(color).lighten(0.7) + : Color(color).darken(0.7); + + if (span.hasError) { + color = `var(--bg-cherry-500)`; + } + + return ( + +
setHoveredSpanId(span.spanId)} + onMouseLeave={(): void => setHoveredSpanId('')} + onClick={(event): void => { + event.stopPropagation(); + event.preventDefault(); + searchParams.set('spanId', span.spanId); + history.replace({ search: searchParams.toString() }); + }} + /> + + ); + })} +
+ ), + // eslint-disable-next-line react-hooks/exhaustive-deps + [traceMetadata.endTime, traceMetadata.startTime, selectedSpan, hoveredSpanId], + ); + + const handleRangeChanged = useCallback( + (range: ListRange) => { + // if there are less than 50 levels on any load that means a single API call is sufficient + if (spans.length < 50) { + return; + } + + const { startIndex, endIndex } = range; + if (startIndex === 0 && spans[0][0].level !== 0) { + setFirstSpanAtFetchLevel(spans[0][0].spanId); + } + + if (endIndex === spans.length - 1) { + setFirstSpanAtFetchLevel(spans[spans.length - 1][0].spanId); + } + }, + [setFirstSpanAtFetchLevel, spans], + ); + + useEffect(() => { + const index = spans.findIndex( + (span) => span[0].spanId === firstSpanAtFetchLevel, + ); + + virtuosoRef.current?.scrollToIndex({ + index, + behavior: 'auto', + }); + }, [firstSpanAtFetchLevel, spans]); + + return ( + <> +
+ +
+ + + ); +} + +export default Success; diff --git a/frontend/src/container/PaginatedTraceFlamegraph/constants.ts b/frontend/src/container/PaginatedTraceFlamegraph/constants.ts new file mode 100644 index 00000000000..2f7cb4958bf --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/constants.ts @@ -0,0 +1,7 @@ +export enum TraceFlamegraphStates { + LOADING = 'LOADING', + SUCCESS = 'SUCCESS', + NO_DATA = 'NO_DATA', + ERROR = 'ERROR', + FETCHING_WITH_OLD_DATA_PRESENT = 'FETCHING_WTIH_OLD_DATA_PRESENT', +} diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx index 2e9f8341e33..577b72ff839 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchDropdown.tsx @@ -65,7 +65,7 @@ export default function QueryBuilderSearchDropdown(
)} {menu} - {!searchValue && tags.length === 0 && ( + {!searchValue && tags.length === 0 && exampleQueries.length > 0 && (
Example Queries
diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss index 60eec0bdb65..c7a96456e92 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.styles.scss @@ -9,7 +9,7 @@ .show-all-filters { .content { .rc-virtual-list-holder { - height: 100px; + height: 115px; } } } diff --git a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx index e319ae3d9ee..48ee2d0a604 100644 --- a/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx +++ b/frontend/src/container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2.tsx @@ -224,7 +224,7 @@ function QueryBuilderSearchV2( const { data, isFetching } = useGetAggregateKeys( { - searchText: searchValue, + searchText: searchValue?.split(' ')[0], dataSource: query.dataSource, aggregateOperator: query.aggregateOperator, aggregateAttribute: query.aggregateAttribute.key, diff --git a/frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.styles.scss b/frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.styles.scss new file mode 100644 index 00000000000..5c686e5aec0 --- /dev/null +++ b/frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.styles.scss @@ -0,0 +1,89 @@ +.attributes-corner { + display: flex; + flex-direction: column; + + .no-data { + height: 400px; + justify-content: center; + align-items: center; + } + + .search-input { + margin: 12px; + width: auto; + } + + .attributes-container { + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px; + + .item { + display: flex; + flex-direction: column; + gap: 8px; + justify-content: flex-start; + + .item-key { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .value-wrapper { + display: flex; + padding: 2px 8px; + align-items: center; + width: fit-content; + max-width: 100%; + gap: 8px; + border-radius: 50px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-slate-500); + .item-value { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: 0.56px; + } + } + } + } + + .border-top { + border-top: 1px solid var(--bg-slate-400); + } +} + +.lightMode { + .attributes-corner { + .attributes-container { + .item { + .item-key { + color: var(--bg-ink-100); + } + + .value-wrapper { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + + .item-value { + color: var(--bg-ink-400); + } + } + } + } + + .border-top { + border-top: 1px solid var(--bg-vanilla-300); + } + } +} diff --git a/frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.tsx b/frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.tsx new file mode 100644 index 00000000000..4199aa37ec7 --- /dev/null +++ b/frontend/src/container/SpanDetailsDrawer/Attributes/Attributes.tsx @@ -0,0 +1,65 @@ +import './Attributes.styles.scss'; + +import { Input, Tooltip, Typography } from 'antd'; +import cx from 'classnames'; +import { flattenObject } from 'container/LogDetailedView/utils'; +import { useMemo, useState } from 'react'; +import { Span } from 'types/api/trace/getTraceV2'; + +import NoData from '../NoData/NoData'; + +interface IAttributesProps { + span: Span; + isSearchVisible: boolean; +} + +function Attributes(props: IAttributesProps): JSX.Element { + const { span, isSearchVisible } = props; + const [fieldSearchInput, setFieldSearchInput] = useState(''); + + const flattenSpanData: Record = useMemo( + () => (span.tagMap ? flattenObject(span.tagMap) : {}), + [span], + ); + + const datasource = Object.keys(flattenSpanData) + .filter((attribute) => + attribute.toLowerCase().includes(fieldSearchInput.toLowerCase()), + ) + .map((key) => ({ field: key, value: flattenSpanData[key] })); + + return ( +
+ {datasource.length === 0 && } + {isSearchVisible && datasource.length > 0 && ( + setFieldSearchInput(e.target.value)} + /> + )} +
+ {datasource.map((item) => ( +
+ + {item.field} + +
+ + + {item.value} + + +
+
+ ))} +
+
+ ); +} + +export default Attributes; diff --git a/frontend/src/container/SpanDetailsDrawer/Events/Events.styles.scss b/frontend/src/container/SpanDetailsDrawer/Events/Events.styles.scss new file mode 100644 index 00000000000..70382bf7152 --- /dev/null +++ b/frontend/src/container/SpanDetailsDrawer/Events/Events.styles.scss @@ -0,0 +1,188 @@ +.events-table { + .no-events { + display: flex; + justify-content: center; + align-items: center; + height: 400px; + } + .events-container { + display: flex; + flex-direction: column; + gap: 12px; + padding: 12px; + + .event { + .ant-collapse { + border: none; + } + .ant-collapse-content { + border-top: none; + } + + .ant-collapse-item { + border-bottom: 0px; + } + + .ant-collapse-content-box { + border: 1px solid var(--bg-slate-500); + border-top: none; + } + .ant-collapse-header { + display: flex; + padding: 8px 6px; + align-items: center; + justify-content: space-between; + gap: 16px; + border-radius: 2px; + border: 1px solid var(--bg-slate-500); + background: var(--bg-ink-400); + + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + + .ant-collapse-expand-icon { + padding-inline-start: 0px; + padding-inline-end: 0px; + } + + .collapse-title { + display: flex; + align-items: center; + gap: 6px; + + .diamond { + fill: var(--bg-cherry-500); + } + } + } + .event-details { + display: flex; + flex-direction: column; + gap: 16px; + + .attribute-container { + display: flex; + flex-direction: column; + gap: 8px; + + .attribute-key { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .timestamp-container { + display: flex; + gap: 4px; + align-items: center; + + .timestamp-text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .attribute-value { + display: flex; + padding: 2px 8px; + width: fit-content; + align-items: center; + gap: 8px; + border-radius: 50px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-slate-500); + + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + + .wrapper { + display: flex; + padding: 2px 8px; + width: fit-content; + max-width: 100%; + align-items: center; + gap: 8px; + border-radius: 50px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-slate-500); + .attribute-value { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + } + } + } + } +} + +.lightMode { + .events-table { + .events-container { + .event { + .ant-collapse-content-box { + border: 1px solid var(--bg-vanilla-300); + } + .ant-collapse-header { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + color: var(--bg-ink-100); + border-bottom: none; + } + .event-details { + .attribute-container { + .attribute-key { + color: var(--bg-ink-400); + } + + .timestamp-container { + .timestamp-text { + color: var(--bg-ink-400); + } + + .attribute-value { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + + color: var(--bg-ink-100); + } + } + + .wrapper { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + .attribute-value { + color: var(--bg-ink-100); + } + } + } + } + } + } + } +} diff --git a/frontend/src/container/SpanDetailsDrawer/Events/Events.tsx b/frontend/src/container/SpanDetailsDrawer/Events/Events.tsx new file mode 100644 index 00000000000..e339f3f9a25 --- /dev/null +++ b/frontend/src/container/SpanDetailsDrawer/Events/Events.tsx @@ -0,0 +1,118 @@ +import './Events.styles.scss'; + +import { Collapse, Input, Tooltip, Typography } from 'antd'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; +import { Diamond } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { Event, Span } from 'types/api/trace/getTraceV2'; + +import NoData from '../NoData/NoData'; + +interface IEventsTableProps { + span: Span; + startTime: number; + isSearchVisible: boolean; +} + +function EventsTable(props: IEventsTableProps): JSX.Element { + const { span, startTime, isSearchVisible } = props; + const [fieldSearchInput, setFieldSearchInput] = useState(''); + const events: Event[] = useMemo(() => { + const tempEvents = []; + for (let i = 0; i < span.event?.length; i++) { + const parsedEvent = JSON.parse(span.event[i]); + tempEvents.push(parsedEvent); + } + return tempEvents; + }, [span.event]); + + return ( +
+ {events.length === 0 && ( +
+ +
+ )} +
+ {isSearchVisible && ( + setFieldSearchInput(e.target.value)} + /> + )} + {events + .filter((eve) => + eve.name.toLowerCase().includes(fieldSearchInput.toLowerCase()), + ) + .map((event) => ( +
+ + + + {event.name} + +
+ ), + children: ( +
+
+ + Start Time + +
+ + {getYAxisFormattedValue( + `${event.timeUnixNano / 1e6 - startTime}`, + 'ms', + )} + + + after the start + +
+
+ {event.attributeMap && + Object.keys(event.attributeMap).map((attributeKey) => ( +
+ + + {attributeKey} + + + +
+ + + {event.attributeMap[attributeKey]} + + +
+
+ ))} +
+ ), + }, + ]} + /> +
+ ))} +
+
+ ); +} + +export default EventsTable; diff --git a/frontend/src/container/SpanDetailsDrawer/NoData/NoData.styles.scss b/frontend/src/container/SpanDetailsDrawer/NoData/NoData.styles.scss new file mode 100644 index 00000000000..e40fb340a9e --- /dev/null +++ b/frontend/src/container/SpanDetailsDrawer/NoData/NoData.styles.scss @@ -0,0 +1,28 @@ +.no-data { + display: flex; + gap: 4px; + flex-direction: column; + + .no-data-img { + height: 32px; + width: 32px; + } + + .no-data-text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } +} + +.lightMode { + .no-data { + .no-data-text { + color: var(--bg-ink-400); + } + } +} diff --git a/frontend/src/container/SpanDetailsDrawer/NoData/NoData.tsx b/frontend/src/container/SpanDetailsDrawer/NoData/NoData.tsx new file mode 100644 index 00000000000..df4e5f38c3b --- /dev/null +++ b/frontend/src/container/SpanDetailsDrawer/NoData/NoData.tsx @@ -0,0 +1,22 @@ +import './NoData.styles.scss'; + +import { Typography } from 'antd'; + +interface INoDataProps { + name: string; +} + +function NoData(props: INoDataProps): JSX.Element { + const { name } = props; + + return ( +
+ no-data + + No {name} found for selected span + +
+ ); +} + +export default NoData; diff --git a/frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.styles.scss b/frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.styles.scss new file mode 100644 index 00000000000..572f57396ae --- /dev/null +++ b/frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.styles.scss @@ -0,0 +1,263 @@ +.span-details-drawer { + display: flex; + flex-direction: column; + width: 330px; + border-left: 1px solid var(--bg-slate-400); + overflow-y: auto; + + &::-webkit-scrollbar { + width: 0.1rem; + } + + .header { + display: flex; + align-items: center; + justify-content: space-between; + height: 48px; + padding: 12px; + border-bottom: 1px solid var(--bg-slate-400); + + .heading { + display: flex; + align-items: center; + gap: 8px; + + .dot { + height: 8px; + width: 8px; + border-radius: 2px; + background: var(--bg-cherry-500); + } + + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + } + + .description { + display: flex; + flex-direction: column; + gap: 16px; + padding: 10px 12px; + + .item { + display: flex; + flex-direction: column; + gap: 8px; + + .attribute-key { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 11px; + font-style: normal; + font-weight: 500; + line-height: 18px; /* 163.636% */ + letter-spacing: 0.44px; + text-transform: uppercase; + } + + .value-wrapper { + display: flex; + padding: 2px 8px; + align-items: center; + width: fit-content; + max-width: 100%; + border-radius: 50px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-slate-500); + + .attribute-value { + color: var(--bg-vanilla-400); + font-family: 'Inter'; + font-size: 14px; + font-style: normal; + width: 100%; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: 0.28px; + } + } + + .service { + display: flex; + padding: 2px 8px; + align-items: center; + gap: 8px; + border-radius: 50px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-slate-500); + width: fit-content; + + .dot { + height: 4px; + width: 4px; + } + + .value-wrapper { + display: flex; + padding: 0px; + align-items: center; + width: fit-content; + max-width: 100%; + border-radius: 0px; + border: none; + background: var(--bg-slate-500); + + .service-value { + color: var(--bg-vanilla-400); + font-family: 'Inter'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: 0.28px; + } + } + } + } + } + + .related-logs { + display: flex; + align-items: center; + justify-content: center; + width: fit-content; + padding: 5px 12px; + margin: 10px 12px; + box-shadow: none; + + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .attributes-events { + .details-drawer-tabs { + .ant-tabs-extra-content { + display: flex; + align-items: center; + + .search-icon { + width: 33px; + padding-right: 12px; + } + } + + .ant-tabs-nav::before { + border-bottom: 1px solid var(--bg-slate-400) !important; + } + + .attributes-tab-btn { + display: flex; + align-items: center; + } + .attributes-tab-btn:hover { + background: unset; + } + + .events-tab-btn { + display: flex; + align-items: center; + } + + .events-tab-btn:hover { + background: unset; + } + } + } +} + +.span-details-drawer-docked { + width: 48px; + + .header { + justify-content: center; + } +} + +.lightMode { + .span-details-drawer { + border-left: 1px solid var(--bg-vanilla-300); + + .header { + border-bottom: 1px solid var(--bg-vanilla-300); + + .heading { + .text { + color: var(--bg-ink-400); + } + } + } + + .description { + .item { + .attribute-key { + color: var(--bg-ink-400); + } + + .value-wrapper { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + + .attribute-value { + color: var(--bg-ink-400); + } + } + + .service { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + + .value-wrapper { + background: var(--bg-vanilla-300); + border: none; + + .service-value { + color: var(--bg-ink-400); + } + } + } + } + } + + .related-logs { + color: var(--bg-ink-400); + } + + .attributes-events { + .details-drawer-tabs { + .ant-tabs-nav::before { + border-bottom: 1px solid var(--bg-vanilla-300) !important; + } + + .ant-tabs-nav-wrap { + height: 32px; + } + + .ant-tabs-tab { + border: none; + background-color: var(--bg-vanilla-200); + + .ant-btn { + border-bottom: 1px solid var(--bg-vanilla-300); + } + } + + .ant-tabs-ink-bar { + background: #4e74f8 !important; + } + } + } + } +} diff --git a/frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.tsx b/frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.tsx new file mode 100644 index 00000000000..222202ba446 --- /dev/null +++ b/frontend/src/container/SpanDetailsDrawer/SpanDetailsDrawer.tsx @@ -0,0 +1,208 @@ +import './SpanDetailsDrawer.styles.scss'; + +import { Button, Tabs, TabsProps, Tooltip, Typography } from 'antd'; +import cx from 'classnames'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; +import { QueryParams } from 'constants/query'; +import ROUTES from 'constants/routes'; +import { themeColors } from 'constants/theme'; +import { getTraceToLogsQuery } from 'container/TraceDetail/SelectedSpanDetails/config'; +import createQueryParams from 'lib/createQueryParams'; +import history from 'lib/history'; +import { generateColor } from 'lib/uPlotLib/utils/generateColor'; +import { Anvil, Bookmark, PanelRight, Search } from 'lucide-react'; +import { Dispatch, SetStateAction, useState } from 'react'; +import { Span } from 'types/api/trace/getTraceV2'; +import { formatEpochTimestamp } from 'utils/timeUtils'; + +import Attributes from './Attributes/Attributes'; +import Events from './Events/Events'; + +interface ISpanDetailsDrawerProps { + isSpanDetailsDocked: boolean; + setIsSpanDetailsDocked: Dispatch>; + selectedSpan: Span | undefined; + traceID: string; + traceStartTime: number; + traceEndTime: number; +} + +function SpanDetailsDrawer(props: ISpanDetailsDrawerProps): JSX.Element { + const { + isSpanDetailsDocked, + setIsSpanDetailsDocked, + selectedSpan, + traceStartTime, + traceID, + traceEndTime, + } = props; + + const [isSearchVisible, setIsSearchVisible] = useState(false); + const color = generateColor( + selectedSpan?.serviceName || '', + themeColors.traceDetailColors, + ); + + function getItems(span: Span, startTime: number): TabsProps['items'] { + return [ + { + label: ( + + ), + key: 'attributes', + children: , + }, + { + label: ( + + ), + key: 'events', + children: ( + + ), + }, + ]; + } + const onLogsHandler = (): void => { + const query = getTraceToLogsQuery(traceID, traceStartTime, traceEndTime); + + history.push( + `${ROUTES.LOGS_EXPLORER}?${createQueryParams({ + [QueryParams.compositeQuery]: JSON.stringify(query), + // we subtract 1000 milliseconds from the start time to handle the cases when the trace duration is in nanoseconds + [QueryParams.startTime]: traceStartTime - 1000, + // we add 1000 milliseconds to the end time for nano second duration traces + [QueryParams.endTime]: traceEndTime + 1000, + })}`, + ); + }; + + return ( +
+
+ {!isSpanDetailsDocked && ( +
+
+ Span Details +
+ )} + setIsSpanDetailsDocked((prev) => !prev)} + /> +
+ {selectedSpan && !isSpanDetailsDocked && ( + <> +
+
+ span name + +
+ + {selectedSpan.name} + +
+
+
+
+ span id +
+ + {selectedSpan.spanId} + +
+
+
+ start time +
+ + {formatEpochTimestamp(selectedSpan.timestamp)} + +
+
+
+ duration +
+ + {getYAxisFormattedValue(`${selectedSpan.durationNano}`, 'ns')} + +
+
+
+ service +
+
+
+ + + {selectedSpan.serviceName} + + +
+
+
+
+ span kind +
+ + {selectedSpan.spanKind} + +
+
+
+ + status code string + +
+ + {selectedSpan.statusCodeString} + +
+
+
+ + + +
+ setIsSearchVisible((prev) => !prev)} + /> + } + /> +
+ + )} +
+ ); +} + +export default SpanDetailsDrawer; diff --git a/frontend/src/container/TraceMetadata/TraceMetadata.styles.scss b/frontend/src/container/TraceMetadata/TraceMetadata.styles.scss new file mode 100644 index 00000000000..3abad00ca46 --- /dev/null +++ b/frontend/src/container/TraceMetadata/TraceMetadata.styles.scss @@ -0,0 +1,262 @@ +.trace-metadata { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0px 16px 0px 16px; + + .metadata-info { + display: flex; + flex-direction: column; + gap: 10px; + + .first-row { + display: flex; + align-items: center; + + .previous-btn { + display: flex; + height: 30px; + padding: 6px 8px; + align-items: center; + gap: 4px; + border: 1px solid var(--bg-slate-300); + background: var(--bg-slate-500); + border-radius: 4px; + box-shadow: none; + } + + .trace-name { + display: flex; + padding: 6px 8px; + margin-left: 6px; + align-items: center; + gap: 4px; + border: 1px solid var(--bg-slate-300); + border-radius: 4px 0px 0px 4px; + background: var(--bg-slate-500); + + .drafting { + color: white; + } + + .trace-id { + color: #fff; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + } + + .trace-id-value { + display: flex; + padding: 6px 8px; + justify-content: center; + align-items: center; + gap: 10px; + background: var(--bg-slate-400); + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + border: 1px solid var(--bg-slate-300); + border-left: unset; + border-radius: 0px 4px 4px 0px; + } + } + + .second-row { + display: flex; + gap: 24px; + + .service-entry-info { + display: flex; + gap: 6px; + color: var(--bg-vanilla-400); + align-items: center; + + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .root-span-name { + display: flex; + padding: 2px 8px; + align-items: center; + gap: 8px; + border-radius: 50px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-slate-500); + } + } + + .trace-duration { + display: flex; + gap: 6px; + color: var(--bg-vanilla-400); + align-items: center; + + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + + .start-time-info { + display: flex; + gap: 6px; + color: var(--bg-vanilla-400); + align-items: center; + + .text { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + } + } + + .datapoints-info { + display: flex; + gap: 16px; + + .separator { + width: 1px; + background: #1d212d; + } + + .data-point { + display: flex; + flex-direction: column; + justify-content: space-between; + gap: 4px; + + .text { + color: var(--bg-vanilla-400); + text-align: center; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + + .value { + color: var(--bg-vanilla-100); + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on; + font-family: Inter; + font-size: 20px; + font-style: normal; + font-weight: 500; + line-height: 28px; /* 140% */ + letter-spacing: -0.1px; + text-transform: uppercase; + text-align: right; + } + } + } +} + +.lightMode { + .trace-metadata { + .metadata-info { + .first-row { + .previous-btn { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-200); + } + + .trace-name { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-200); + border-right: none; + + .drafting { + color: var(--bg-ink-100); + } + + .trace-id { + color: var(--bg-ink-100); + } + } + + .trace-id-value { + background: var(--bg-vanilla-300); + color: var(--bg-ink-400); + border: 1px solid var(--bg-vanilla-300); + } + } + + .second-row { + .service-entry-info { + color: var(--bg-ink-400); + + .text { + color: var(--bg-ink-400); + } + + .root-span-name { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + } + } + + .trace-duration { + color: var(--bg-ink-400); + + .text { + color: var(--bg-ink-400); + } + } + + .start-time-info { + color: var(--bg-ink-400); + + .text { + color: var(--bg-ink-400); + } + } + } + } + + .datapoints-info { + .separator { + background: var(--bg-vanilla-300); + } + + .data-point { + .text { + color: var(--bg-ink-400); + } + + .value { + color: var(--bg-ink-100); + } + } + } + } +} diff --git a/frontend/src/container/TraceMetadata/TraceMetadata.tsx b/frontend/src/container/TraceMetadata/TraceMetadata.tsx new file mode 100644 index 00000000000..042ee654feb --- /dev/null +++ b/frontend/src/container/TraceMetadata/TraceMetadata.tsx @@ -0,0 +1,100 @@ +import './TraceMetadata.styles.scss'; + +import { Button, Tooltip, Typography } from 'antd'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; +import ROUTES from 'constants/routes'; +import history from 'lib/history'; +import { + ArrowLeft, + BetweenHorizonalStart, + CalendarClock, + DraftingCompass, + Timer, +} from 'lucide-react'; +import { formatEpochTimestamp } from 'utils/timeUtils'; + +export interface ITraceMetadataProps { + traceID: string; + rootServiceName: string; + rootSpanName: string; + startTime: number; + duration: number; + totalSpans: number; + totalErrorSpans: number; + notFound: boolean; +} + +function TraceMetadata(props: ITraceMetadataProps): JSX.Element { + const { + traceID, + rootServiceName, + rootSpanName, + startTime, + duration, + totalErrorSpans, + totalSpans, + notFound, + } = props; + return ( +
+
+
+ +
+ + Trace ID +
+ {traceID} +
+ {!notFound && ( +
+
+ + {rootServiceName} + — + + {rootSpanName} + +
+
+ + + + + {getYAxisFormattedValue(`${duration}`, 'ms')} + +
+
+ + + + + {formatEpochTimestamp(startTime * 1000)} + +
+
+ )} +
+ {!notFound && ( +
+
+ Total Spans + {totalSpans} +
+
+
+ Error Spans + {totalErrorSpans} +
+
+ )} +
+ ); +} + +export default TraceMetadata; diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfall.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfall.styles.scss new file mode 100644 index 00000000000..015fc601342 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfall.styles.scss @@ -0,0 +1,9 @@ +.trace-waterfall { + height: calc(70vh - 200px); + + .loading-skeleton { + justify-content: center; + align-items: center; + padding: 20px; + } +} diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfall.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfall.tsx new file mode 100644 index 00000000000..cd6db42f4f4 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfall.tsx @@ -0,0 +1,136 @@ +import './TraceWaterfall.styles.scss'; + +import { Skeleton } from 'antd'; +import { AxiosError } from 'axios'; +import Spinner from 'components/Spinner'; +import { Dispatch, SetStateAction, useMemo } from 'react'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { GetTraceV2SuccessResponse, Span } from 'types/api/trace/getTraceV2'; + +import { TraceWaterfallStates } from './constants'; +import Error from './TraceWaterfallStates/Error/Error'; +import NoData from './TraceWaterfallStates/NoData/NoData'; +import Success from './TraceWaterfallStates/Success/Success'; + +export interface IInterestedSpan { + spanId: string; + isUncollapsed: boolean; +} + +interface ITraceWaterfallProps { + traceId: string; + uncollapsedNodes: string[]; + traceData: + | SuccessResponse + | ErrorResponse + | undefined; + isFetchingTraceData: boolean; + errorFetchingTraceData: unknown; + interestedSpanId: IInterestedSpan; + setInterestedSpanId: Dispatch>; + setTraceFlamegraphStatsWidth: Dispatch>; + selectedSpan: Span | undefined; + setSelectedSpan: Dispatch>; +} + +function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element { + const { + traceData, + isFetchingTraceData, + errorFetchingTraceData, + interestedSpanId, + traceId, + uncollapsedNodes, + setInterestedSpanId, + setTraceFlamegraphStatsWidth, + setSelectedSpan, + selectedSpan, + } = props; + // get the current state of trace waterfall based on the API lifecycle + const traceWaterfallState = useMemo(() => { + if (isFetchingTraceData) { + if ( + traceData && + traceData.payload && + traceData.payload.spans && + traceData.payload.spans.length > 0 + ) { + return TraceWaterfallStates.FETCHING_WITH_OLD_DATA_PRESENT; + } + return TraceWaterfallStates.LOADING; + } + if (errorFetchingTraceData) { + return TraceWaterfallStates.ERROR; + } + if ( + traceData && + traceData.payload && + traceData.payload.spans && + traceData.payload.spans.length === 0 + ) { + return TraceWaterfallStates.NO_DATA; + } + + return TraceWaterfallStates.SUCCESS; + }, [errorFetchingTraceData, isFetchingTraceData, traceData]); + + // capture the spans from the response, since we do not need to do any manipulation on the same we will keep this as a simple constant [ memoized ] + const spans = useMemo(() => traceData?.payload?.spans || [], [ + traceData?.payload?.spans, + ]); + + // get the content based on the current state of the trace waterfall + const getContent = useMemo(() => { + switch (traceWaterfallState) { + case TraceWaterfallStates.LOADING: + return ( +
+ +
+ ); + case TraceWaterfallStates.ERROR: + return ; + case TraceWaterfallStates.NO_DATA: + return ; + case TraceWaterfallStates.SUCCESS: + case TraceWaterfallStates.FETCHING_WITH_OLD_DATA_PRESENT: + return ( + + ); + default: + return ; + } + }, [ + errorFetchingTraceData, + interestedSpanId, + selectedSpan, + setInterestedSpanId, + setSelectedSpan, + setTraceFlamegraphStatsWidth, + spans, + traceData?.payload?.endTimestampMillis, + traceData?.payload?.hasMissingSpans, + traceData?.payload?.startTimestampMillis, + traceId, + traceWaterfallState, + uncollapsedNodes, + ]); + + return
{getContent}
; +} + +export default TraceWaterfall; diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Error/Error.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Error/Error.styles.scss new file mode 100644 index 00000000000..6382b0157a9 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Error/Error.styles.scss @@ -0,0 +1,30 @@ +.error-waterfall { + display: flex; + padding: 12px; + margin: 20px; + gap: 12px; + align-items: flex-start; + border-radius: 4px; + background: var(--bg-cherry-500); + + .text { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + flex-shrink: 0; + } + + .value { + color: var(--bg-vanilla-100); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } +} diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Error/Error.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Error/Error.tsx new file mode 100644 index 00000000000..27b8428c0ef --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Error/Error.tsx @@ -0,0 +1,25 @@ +import './Error.styles.scss'; + +import { Tooltip, Typography } from 'antd'; +import { AxiosError } from 'axios'; + +interface IErrorProps { + error: AxiosError; +} + +function Error(props: IErrorProps): JSX.Element { + const { error } = props; + + return ( +
+ Something went wrong! + + + {error?.message} + + +
+ ); +} + +export default Error; diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/NoData/NoData.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/NoData/NoData.tsx new file mode 100644 index 00000000000..0be04ffc234 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/NoData/NoData.tsx @@ -0,0 +1,12 @@ +import { Typography } from 'antd'; + +interface INoDataProps { + id: string; +} + +function NoData(props: INoDataProps): JSX.Element { + const { id } = props; + return No Trace found with the id: {id} ; +} + +export default NoData; diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.styles.scss new file mode 100644 index 00000000000..421a0e6db60 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.styles.scss @@ -0,0 +1,60 @@ +.filter-row { + display: flex; + align-items: center; + padding: 16px 20px 0px 20px; + gap: 12px; + + .query-builder-search-v2 { + width: 100%; + } + + .pre-next-toggle { + display: flex; + flex-shrink: 0; + gap: 12px; + + .ant-typography { + display: flex; + align-items: center; + color: var(--bg-vanilla-400); + font-family: 'Geist Mono'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + } + + .ant-btn { + display: flex; + align-items: center; + justify-content: center; + box-shadow: none; + } + } + + .no-results { + display: flex; + align-items: center; + flex-shrink: 0; + color: var(--bg-vanilla-400); + font-family: 'Geist Mono'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 150% */ + } +} + +.lightMode { + .filter-row { + .pre-next-toggle { + .ant-typography { + color: var(--bg-ink-400); + } + } + + .no-results { + color: var(--bg-ink-400); + } + } +} diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx new file mode 100644 index 00000000000..e0bee45465a --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/Filters.tsx @@ -0,0 +1,181 @@ +import './Filters.styles.scss'; + +import { InfoCircleOutlined, LoadingOutlined } from '@ant-design/icons'; +import { Button, Spin, Tooltip, Typography } from 'antd'; +import { AxiosError } from 'axios'; +import { DEFAULT_ENTITY_VERSION } from 'constants/app'; +import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder'; +import QueryBuilderSearchV2 from 'container/QueryBuilder/filters/QueryBuilderSearchV2/QueryBuilderSearchV2'; +import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import { ChevronDown, ChevronUp } from 'lucide-react'; +import { useCallback, useEffect, useState } from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData'; +import { TracesAggregatorOperator } from 'types/common/queryBuilder'; + +import { BASE_FILTER_QUERY } from './constants'; + +function prepareQuery(filters: TagFilter, traceID: string): Query { + return { + ...initialQueriesMap.traces, + builder: { + ...initialQueriesMap.traces.builder, + queryData: [ + { + ...initialQueriesMap.traces.builder.queryData[0], + aggregateOperator: TracesAggregatorOperator.NOOP, + orderBy: [{ columnName: 'timestamp', order: 'asc' }], + filters: { + ...filters, + items: [ + ...filters.items, + { + id: '5ab8e1cf', + key: { + key: 'trace_id', + dataType: DataTypes.String, + type: '', + isColumn: true, + isJSON: false, + id: 'trace_id--string----true', + }, + op: '=', + value: traceID, + }, + ], + }, + }, + ], + }, + }; +} + +function Filters({ + startTime, + endTime, + traceID, +}: { + startTime: number; + endTime: number; + traceID: string; +}): JSX.Element { + const [filters, setFilters] = useState(BASE_FILTER_QUERY.filters); + const [noData, setNoData] = useState(false); + const [filteredSpanIds, setFilteredSpanIds] = useState([]); + const handleFilterChange = (value: TagFilter): void => { + setFilters(value); + }; + const [currentSearchedIndex, setCurrentSearchedIndex] = useState(0); + const { search } = useLocation(); + const history = useHistory(); + + const { isFetching, error } = useGetQueryRange( + { + query: prepareQuery(filters, traceID), + graphType: PANEL_TYPES.LIST, + selectedTime: 'GLOBAL_TIME', + start: startTime, + end: endTime, + params: { + dataSource: 'traces', + }, + tableParams: { + pagination: { + offset: 0, + limit: 200, + }, + selectColumns: [ + { + key: 'name', + dataType: 'string', + type: 'tag', + isColumn: true, + isJSON: false, + id: 'name--string--tag--true', + isIndexed: false, + }, + ], + }, + }, + DEFAULT_ENTITY_VERSION, + { + queryKey: [filters], + enabled: filters.items.length > 0, + onSuccess: (data) => { + if (data?.payload.data.newResult.data.result[0].list) { + setFilteredSpanIds( + data?.payload.data.newResult.data.result[0].list.map( + (val) => val.data.spanID, + ), + ); + setNoData(false); + } else { + setNoData(true); + setFilteredSpanIds([]); + setCurrentSearchedIndex(0); + } + }, + }, + ); + + const handlePrevNext = useCallback( + (id: number): void => { + const searchParams = new URLSearchParams(search); + searchParams.set('spanId', filteredSpanIds[id]); + history.replace({ search: searchParams.toString() }); + }, + [filteredSpanIds, history, search], + ); + + useEffect(() => { + if (filteredSpanIds.length > 0) { + handlePrevNext(0); + } + }, [filteredSpanIds, handlePrevNext]); + + return ( +
+ + {filteredSpanIds.length > 0 && ( +
+ + {currentSearchedIndex + 1} / {filteredSpanIds.length} + +
+ )} + {isFetching && } size="small" />} + {error && ( + + + + )} + {noData && ( + No results found + )} +
+ ); +} + +export default Filters; diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/constants.ts b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/constants.ts new file mode 100644 index 00000000000..aeaa46baef4 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Filters/constants.ts @@ -0,0 +1,40 @@ +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; +import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; +import { DataSource } from 'types/common/queryBuilder'; + +export const BASE_FILTER_QUERY: IBuilderQuery = { + queryName: 'A', + dataSource: DataSource.TRACES, + aggregateOperator: 'noop', + aggregateAttribute: { + id: '------false', + dataType: DataTypes.EMPTY, + key: '', + isColumn: false, + type: '', + isJSON: false, + }, + timeAggregation: 'rate', + spaceAggregation: 'sum', + functions: [], + filters: { + items: [], + op: 'AND', + }, + expression: 'A', + disabled: false, + stepInterval: 60, + having: [], + limit: 200, + orderBy: [ + { + columnName: 'timestamp', + order: 'desc', + }, + ], + groupBy: [], + legend: '', + reduceTo: 'avg', + offset: 0, + selectColumns: [], +}; diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.styles.scss new file mode 100644 index 00000000000..1b33cff2954 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.styles.scss @@ -0,0 +1,396 @@ +.success-content { + overflow-y: hidden; + overflow-x: hidden; + max-width: 100%; + + .missing-spans { + display: flex; + align-items: center; + justify-content: space-between; + height: 44px; + margin: 16px; + padding: 12px; + border-radius: 4px; + background: rgba(69, 104, 220, 0.1); + + .left-info { + display: flex; + align-items: center; + gap: 8px; + color: var(--bg-robin-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + + .text { + color: var(--bg-robin-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + + .right-info { + display: flex; + align-items: center; + justify-content: center; + flex-direction: row-reverse; + gap: 8px; + color: var(--bg-robin-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .right-info:hover { + background-color: unset; + color: var(--bg-robin-200); + } + } + + .waterfall-table { + height: calc(70vh - 220px); + overflow: auto; + overflow-x: hidden; + padding: 0px 20px 20px 20px; + + &::-webkit-scrollbar { + width: 0.1rem; + } + + // default table overrides css for table v3 + .div-table { + width: 100% !important; + border: none !important; + } + + .div-thead { + position: sticky; + top: 0; + z-index: 2; + background-color: var(--bg-ink-500) !important; + + .div-tr { + height: 16px; + } + } + + .div-tr { + display: flex; + width: 100%; + align-items: center; + height: 54px; + } + + .div-tr:hover { + border-radius: 4px; + background: rgba(171, 189, 255, 0.06) !important; + + .span-overview { + background: unset !important; + + .span-overview-content { + background: unset !important; + } + } + } + + .div-th, + .div-td { + box-shadow: none; + padding: 0px !important; + } + + .div-th { + padding: 2px 4px; + position: relative; + font-weight: bold; + text-align: center; + } + + .div-td { + display: flex; + height: 54px; + align-items: center; + overflow: hidden; + + .span-overview { + display: flex; + align-items: center; + flex-shrink: 0; + height: 100%; + width: 100%; + cursor: pointer; + + .connector-lines { + display: flex; + } + + .span-overview-content { + display: flex; + flex-shrink: 0; + flex-direction: column; + align-items: flex-start; + gap: 5px; + width: 100%; + background-color: #0b0c0e; + height: 100%; + justify-content: center; + + .first-row { + display: flex; + align-items: center; + justify-content: space-between; + height: 20px; + width: 100%; + + .span-det { + display: flex; + gap: 6px; + flex-shrink: 0; + + .collapse-uncollapse-button { + display: flex; + align-items: center; + justify-content: center; + padding: 4px 4px; + gap: 4px; + border-radius: 2px; + border: 1px solid var(--bg-slate-400); + background: var(--bg-slate-500); + box-shadow: none; + height: 20px; + + .children-count { + color: var(--bg-vanilla-400); + font-family: 'Space Mono'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: 0.28px; + } + } + + .span-name { + color: #fff; + font-family: 'Inter'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: 0.28px; + } + } + + .status-code-container { + display: flex; + padding-right: 10px; + + .status-code { + display: flex; + height: 20px; + padding: 3px; + align-items: center; + border-radius: 3px; + } + + .success { + border: 1px solid var(--bg-robin-500); + background: var(--bg-robin-500); + } + + .error { + border: 1px solid var(--bg-cherry-500); + background: var(--bg-cherry-500); + } + } + } + + .second-row { + display: flex; + align-items: center; + gap: 8px; + height: 18px; + width: 100%; + .service-name { + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + } + } + } + + .span-duration { + display: flex; + flex-direction: column; + height: 54px; + position: relative; + width: 100%; + cursor: pointer; + + .span-line { + position: absolute; + height: 12px; + top: 35%; + border-radius: 6px; + } + + .span-line-text { + position: absolute; + top: 65%; + font-variant-numeric: lining-nums tabular-nums stacked-fractions + slashed-zero; + font-feature-settings: 'case' on, 'cpsp' on, 'dlig' on, 'salt' on; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + } + + .interested-span { + border-radius: 4px; + background: rgba(171, 189, 255, 0.06) !important; + + .span-overview-content { + background: unset; + } + } + } + + .div-td + .div-td { + border-left: 1px solid var(--bg-slate-400); + } + + .div-th + .div-th { + border-left: 1px solid var(--bg-slate-400); + } + + .div-tr .div-th:nth-child(2) { + width: calc(100% - var(--header-span-name-size) * 1px) !important; + } + .div-tr .div-td:nth-child(2) { + width: calc(100% - var(--header-span-name-size) * 1px) !important; + } + .resizer { + width: 10px !important; + position: absolute; + top: 0; + height: calc(70vh - 200px); + right: 0; + width: 2px; + background: rgba(35, 196, 248, 0.2); + cursor: col-resize; + user-select: none; + touch-action: none; + } + + .resizer.isResizing { + background: rgba(35, 196, 248, 0.2); + opacity: 1; + } + + @media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } + } + } + + .missing-spans-waterfall-table { + height: calc(70vh - 280px); + } +} + +.span-dets { + .related-logs { + display: flex; + width: 160px; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 2px; + border: 1px solid var(--Slate-400, #1d212d); + background: var(--Slate-500, #161922); + box-shadow: none; + } +} + +.lightMode { + .success-content { + .waterfall-table { + .div-td { + .span-overview { + .span-overview-content { + background-color: var(--bg-vanilla-200); + .first-row { + .collapse-uncollapse-button { + border: 1px solid var(--bg-vanilla-400); + background: var(--bg-vanilla-400); + + .children-count { + color: var(--bg-ink-400); + } + } + + .span-name { + color: var(--bg-ink-400); + } + } + + .second-row { + .service-name { + color: var(--bg-ink-400); + } + } + } + } + + .interested-span { + border-radius: 4px; + background: var(--bg-vanilla-300); + } + } + + .div-td + .div-td { + border-left: 1px solid var(--bg-vanilla-300); + } + + .div-th + .div-th { + border-left: 1px solid var(--bg-vanilla-300); + } + + .div-thead { + background-color: var(--bg-vanilla-200) !important; + } + } + } + .span-dets { + .related-logs { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + } + } +} diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx new file mode 100644 index 00000000000..73cd7b0e16f --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx @@ -0,0 +1,381 @@ +/* eslint-disable jsx-a11y/no-static-element-interactions */ +/* eslint-disable jsx-a11y/click-events-have-key-events */ +import './Success.styles.scss'; + +import { ColumnDef, createColumnHelper } from '@tanstack/react-table'; +import { Virtualizer } from '@tanstack/react-virtual'; +import { Button, Tooltip, Typography } from 'antd'; +import cx from 'classnames'; +import { TableV3 } from 'components/TableV3/TableV3'; +import { themeColors } from 'constants/theme'; +import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils'; +import { IInterestedSpan } from 'container/TraceWaterfall/TraceWaterfall'; +import { generateColor } from 'lib/uPlotLib/utils/generateColor'; +import { + AlertCircle, + ArrowUpRight, + ChevronDown, + ChevronRight, + Leaf, +} from 'lucide-react'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; +import { Span } from 'types/api/trace/getTraceV2'; +import { toFixed } from 'utils/toFixed'; + +import Filters from './Filters/Filters'; + +// css config +const CONNECTOR_WIDTH = 28; +const VERTICAL_CONNECTOR_WIDTH = 1; + +interface ITraceMetadata { + traceId: string; + startTime: number; + endTime: number; + hasMissingSpans: boolean; +} +interface ISuccessProps { + spans: Span[]; + traceMetadata: ITraceMetadata; + interestedSpanId: IInterestedSpan; + uncollapsedNodes: string[]; + setInterestedSpanId: Dispatch>; + setTraceFlamegraphStatsWidth: Dispatch>; + selectedSpan: Span | undefined; + setSelectedSpan: Dispatch>; +} + +function SpanOverview({ + span, + isSpanCollapsed, + handleCollapseUncollapse, + setSelectedSpan, + selectedSpan, +}: { + span: Span; + isSpanCollapsed: boolean; + handleCollapseUncollapse: (id: string, collapse: boolean) => void; + selectedSpan: Span | undefined; + setSelectedSpan: Dispatch>; +}): JSX.Element { + const isRootSpan = span.level === 0; + const spanRef = useRef(null); + + let color = generateColor(span.serviceName, themeColors.traceDetailColors); + if (span.hasError) { + color = `var(--bg-cherry-500)`; + } + + return ( +
')`, + backgroundRepeat: 'repeat', + backgroundSize: `${CONNECTOR_WIDTH + 1}px 54px`, + }} + onClick={(): void => { + setSelectedSpan(span); + }} + > + {!isRootSpan && ( +
+
+
+ )} +
+
+
+ {span.hasChildren ? ( + + ) : ( + + )} + {span.name} +
+
+
+
+ + {span.serviceName} + +
+
+
+ ); +} + +function SpanDuration({ + span, + traceMetadata, + setSelectedSpan, + selectedSpan, +}: { + span: Span; + traceMetadata: ITraceMetadata; + selectedSpan: Span | undefined; + setSelectedSpan: Dispatch>; +}): JSX.Element { + const { time, timeUnitName } = convertTimeToRelevantUnit( + span.durationNano / 1e6, + ); + + const spread = traceMetadata.endTime - traceMetadata.startTime; + const leftOffset = ((span.timestamp - traceMetadata.startTime) * 1e2) / spread; + const width = (span.durationNano * 1e2) / (spread * 1e6); + + let color = generateColor(span.serviceName, themeColors.traceDetailColors); + + if (span.hasError) { + color = `var(--bg-cherry-500)`; + } + + return ( +
{ + setSelectedSpan(span); + }} + > +
+ + {`${toFixed(time, 2)} ${timeUnitName}`} + +
+ ); +} + +// table config +const columnDefHelper = createColumnHelper(); + +function getWaterfallColumns({ + handleCollapseUncollapse, + uncollapsedNodes, + traceMetadata, + selectedSpan, + setSelectedSpan, +}: { + handleCollapseUncollapse: (id: string, collapse: boolean) => void; + uncollapsedNodes: string[]; + traceMetadata: ITraceMetadata; + selectedSpan: Span | undefined; + setSelectedSpan: Dispatch>; +}): ColumnDef[] { + const waterfallColumns: ColumnDef[] = [ + columnDefHelper.display({ + id: 'span-name', + header: '', + cell: (props): JSX.Element => ( + + ), + size: 450, + }), + columnDefHelper.display({ + id: 'span-duration', + header: () =>
, + enableResizing: false, + cell: (props): JSX.Element => ( + + ), + }), + ]; + + return waterfallColumns; +} + +function Success(props: ISuccessProps): JSX.Element { + const { + spans, + traceMetadata, + interestedSpanId, + uncollapsedNodes, + setInterestedSpanId, + setTraceFlamegraphStatsWidth, + setSelectedSpan, + selectedSpan, + } = props; + const virtualizerRef = useRef>(); + + const handleCollapseUncollapse = useCallback( + (spanId: string, collapse: boolean) => { + setInterestedSpanId({ spanId, isUncollapsed: !collapse }); + }, + [setInterestedSpanId], + ); + + const handleVirtualizerInstanceChanged = ( + instance: Virtualizer, + ): void => { + const { range } = instance; + if (spans.length < 500) return; + + if (range?.startIndex === 0 && instance.isScrolling) { + if (spans[0].level !== 0) { + setInterestedSpanId({ spanId: spans[0].spanId, isUncollapsed: false }); + } + return; + } + + if (range?.endIndex === spans.length - 1 && instance.isScrolling) { + setInterestedSpanId({ + spanId: spans[spans.length - 1].spanId, + isUncollapsed: false, + }); + } + }; + + const columns = useMemo( + () => + getWaterfallColumns({ + handleCollapseUncollapse, + uncollapsedNodes, + traceMetadata, + selectedSpan, + setSelectedSpan, + }), + [ + handleCollapseUncollapse, + uncollapsedNodes, + traceMetadata, + selectedSpan, + setSelectedSpan, + ], + ); + + useEffect(() => { + if (interestedSpanId.spanId !== '' && virtualizerRef.current) { + const idx = spans.findIndex( + (span) => span.spanId === interestedSpanId.spanId, + ); + if (idx !== -1) { + setTimeout(() => { + virtualizerRef.current?.scrollToIndex(idx, { + align: 'center', + behavior: 'auto', + }); + }, 400); + + setSelectedSpan(spans[idx]); + } + } else { + setSelectedSpan((prev) => { + if (!prev) { + return spans[0]; + } + return prev; + }); + } + }, [interestedSpanId, setSelectedSpan, spans]); + + return ( +
+ {traceMetadata.hasMissingSpans && ( +
+
+ + + This trace has missing spans + +
+ +
+ )} + + +
+ ); +} + +export default Success; diff --git a/frontend/src/container/TraceWaterfall/constants.ts b/frontend/src/container/TraceWaterfall/constants.ts new file mode 100644 index 00000000000..e9056512f4f --- /dev/null +++ b/frontend/src/container/TraceWaterfall/constants.ts @@ -0,0 +1,7 @@ +export enum TraceWaterfallStates { + LOADING = 'LOADING', + SUCCESS = 'SUCCESS', + NO_DATA = 'NO_DATA', + ERROR = 'ERROR', + FETCHING_WITH_OLD_DATA_PRESENT = 'FETCHING_WTIH_OLD_DATA_PRESENT', +} diff --git a/frontend/src/hooks/trace/useGetTraceFlamegraph.tsx b/frontend/src/hooks/trace/useGetTraceFlamegraph.tsx new file mode 100644 index 00000000000..a7444d3a20e --- /dev/null +++ b/frontend/src/hooks/trace/useGetTraceFlamegraph.tsx @@ -0,0 +1,31 @@ +import getTraceFlamegraph from 'api/trace/getTraceFlamegraph'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useQuery, UseQueryResult } from 'react-query'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + GetTraceFlamegraphPayloadProps, + GetTraceFlamegraphSuccessResponse, +} from 'types/api/trace/getTraceFlamegraph'; + +const useGetTraceFlamegraph = ( + props: GetTraceFlamegraphPayloadProps, +): UseLicense => + useQuery({ + queryFn: () => getTraceFlamegraph(props), + // if any of the props changes then we need to trigger an API call as the older data will be obsolete + queryKey: [ + REACT_QUERY_KEY.GET_TRACE_V2_FLAMEGRAPH, + props.traceId, + props.selectedSpanId, + ], + enabled: !!props.traceId, + keepPreviousData: true, + refetchOnWindowFocus: false, + }); + +type UseLicense = UseQueryResult< + SuccessResponse | ErrorResponse, + unknown +>; + +export default useGetTraceFlamegraph; diff --git a/frontend/src/hooks/trace/useGetTraceV2.tsx b/frontend/src/hooks/trace/useGetTraceV2.tsx new file mode 100644 index 00000000000..90b821a187d --- /dev/null +++ b/frontend/src/hooks/trace/useGetTraceV2.tsx @@ -0,0 +1,30 @@ +import getTraceV2 from 'api/trace/getTraceV2'; +import { REACT_QUERY_KEY } from 'constants/reactQueryKeys'; +import { useQuery, UseQueryResult } from 'react-query'; +import { ErrorResponse, SuccessResponse } from 'types/api'; +import { + GetTraceV2PayloadProps, + GetTraceV2SuccessResponse, +} from 'types/api/trace/getTraceV2'; + +const useGetTraceV2 = (props: GetTraceV2PayloadProps): UseLicense => + useQuery({ + queryFn: () => getTraceV2(props), + // if any of the props changes then we need to trigger an API call as the older data will be obsolete + queryKey: [ + REACT_QUERY_KEY.GET_TRACE_V2_WATERFALL, + props.traceId, + props.selectedSpanId, + props.isSelectedSpanIDUnCollapsed, + ], + enabled: !!props.traceId, + keepPreviousData: true, + refetchOnWindowFocus: false, + }); + +type UseLicense = UseQueryResult< + SuccessResponse | ErrorResponse, + unknown +>; + +export default useGetTraceV2; diff --git a/frontend/src/pages/TraceDetail/TraceDetail.styles.scss b/frontend/src/pages/TraceDetail/TraceDetail.styles.scss new file mode 100644 index 00000000000..bf1696ebe4d --- /dev/null +++ b/frontend/src/pages/TraceDetail/TraceDetail.styles.scss @@ -0,0 +1,45 @@ +.old-trace-container { + display: flex; + flex-direction: column; + height: 100%; + + .top-header { + display: flex; + flex-direction: row-reverse; + padding: 5px; + border-bottom: 1px solid var(--bg-slate-400); + + .new-cta-btn { + display: flex; + padding: 4px 6px; + align-items: center; + gap: 8px; + color: var(--Vanilla-400, #c0c1c3); + + /* Bifrost (Ancient)/Content/sm */ + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + box-shadow: none; + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + } +} + +.lightMode { + .old-trace-container { + .top-header { + border-bottom: 1px solid var(--bg-vanilla-300); + + .new-cta-btn { + color: var(--bg-ink-400); + } + } + } +} diff --git a/frontend/src/pages/TraceDetail/index.tsx b/frontend/src/pages/TraceDetail/index.tsx index f936bd2d0c0..3ab48562675 100644 --- a/frontend/src/pages/TraceDetail/index.tsx +++ b/frontend/src/pages/TraceDetail/index.tsx @@ -1,10 +1,14 @@ -import { Typography } from 'antd'; +import './TraceDetail.styles.scss'; + +import { Button, Typography } from 'antd'; import getTraceItem from 'api/trace/getTraceItem'; import NotFound from 'components/NotFound'; import Spinner from 'components/Spinner'; import TraceDetailContainer from 'container/TraceDetail'; import useUrlQuery from 'hooks/useUrlQuery'; -import { useMemo } from 'react'; +import { Undo } from 'lucide-react'; +import TraceDetailsPage from 'pages/TraceDetailV2'; +import { useMemo, useState } from 'react'; import { useQuery } from 'react-query'; import { useParams } from 'react-router-dom'; import { Props as TraceDetailProps } from 'types/api/trace/getTraceItem'; @@ -13,6 +17,7 @@ import { noEventMessage } from './constants'; function TraceDetail(): JSX.Element { const { id } = useParams(); + const [showNewTraceDetails, setShowNewTraceDetails] = useState(false); const urlQuery = useUrlQuery(); const { spanId, levelUp, levelDown } = useMemo( () => ({ @@ -31,6 +36,10 @@ function TraceDetail(): JSX.Element { }, ); + if (showNewTraceDetails) { + return ; + } + if (traceDetailResponse?.error || error || isError) { return ( @@ -47,7 +56,21 @@ function TraceDetail(): JSX.Element { return ; } - return ; + return ( +
+
+ +
+ ; +
+ ); } export default TraceDetail; diff --git a/frontend/src/pages/TraceDetailV2/NoData/NoData.styles.scss b/frontend/src/pages/TraceDetailV2/NoData/NoData.styles.scss new file mode 100644 index 00000000000..f674b3ea97e --- /dev/null +++ b/frontend/src/pages/TraceDetailV2/NoData/NoData.styles.scss @@ -0,0 +1,167 @@ +.not-found-trace { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 60vh; + width: 500px; + gap: 24px; + margin: 0 auto; + + .description { + display: flex; + flex-direction: column; + gap: 6px; + + .not-found-img { + height: 32px; + width: 32px; + } + + .not-found-text-1 { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + .not-found-text-2 { + color: var(--Vanilla-100, #fff); + } + } + } + + .reasons { + display: flex; + flex-direction: column; + gap: 12px; + + .reason-1 { + display: flex; + padding: 12px; + align-items: flex-start; + gap: 12px; + border-radius: 4px; + background: rgba(171, 189, 255, 0.04); + + .construction-img { + height: 16px; + width: 16px; + } + + .text { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + .reason-2 { + display: flex; + padding: 12px; + align-items: flex-start; + gap: 12px; + border-radius: 4px; + background: rgba(171, 189, 255, 0.04); + + .broom-img { + height: 16px; + width: 16px; + } + + .text { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + } + + .none-of-above { + display: flex; + flex-direction: column; + gap: 12px; + + .text { + color: var(--Vanilla-400, #c0c1c3); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 500; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + + .action-btns { + display: flex; + gap: 8px; + + .action-btn { + display: flex; + width: 160px; + padding: 4px 8px; + justify-content: center; + align-items: center; + gap: 8px; + border-radius: 2px; + border: 1px solid var(--Slate-400, #1d212d); + background: var(--Slate-500, #161922); + box-shadow: none; + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + } + } +} + +.lightMode { + .not-found-trace { + .description { + .not-found-text-1 { + color: var(--bg-ink-400); + .not-found-text-2 { + color: var(--bg-ink-100); + } + } + } + + .reasons { + .reason-1 { + background: var(--bg-vanilla-300); + .text { + color: var(--bg-ink-400); + } + } + .reason-2 { + background: var(--bg-vanilla-300); + + .text { + color: var(--bg-ink-400); + } + } + } + + .none-of-above { + .text { + color: var(--bg-ink-400); + } + + .action-btns { + .action-btn { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + } + } + } + } +} diff --git a/frontend/src/pages/TraceDetailV2/NoData/NoData.tsx b/frontend/src/pages/TraceDetailV2/NoData/NoData.tsx new file mode 100644 index 00000000000..990981f5214 --- /dev/null +++ b/frontend/src/pages/TraceDetailV2/NoData/NoData.tsx @@ -0,0 +1,65 @@ +import './NoData.styles.scss'; + +import { Button, Typography } from 'antd'; +import { LifeBuoy, RefreshCw } from 'lucide-react'; +import { handleContactSupport } from 'pages/Integrations/utils'; +import { isCloudUser } from 'utils/app'; + +function NoData(): JSX.Element { + const isCloudUserVal = isCloudUser(); + return ( +
+
+ no-data + + Uh-oh! We cannot show the selected trace. + + This can happen in either of the two scenraios - + + +
+
+
+ no-data + + The trace data has not been rendered on your SigNoz server yet. You can + wait for a bit and refresh this page if this is the case. + +
+
+ no-data + + The trace has been deleted as the data has crossed it’s retention period. + +
+
+
+ + If you feel the issue is none of the above, please contact support. + +
+ + +
+
+
+ ); +} + +export default NoData; diff --git a/frontend/src/pages/TraceDetailV2/TraceDetailV2.styles.scss b/frontend/src/pages/TraceDetailV2/TraceDetailV2.styles.scss new file mode 100644 index 00000000000..c84884fcf33 --- /dev/null +++ b/frontend/src/pages/TraceDetailV2/TraceDetailV2.styles.scss @@ -0,0 +1,207 @@ +.traces-module-container { + .trace-module { + .ant-tabs-tab { + .tab-item { + display: flex; + align-items: center; + gap: 8px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + } + } + + .ant-tabs-tab-active { + .tab-item { + color: var(--bg-vanilla-100); + } + } + .ant-tabs-nav { + margin: 0px; + padding: 0px !important; + } + + .ant-tabs-nav::before { + border-bottom: 1px solid var(--bg-slate-400) !important; + } + + .ant-tabs-nav-list { + transform: translate(15px, 0px) !important; + } + } + + .old-switch { + display: flex; + align-items: center; + color: var(--Vanilla-400, #c0c1c3); + + /* Bifrost (Ancient)/Content/sm */ + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + + .ant-btn-icon { + margin-inline-end: 0px; + } + } + + .trace-layout { + display: flex; + height: calc(100vh - 44px); + + .trace-left-content { + display: flex; + flex-direction: column; + gap: 25px; + padding-top: 16px; + + .flamegraph-waterfall-toggle { + display: flex; + gap: 4px; + align-items: center; + justify-content: center; + height: 31px; + color: var(--bg-vanilla-400); + padding: 5px 20px; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + + .ant-btn-icon { + margin-inline-end: 0px !important; + } + } + + .span-list-toggle { + display: flex; + gap: 4px; + align-items: center; + justify-content: center; + height: 31px; + padding: 5px 20px; + color: var(--bg-vanilla-400); + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; /* 142.857% */ + letter-spacing: -0.07px; + + .ant-btn-icon { + margin-inline-end: 0px !important; + } + } + + .trace-visualisation-tabs { + .ant-tabs-tab { + border-radius: 2px 0px 0px 0px; + background: var(--bg-ink-400); + border-radius: 2px 2px 0px 0px; + border: 1px solid var(--bg-slate-400); + box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); + height: 31px; + } + + .ant-tabs-tab-active { + background-color: var(--bg-ink-500); + + .ant-btn { + color: var(--bg-vanilla-100) !important; + } + } + + .ant-tabs-tab + .ant-tabs-tab { + margin: 0px; + border-left: 0px; + } + + .ant-tabs-ink-bar { + height: 1px !important; + background: var(--bg-ink-500) !important; + } + + .ant-tabs-nav-list { + transform: translate(15px, 0px) !important; + } + + .ant-tabs-nav::before { + border-bottom: 1px solid var(--bg-slate-400); + } + + .ant-tabs-nav { + margin: 0px; + padding: 0px !important; + } + } + } + } +} + +.lightMode { + .traces-module-container { + .trace-module { + .ant-tabs-tab { + .tab-item { + color: var(--bg-ink-400); + } + } + + .ant-tabs-tab-active { + .tab-item { + color: var(--bg-ink-100); + } + } + + .ant-tabs-nav::before { + border-bottom: 1px solid var(--bg-vanilla-300) !important; + } + } + .old-switch { + color: var(--bg-ink-400); + } + + .trace-layout { + .flamegraph-waterfall-toggle { + color: var(--bg-ink-400); + } + + .span-list-toggle { + color: var(--bg-ink-400); + } + + .trace-visualisation-tabs { + .ant-tabs-tab { + background: var(--bg-vanilla-100); + border: 1px solid var(--bg-vanilla-300); + } + + .ant-tabs-tab-active { + background-color: var(--bg-vanilla-200); + + .ant-btn { + color: var(--bg-ink-100) !important; + } + } + + .ant-tabs-ink-bar { + height: 1px !important; + background: var(--bg-vanilla-200) !important; + } + + .ant-tabs-nav::before { + border-bottom: 1px solid var(--bg-vanilla-300); + } + } + } + } +} diff --git a/frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx b/frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx new file mode 100644 index 00000000000..726eeebcde4 --- /dev/null +++ b/frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx @@ -0,0 +1,154 @@ +import './TraceDetailV2.styles.scss'; + +import { Button, Tabs } from 'antd'; +import FlamegraphImg from 'assets/TraceDetail/Flamegraph'; +import TraceFlamegraph from 'container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph'; +import SpanDetailsDrawer from 'container/SpanDetailsDrawer/SpanDetailsDrawer'; +import TraceMetadata from 'container/TraceMetadata/TraceMetadata'; +import TraceWaterfall, { + IInterestedSpan, +} from 'container/TraceWaterfall/TraceWaterfall'; +import useGetTraceV2 from 'hooks/trace/useGetTraceV2'; +import useUrlQuery from 'hooks/useUrlQuery'; +import { defaultTo } from 'lodash-es'; +import { useEffect, useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { Span, TraceDetailV2URLProps } from 'types/api/trace/getTraceV2'; + +import NoData from './NoData/NoData'; + +function TraceDetailsV2(): JSX.Element { + const { id: traceId } = useParams(); + const urlQuery = useUrlQuery(); + const [interestedSpanId, setInterestedSpanId] = useState( + () => ({ + spanId: urlQuery.get('spanId') || '', + isUncollapsed: urlQuery.get('spanId') !== '', + }), + ); + const [ + traceFlamegraphStatsWidth, + setTraceFlamegraphStatsWidth, + ] = useState(450); + const [isSpanDetailsDocked, setIsSpanDetailsDocked] = useState(false); + const [selectedSpan, setSelectedSpan] = useState(); + + useEffect(() => { + setInterestedSpanId({ + spanId: urlQuery.get('spanId') || '', + isUncollapsed: urlQuery.get('spanId') !== '', + }); + }, [urlQuery]); + + const [uncollapsedNodes, setUncollapsedNodes] = useState([]); + const { + data: traceData, + isFetching: isFetchingTraceData, + error: errorFetchingTraceData, + } = useGetTraceV2({ + traceId, + uncollapsedSpans: uncollapsedNodes, + selectedSpanId: interestedSpanId.spanId, + isSelectedSpanIDUnCollapsed: interestedSpanId.isUncollapsed, + }); + + useEffect(() => { + if (traceData && traceData.payload && traceData.payload.uncollapsedSpans) { + setUncollapsedNodes(traceData.payload.uncollapsedSpans); + } + }, [traceData]); + + useEffect(() => { + if (selectedSpan) { + setIsSpanDetailsDocked(false); + } + }, [selectedSpan]); + + const noData = useMemo( + () => + !isFetchingTraceData && + !errorFetchingTraceData && + defaultTo(traceData?.payload?.spans.length, 0) === 0, + [ + errorFetchingTraceData, + isFetchingTraceData, + traceData?.payload?.spans.length, + ], + ); + + const items = [ + { + label: ( + + ), + key: 'flamegraph', + children: ( + <> + + + + ), + }, + ]; + + return ( +
+
+ + {!noData ? ( + + ) : ( + + )} +
+ +
+ ); +} + +export default TraceDetailsV2; diff --git a/frontend/src/pages/TraceDetailV2/index.tsx b/frontend/src/pages/TraceDetailV2/index.tsx new file mode 100644 index 00000000000..20277942334 --- /dev/null +++ b/frontend/src/pages/TraceDetailV2/index.tsx @@ -0,0 +1,67 @@ +import './TraceDetailV2.styles.scss'; + +import { Button, Tabs } from 'antd'; +import ROUTES from 'constants/routes'; +import history from 'lib/history'; +import { Compass, TowerControl, Undo } from 'lucide-react'; +import TraceDetail from 'pages/TraceDetail'; +import { useCallback, useState } from 'react'; + +import TraceDetailsV2 from './TraceDetailV2'; + +export default function TraceDetailsPage(): JSX.Element { + const [showOldTraceDetails, setShowOldTraceDetails] = useState(false); + const items = [ + { + label: ( +
+ Explorer +
+ ), + key: 'trace-details', + children: , + }, + { + label: ( +
+ Views +
+ ), + key: 'saved-views', + children:
, + }, + ]; + const handleOldTraceDetails = useCallback(() => { + setShowOldTraceDetails(true); + }, []); + + return showOldTraceDetails ? ( + + ) : ( +
+ { + if (activeKey === 'saved-views') { + history.push(ROUTES.TRACES_SAVE_VIEWS); + } + if (activeKey === 'trace-details') { + history.push(ROUTES.TRACES_EXPLORER); + } + }} + tabBarExtraContent={ + + } + /> +
+ ); +} diff --git a/frontend/src/types/api/logs/log.ts b/frontend/src/types/api/logs/log.ts index 1224f55fee7..d40dbb3d2e5 100644 --- a/frontend/src/types/api/logs/log.ts +++ b/frontend/src/types/api/logs/log.ts @@ -3,7 +3,7 @@ export interface ILog { timestamp: number | string; id: string; traceId: string; - spanId: string; + spanID: string; traceFlags: number; severityText: string; severityNumber: number; diff --git a/frontend/src/types/api/trace/getTraceFlamegraph.ts b/frontend/src/types/api/trace/getTraceFlamegraph.ts new file mode 100644 index 00000000000..df199dd4e51 --- /dev/null +++ b/frontend/src/types/api/trace/getTraceFlamegraph.ts @@ -0,0 +1,26 @@ +export interface TraceDetailFlamegraphURLProps { + id: string; +} + +export interface GetTraceFlamegraphPayloadProps { + traceId: string; + selectedSpanId: string; +} + +export interface FlamegraphSpan { + timestamp: number; + durationNano: number; + spanId: string; + parentSpanId: string; + traceId: string; + hasError: boolean; + serviceName: string; + name: string; + level: number; +} + +export interface GetTraceFlamegraphSuccessResponse { + spans: FlamegraphSpan[][]; + startTimestampMillis: number; + endTimestampMillis: number; +} diff --git a/frontend/src/types/api/trace/getTraceV2.ts b/frontend/src/types/api/trace/getTraceV2.ts new file mode 100644 index 00000000000..e11a6628514 --- /dev/null +++ b/frontend/src/types/api/trace/getTraceV2.ts @@ -0,0 +1,52 @@ +export interface TraceDetailV2URLProps { + id: string; +} + +export interface GetTraceV2PayloadProps { + traceId: string; + selectedSpanId: string; + uncollapsedSpans: string[]; + isSelectedSpanIDUnCollapsed: boolean; +} + +export interface Event { + name: string; + timeUnixNano: number; + attributeMap: Record; +} +export interface Span { + timestamp: number; + durationNano: number; + spanId: string; + rootSpanId: string; + parentSpanId: string; + traceId: string; + hasError: boolean; + kind: number; + serviceName: string; + name: string; + references: any; + tagMap: Record; + event: string[]; + rootName: string; + statusMessage: string; + statusCodeString: string; + spanKind: string; + hasChildren: boolean; + hasSibling: boolean; + subTreeNodeCount: number; + level: number; +} + +export interface GetTraceV2SuccessResponse { + spans: Span[]; + hasMissingSpans: boolean; + uncollapsedSpans: string[]; + startTimestampMillis: number; + endTimestampMillis: number; + totalSpansCount: number; + totalErrorSpansCount: number; + rootServiceName: string; + rootServiceEntryPoint: string; + serviceNameToTotalDurationMap: Record; +} diff --git a/frontend/src/utils/timeUtils.ts b/frontend/src/utils/timeUtils.ts index e93a96b4d25..3eb863363ed 100644 --- a/frontend/src/utils/timeUtils.ts +++ b/frontend/src/utils/timeUtils.ts @@ -28,6 +28,16 @@ export const getFormattedDateWithMinutes = (epochTimestamp: number): string => { return date.format(DATE_TIME_FORMATS.MONTH_DATETIME_SHORT); }; +export const getFormattedDateWithMinutesAndSeconds = ( + epochTimestamp: number, +): string => { + // Convert epoch timestamp to a date + const date = dayjs.unix(epochTimestamp); + + // Format the date as "18 Nov 2013" + return date.format('DD MMM YYYY HH:mm:ss'); +}; + export const getRemainingDays = (billingEndDate: number): number => { // Convert Epoch timestamps to Date objects const startDate = new Date(); // Convert seconds to milliseconds diff --git a/frontend/yarn.lock b/frontend/yarn.lock index bc745e556d3..bfa1f03cd7d 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -3690,11 +3690,23 @@ dependencies: "@tanstack/table-core" "8.20.5" +"@tanstack/react-virtual@3.11.2": + version "3.11.2" + resolved "https://registry.yarnpkg.com/@tanstack/react-virtual/-/react-virtual-3.11.2.tgz#d6b9bd999c181f0a2edce270c87a2febead04322" + integrity sha512-OuFzMXPF4+xZgx8UzJha0AieuMihhhaWG0tCqpp6tDzlFwOmNBPYMuLOtMJ1Tr4pXLHmgjcWhG6RlknY2oNTdQ== + dependencies: + "@tanstack/virtual-core" "3.11.2" + "@tanstack/table-core@8.20.5": version "8.20.5" resolved "https://registry.yarnpkg.com/@tanstack/table-core/-/table-core-8.20.5.tgz#3974f0b090bed11243d4107283824167a395cf1d" integrity sha512-P9dF7XbibHph2PFRz8gfBKEXEY/HJPOhym8CHmjF8y3q5mWpKx9xtZapXQUWCgkqvsK0R46Azuz+VaxD4Xl+Tg== +"@tanstack/virtual-core@3.11.2": + version "3.11.2" + resolved "https://registry.yarnpkg.com/@tanstack/virtual-core/-/virtual-core-3.11.2.tgz#00409e743ac4eea9afe5b7708594d5fcebb00212" + integrity sha512-vTtpNt7mKCiZ1pwU9hfKPhpdVO2sVzFQsxoVBGtOSHxlrRRzYr8iQ2TlwbAcRYCcEiZ9ECAM8kBzH0v2+VzfKw== + "@testing-library/dom@^8.5.0": version "8.20.0" resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.0.tgz" diff --git a/pkg/query-service/app/clickhouseReader/reader.go b/pkg/query-service/app/clickhouseReader/reader.go index 89f8c0536de..ca0d1aaf2bf 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -1811,7 +1811,7 @@ func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, trace } processingPostCache := time.Now() - selectedSpansForRequest := tracedetail.GetSelectedSpansForFlamegraphForRequest(req.SelectedSpanID, selectedSpans) + selectedSpansForRequest := tracedetail.GetSelectedSpansForFlamegraphForRequest(req.SelectedSpanID, selectedSpans, startTime, endTime) zap.L().Info("getFlamegraphSpansForTrace: processing post cache", zap.Duration("duration", time.Since(processingPostCache)), zap.String("traceID", traceID)) trace.Spans = selectedSpansForRequest diff --git a/pkg/query-service/app/traces/tracedetail/flamegraph.go b/pkg/query-service/app/traces/tracedetail/flamegraph.go index b9412ebf8ce..b2c2c81b52b 100644 --- a/pkg/query-service/app/traces/tracedetail/flamegraph.go +++ b/pkg/query-service/app/traces/tracedetail/flamegraph.go @@ -8,6 +8,8 @@ import ( var ( SPAN_LIMIT_PER_REQUEST_FOR_FLAMEGRAPH float64 = 50 + SPAN_LIMIT_PER_LEVEL int = 100 + TIMESTAMP_SAMPLING_BUCKET_COUNT int = 50 ) func ContainsFlamegraphSpan(slice []*model.FlamegraphSpan, item *model.FlamegraphSpan) bool { @@ -39,9 +41,11 @@ func FindIndexForSelectedSpan(spans [][]*model.FlamegraphSpan, selectedSpanId st var selectedSpanLevel int = 0 for index, _spans := range spans { - if len(_spans) > 0 && _spans[0].SpanID == selectedSpanId { - selectedSpanLevel = index - break + for _, span := range _spans { + if span.SpanID == selectedSpanId { + selectedSpanLevel = index + break + } } } @@ -88,7 +92,64 @@ func GetSelectedSpansForFlamegraph(traceRoots []*model.FlamegraphSpan, spanIdToS return selectedSpans } -func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpans [][]*model.FlamegraphSpan) [][]*model.FlamegraphSpan { +func getLatencyAndTimestampBucketedSpans(spans []*model.FlamegraphSpan, selectedSpanID string, isSelectedSpanIDPresent bool, startTime uint64, endTime uint64) []*model.FlamegraphSpan { + var sampledSpans []*model.FlamegraphSpan + // sort the spans by latency for latency filtering + sort.Slice(spans, func(i, j int) bool { + return spans[i].DurationNano > spans[j].DurationNano + }) + + // pick the top 5 latency spans + for idx := range 5 { + sampledSpans = append(sampledSpans, spans[idx]) + } + + // always add the selectedSpan + if isSelectedSpanIDPresent { + idx := -1 + for _idx, span := range spans { + if span.SpanID == selectedSpanID { + idx = _idx + } + } + if idx != -1 { + sampledSpans = append(sampledSpans, spans[idx]) + } + } + + bucketSize := (endTime - startTime) / uint64(TIMESTAMP_SAMPLING_BUCKET_COUNT) + if bucketSize == 0 { + bucketSize = 1 + } + + bucketedSpans := make([][]*model.FlamegraphSpan, 50) + + for _, span := range spans { + if span.TimeUnixNano >= startTime && span.TimeUnixNano <= endTime { + bucketIndex := int((span.TimeUnixNano - startTime) / bucketSize) + if bucketIndex >= 0 && bucketIndex < 50 { + bucketedSpans[bucketIndex] = append(bucketedSpans[bucketIndex], span) + } + } + } + + for i := range bucketedSpans { + if len(bucketedSpans[i]) > 2 { + // Keep only the first 2 spans + bucketedSpans[i] = bucketedSpans[i][:2] + } + } + + // Flatten the bucketed spans into a single slice + for _, bucket := range bucketedSpans { + sampledSpans = append(sampledSpans, bucket...) + } + + return sampledSpans +} + +func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpans [][]*model.FlamegraphSpan, startTime uint64, endTime uint64) [][]*model.FlamegraphSpan { + var selectedSpansForRequest [][]*model.FlamegraphSpan var selectedIndex = 0 if selectedSpanID != "" { @@ -112,5 +173,14 @@ func GetSelectedSpansForFlamegraphForRequest(selectedSpanID string, selectedSpan lowerLimit = 0 } - return selectedSpans[lowerLimit:upperLimit] + for i := lowerLimit; i < upperLimit; i++ { + if len(selectedSpans[i]) > SPAN_LIMIT_PER_LEVEL { + _spans := getLatencyAndTimestampBucketedSpans(selectedSpans[i], selectedSpanID, i == selectedIndex, startTime, endTime) + selectedSpansForRequest = append(selectedSpansForRequest, _spans) + } else { + selectedSpansForRequest = append(selectedSpansForRequest, selectedSpans[i]) + } + } + + return selectedSpansForRequest }