diff --git a/ee/query-service/app/db/reader.go b/ee/query-service/app/db/reader.go index 9794abd013..6c01f96551 100644 --- a/ee/query-service/app/db/reader.go +++ b/ee/query-service/app/db/reader.go @@ -7,6 +7,7 @@ import ( "github.com/jmoiron/sqlx" + cacheV2 "go.signoz.io/signoz/pkg/cache" basechr "go.signoz.io/signoz/pkg/query-service/app/clickhouseReader" "go.signoz.io/signoz/pkg/query-service/interfaces" ) @@ -17,6 +18,7 @@ type ClickhouseReader struct { *basechr.ClickHouseReader } +// dummy func NewDataConnector( localDB *sqlx.DB, promConfigPath string, @@ -27,8 +29,10 @@ func NewDataConnector( cluster string, useLogsNewSchema bool, useTraceNewSchema bool, + fluxIntervalForTraceDetail time.Duration, + cacheV2 cacheV2.Cache, ) *ClickhouseReader { - ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema, useTraceNewSchema) + ch := basechr.NewReader(localDB, promConfigPath, lm, maxIdleConns, maxOpenConns, dialTimeout, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cacheV2) return &ClickhouseReader{ conn: ch.GetConn(), appdb: localDB, diff --git a/ee/query-service/app/server.go b/ee/query-service/app/server.go index acc5132dc2..7377e3a671 100644 --- a/ee/query-service/app/server.go +++ b/ee/query-service/app/server.go @@ -71,18 +71,19 @@ type ServerOptions struct { HTTPHostPort string PrivateHostPort string // alert specific params - DisableRules bool - RuleRepoURL string - PreferSpanMetrics bool - MaxIdleConns int - MaxOpenConns int - DialTimeout time.Duration - CacheConfigPath string - FluxInterval string - Cluster string - GatewayUrl string - UseLogsNewSchema bool - UseTraceNewSchema bool + DisableRules bool + RuleRepoURL string + PreferSpanMetrics bool + MaxIdleConns int + MaxOpenConns int + DialTimeout time.Duration + CacheConfigPath string + FluxInterval string + FluxIntervalForTraceDetail string + Cluster string + GatewayUrl string + UseLogsNewSchema bool + UseTraceNewSchema bool } // Server runs HTTP api service @@ -141,10 +142,29 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } + var c cache.Cache + if serverOptions.CacheConfigPath != "" { + cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath) + if err != nil { + return nil, err + } + c = cache.NewCache(cacheOpts) + } + // set license manager as feature flag provider in dao modelDao.SetFlagProvider(lm) readerReady := make(chan bool) + fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval) + if err != nil { + return nil, err + } + + fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail) + if err != nil { + return nil, err + } + var reader interfaces.DataConnector storage := os.Getenv("STORAGE") if storage == "clickhouse" { @@ -159,6 +179,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { serverOptions.Cluster, serverOptions.UseLogsNewSchema, serverOptions.UseTraceNewSchema, + fluxIntervalForTraceDetail, + serverOptions.SigNoz.Cache, ) go qb.Start(readerReady) reader = qb @@ -173,14 +195,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } } - var c cache.Cache - if serverOptions.CacheConfigPath != "" { - cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath) - if err != nil { - return nil, err - } - c = cache.NewCache(cacheOpts) - } <-readerReady rm, err := makeRulesManager(serverOptions.PromConfigPath, @@ -249,12 +263,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { telemetry.GetInstance().SetReader(reader) telemetry.GetInstance().SetSaasOperator(constants.SaasSegmentKey) - fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval) - - if err != nil { - return nil, err - } - apiOpts := api.APIHandlerOptions{ DataConnector: reader, SkipConfig: skipConfig, diff --git a/ee/query-service/main.go b/ee/query-service/main.go index 9a681f395e..9fdb4f17c4 100644 --- a/ee/query-service/main.go +++ b/ee/query-service/main.go @@ -99,7 +99,7 @@ func main() { var useLogsNewSchema bool var useTraceNewSchema bool - var cacheConfigPath, fluxInterval string + var cacheConfigPath, fluxInterval, fluxIntervalForTraceDetail string var enableQueryServiceLogOTLPExport bool var preferSpanMetrics bool @@ -120,7 +120,8 @@ func main() { flag.DurationVar(&dialTimeout, "dial-timeout", 5*time.Second, "(the maximum time to establish a connection.)") flag.StringVar(&ruleRepoURL, "rules.repo-url", baseconst.AlertHelpPage, "(host address used to build rule link in alert messages)") flag.StringVar(&cacheConfigPath, "experimental.cache-config", "", "(cache config to use)") - flag.StringVar(&fluxInterval, "flux-interval", "5m", "(the interval to exclude data from being cached to avoid incorrect cache for data in motion)") + flag.StringVar(&fluxInterval, "flux-interval", "0m", "(the interval to exclude data from being cached to avoid incorrect cache for data in motion)") + flag.StringVar(&fluxIntervalForTraceDetail, "flux-interval-trace-detail", "2m", "(the interval to exclude data from being cached to avoid incorrect cache for trace data in motion)") flag.BoolVar(&enableQueryServiceLogOTLPExport, "enable.query.service.log.otlp.export", false, "(enable query service log otlp export)") flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')") flag.StringVar(&gatewayUrl, "gateway-url", "", "(url to the gateway)") @@ -151,24 +152,25 @@ func main() { } serverOptions := &app.ServerOptions{ - Config: config, - SigNoz: signoz, - HTTPHostPort: baseconst.HTTPHostPort, - PromConfigPath: promConfigPath, - SkipTopLvlOpsPath: skipTopLvlOpsPath, - PreferSpanMetrics: preferSpanMetrics, - PrivateHostPort: baseconst.PrivateHostPort, - DisableRules: disableRules, - RuleRepoURL: ruleRepoURL, - MaxIdleConns: maxIdleConns, - MaxOpenConns: maxOpenConns, - DialTimeout: dialTimeout, - CacheConfigPath: cacheConfigPath, - FluxInterval: fluxInterval, - Cluster: cluster, - GatewayUrl: gatewayUrl, - UseLogsNewSchema: useLogsNewSchema, - UseTraceNewSchema: useTraceNewSchema, + Config: config, + SigNoz: signoz, + HTTPHostPort: baseconst.HTTPHostPort, + PromConfigPath: promConfigPath, + SkipTopLvlOpsPath: skipTopLvlOpsPath, + PreferSpanMetrics: preferSpanMetrics, + PrivateHostPort: baseconst.PrivateHostPort, + DisableRules: disableRules, + RuleRepoURL: ruleRepoURL, + MaxIdleConns: maxIdleConns, + MaxOpenConns: maxOpenConns, + DialTimeout: dialTimeout, + CacheConfigPath: cacheConfigPath, + FluxInterval: fluxInterval, + FluxIntervalForTraceDetail: fluxIntervalForTraceDetail, + Cluster: cluster, + GatewayUrl: gatewayUrl, + UseLogsNewSchema: useLogsNewSchema, + UseTraceNewSchema: useTraceNewSchema, } // Read the jwt secret key diff --git a/frontend/package.json b/frontend/package.json index 9ab79225e6..c5f4a63393 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 0000000000..0ade634ebb --- /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 0000000000..858fdfe748 --- /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 0857ea4664..b7d5175ee4 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 0000000000..a084507e1f --- /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 0000000000..50e96efb4f --- /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/${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/components/DetailsDrawer/DetailsDrawer.styles.scss b/frontend/src/components/DetailsDrawer/DetailsDrawer.styles.scss new file mode 100644 index 0000000000..2a92f63a5a --- /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 0000000000..59776aec13 --- /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/TableV3/TableV3.tsx b/frontend/src/components/TableV3/TableV3.tsx index 6219297f33..51bc437238 100644 --- a/frontend/src/components/TableV3/TableV3.tsx +++ b/frontend/src/components/TableV3/TableV3.tsx @@ -7,28 +7,47 @@ 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, { MutableRefObject, 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 +59,24 @@ 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; + virtualiserRef?: MutableRefObject< + Virtualizer | undefined + >; } export function TableV3(props: ITableV3Props): JSX.Element { - const { data, columns, config } = props; + const { data, columns, config, customClassName = '', virtualiserRef } = props; const table = useReactTable({ data, @@ -61,11 +87,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), @@ -85,13 +126,14 @@ export function TableV3(props: ITableV3Props): JSX.Element { }, [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 +155,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 +170,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/constants/reactQueryKeys.ts b/frontend/src/constants/reactQueryKeys.ts index 30da4e1362..43fc6060f5 100644 --- a/frontend/src/constants/reactQueryKeys.ts +++ b/frontend/src/constants/reactQueryKeys.ts @@ -21,6 +21,7 @@ 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: 'GET_TRACE_V2', GET_POD_LIST: 'GET_POD_LIST', GET_NODE_LIST: 'GET_NODE_LIST', GET_DEPLOYMENT_LIST: 'GET_DEPLOYMENT_LIST', diff --git a/frontend/src/container/AppLayout/index.tsx b/frontend/src/container/AppLayout/index.tsx index 8bb91d48c3..c313ef29d0 100644 --- a/frontend/src/container/AppLayout/index.tsx +++ b/frontend/src/container/AppLayout/index.tsx @@ -427,7 +427,7 @@ function AppLayout(props: AppLayoutProps): JSX.Element { ? 0 : '0 1rem', - ...(isTraceDetailsView() ? { marginRight: 0 } : {}), + ...(isTraceDetailsView() ? { margin: 0 } : {}), }} > {isToDisplayLayout && !renderFullScreen && } diff --git a/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.styles.scss b/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.styles.scss new file mode 100644 index 0000000000..14fa985e74 --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.styles.scss @@ -0,0 +1,146 @@ +.flamegraph { + display: flex; + height: 20vh; + border-bottom: 1px solid var(--bg-slate-400); + + .flamegraph-chart { + width: 80%; + padding: 15px; + + .loading-skeleton { + justify-content: center; + align-items: center; + } + } + + .flamegraph-stats { + width: 20%; + display: flex; + flex-direction: column; + border-left: 1px solid var(--bg-slate-400); + overflow-y: auto; + overflow-x: hidden; + padding: 16px 12px; + + .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: 'Space Mono'; + 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: 85px; + 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: 'Geist Mono'; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: normal; + letter-spacing: 0.48px; + } + } + } + } + } +} + +.lightMode { + .flamegraph { + border-bottom: 1px solid var(--bg-vanilla-300); + + .flamegraph-stats { + border-left: 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 0000000000..30fc5b96d1 --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph.tsx @@ -0,0 +1,153 @@ +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 { useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { TraceDetailFlamegraphURLProps } from 'types/api/trace/getTraceFlamegraph'; + +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; +} + +function TraceFlamegraph(props: ITraceFlamegraphProps): JSX.Element { + const { serviceExecTime, startTime, endTime } = props; + const { id: traceId } = useParams(); + const urlQuery = useUrlQuery(); + const [firstSpanAtFetchLevel, setFirstSpanAtFetchLevel] = useState( + urlQuery.get('spanId') || '', + ); + 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, + spans, + traceFlamegraphState, + traceId, + ]); + + return ( +
+
{getContent}
+
+
% 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))}% + +
+
+ ); + })} +
+
+
+ ); +} + +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 0000000000..1b837b5ad9 --- /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 0000000000..a9a4d63f94 --- /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 0000000000..0be04ffc23 --- /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 0000000000..d41ae662e4 --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.styles.scss @@ -0,0 +1,28 @@ +.trace-flamegraph { + height: 80%; + 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 0000000000..9e9c325b9a --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/TraceFlamegraphStates/Success/Success.tsx @@ -0,0 +1,141 @@ +/* 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 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, +} from 'react'; +import { useHistory, useLocation } from 'react-router-dom'; +import { ListRange, Virtuoso, VirtuosoHandle } from 'react-virtuoso'; +import { FlamegraphSpan } from 'types/api/trace/getTraceFlamegraph'; + +interface ITraceMetadata { + startTime: number; + endTime: number; +} + +interface ISuccessProps { + spans: FlamegraphSpan[][]; + firstSpanAtFetchLevel: string; + setFirstSpanAtFetchLevel: Dispatch>; + traceMetadata: ITraceMetadata; +} + +function Success(props: ISuccessProps): JSX.Element { + const { + spans, + setFirstSpanAtFetchLevel, + traceMetadata, + firstSpanAtFetchLevel, + } = props; + const { search } = useLocation(); + const history = useHistory(); + const isDarkMode = useIsDarkMode(); + const virtuosoRef = useRef(null); + 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, + isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor, + ); + + if (span.hasError) { + color = `var(--bg-cherry-500)`; + } + return ( + +
{ + 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], + ); + + 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 0000000000..6a9241f0c2 --- /dev/null +++ b/frontend/src/container/PaginatedTraceFlamegraph/constants.ts @@ -0,0 +1,7 @@ +export enum TraceFlamegraphStates { + LOADING = 'LOADING', + SUCCESS = 'SUCCSS', + NO_DATA = 'NO_DATA', + ERROR = 'ERROR', + FETCHING_WITH_OLD_DATA_PRESENT = 'FETCHING_WTIH_OLD_DATA_PRESENT', +} diff --git a/frontend/src/container/TraceMetadata/TraceMetadata.styles.scss b/frontend/src/container/TraceMetadata/TraceMetadata.styles.scss new file mode 100644 index 0000000000..2fc440a00d --- /dev/null +++ b/frontend/src/container/TraceMetadata/TraceMetadata.styles.scss @@ -0,0 +1,247 @@ +.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; + } + } + + .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-400); + 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); + } + } + + .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 0000000000..10204d479a --- /dev/null +++ b/frontend/src/container/TraceMetadata/TraceMetadata.tsx @@ -0,0 +1,87 @@ +import './TraceMetadata.styles.scss'; + +import { Button, Typography } from 'antd'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; +import ROUTES from 'constants/routes'; +import history from 'lib/history'; +import { ArrowLeft, 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 0000000000..0bb2c9ad6c --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfall.styles.scss @@ -0,0 +1,9 @@ +.trace-waterfall { + height: calc(80vh - 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 0000000000..e5def591b1 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfall.tsx @@ -0,0 +1,124 @@ +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 } 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>; +} + +function TraceWaterfall(props: ITraceWaterfallProps): JSX.Element { + const { + traceData, + isFetchingTraceData, + errorFetchingTraceData, + interestedSpanId, + traceId, + uncollapsedNodes, + setInterestedSpanId, + } = 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, + setInterestedSpanId, + 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 0000000000..6382b0157a --- /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 0000000000..27b8428c0e --- /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 0000000000..0be04ffc23 --- /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/DrawerComponents/AttributesTable/AttributesTable.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/AttributesTable/AttributesTable.styles.scss new file mode 100644 index 0000000000..f0a03a33bf --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/AttributesTable/AttributesTable.styles.scss @@ -0,0 +1,116 @@ +.attributes-table { + display: flex; + flex-direction: column; + border-radius: 3px; + border: 1px solid var(--bg-slate-500); + background: rgba(171, 189, 255, 0.04); + + .no-attributes { + display: flex; + align-items: center; + justify-content: center; + height: 40vh; + background-color: var(--bg-ink-400); + } + + .attributes-header { + display: flex; + gap: 16px; + padding: 1px 12px; + height: 38px; + justify-content: space-between; + align-items: center; + + .attributes-tag { + display: flex; + flex-shrink: 0; + width: fit-content; + border-radius: 2px; + background: rgba(113, 144, 249, 0.08); + color: var(--bg-robin-400); + font-family: 'Space Mono'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + } + + .search-input { + } + + .action-btn { + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + background-color: unset; + border: none; + box-shadow: none; + } + } + + .resize-trace-attribute-table { + .ant-table-thead { + display: none; + } + + .attribute-name { + background: unset; + border: 1px solid var(--bg-slate-500); + + .field-key { + color: var(--bg-robin-400); + font-family: 'Space Mono'; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + } + + .attribute-value { + display: flex; + border: 1px solid var(--bg-slate-500); + background: rgba(22, 25, 34, 0.4); + + .field-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); + } + } + } +} + +.lightMode { + .attributes-table { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + + .no-attributes { + background-color: var(--bg-vanilla-200); + } + + .resize-trace-attribute-table { + .attribute-name { + border: 1px solid var(--bg-vanilla-300); + } + + .attribute-value { + border: 1px solid var(--bg-vanilla-200); + background: var(--bg-vanilla-300); + + .field-value { + border: 1px solid var(--bg-vanilla-400); + background: var(--bg-vanilla-200); + } + } + } + } +} diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/AttributesTable/AttributesTable.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/AttributesTable/AttributesTable.tsx new file mode 100644 index 0000000000..e72344d5d4 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/AttributesTable/AttributesTable.tsx @@ -0,0 +1,94 @@ +import './AttributesTable.styles.scss'; + +import { Button, Input, Typography } from 'antd'; +import { ColumnsType } from 'antd/es/table'; +import { ResizeTable } from 'components/ResizeTable'; +import { flattenObject } from 'container/LogDetailedView/utils'; +import { Search } from 'lucide-react'; +import { useMemo, useState } from 'react'; +import { Span } from 'types/api/trace/getTraceV2'; + +import NoData from '../NoData/NoData'; + +interface IAttributesTable { + span: Span; +} + +function AttributesTable(props: IAttributesTable): JSX.Element { + const { span } = props; + const [searchVisible, setSearchVisible] = useState(false); + const [fieldSearchInput, setFieldSearchInput] = useState(''); + + const flattenSpanData: Record = useMemo( + () => (span.tagMap ? flattenObject(span.tagMap) : {}), + [span], + ); + + const datasource = Object.keys(flattenSpanData) + .filter((attribute) => attribute.includes(fieldSearchInput)) + .map((key) => ({ field: key, value: flattenSpanData[key] })); + + const columns: ColumnsType> = [ + { + title: 'Field', + dataIndex: 'field', + key: 'field', + width: 50, + align: 'left', + ellipsis: true, + className: 'attribute-name', + render: (field: string): JSX.Element => ( + {field} + ), + }, + { + title: 'Value', + key: 'value', + width: 50, + ellipsis: false, + className: 'attribute-value', + render: (fieldValue): JSX.Element => ( + + {fieldValue.value} + + ), + }, + ]; + return ( +
+ {datasource.length > 0 ? ( + <> +
+ Attributes + {searchVisible && ( + setFieldSearchInput(e.target.value)} + /> + )} +
+
+ +
+ + ) : ( +
+ +
+ )} +
+ ); +} + +export default AttributesTable; diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/DrawerDescriptiveContent/DrawerDescriptiveContent.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/DrawerDescriptiveContent/DrawerDescriptiveContent.styles.scss new file mode 100644 index 0000000000..5b0fccfb35 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/DrawerDescriptiveContent/DrawerDescriptiveContent.styles.scss @@ -0,0 +1,177 @@ +.trace-drawer-descriptive-content { + display: flex; + flex-direction: column; + gap: 24px; + + .span-name-duration { + display: flex; + align-items: center; + justify-content: space-between; + + .span-name { + display: flex; + gap: 8px; + + .info-pill { + display: flex; + + .text { + display: flex; + padding: 6px 8px; + align-items: center; + gap: 4px; + border-right: 1px solid var(--bg-slate-300); + background: var(--bg-slate-500); + color: #fff; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 18px; /* 128.571% */ + letter-spacing: -0.07px; + } + + .value { + display: flex; + padding: 6px 8px; + justify-content: center; + align-items: center; + gap: 10px; + background: var(--bg-slate-400); + 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; + } + + .success { + border: 1px solid var(--bg-robin-500); + background: var(--bg-robin-500); + color: var(--bg-vanilla-100); + } + + .error { + border: 1px solid var(--bg-cherry-500); + background: var(--bg-cherry-500); + color: var(--bg-vanilla-100); + } + } + } + + .span-duration { + display: flex; + gap: 24px; + + .item { + display: flex; + gap: 6px; + align-items: center; + + .value { + color: var(--bg-vanilla-400); + 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; + } + } + } + } + + .span-metadata { + display: flex; + gap: 24px; + + .item { + display: flex; + flex-direction: column; + gap: 8px; + + .text { + 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 { + .dot { + width: 4px; + height: 4px; + border-radius: 4px; + filter: drop-shadow(0px 0px 6px rgba(37, 225, 146, 0.8)); + } + + 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); + 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; + } + } + } +} + +.lightMode { + .trace-drawer-descriptive-content { + .span-name-duration { + .span-name { + .info-pill { + .text { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-200); + color: var(--bg-ink-100); + } + + .value { + background: var(--bg-vanilla-400); + color: var(--bg-ink-400); + } + } + } + + .span-duration { + .item { + .value { + color: var(--bg-ink-400); + } + } + } + } + + .span-metadata { + .item { + .text { + color: var(--bg-ink-400); + } + + .value { + border: 1px solid var(--bg-vanilla-400); + background: var(--bg-vanilla-400); + color: var(--bg-ink-400); + } + } + } + } +} diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/DrawerDescriptiveContent/DrawerDescriptiveContent.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/DrawerDescriptiveContent/DrawerDescriptiveContent.tsx new file mode 100644 index 0000000000..1c32e62130 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/DrawerDescriptiveContent/DrawerDescriptiveContent.tsx @@ -0,0 +1,95 @@ +import './DrawerDescriptiveContent.styles.scss'; + +import { Typography } from 'antd'; +import cx from 'classnames'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; +import { themeColors } from 'constants/theme'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import { generateColor } from 'lib/uPlotLib/utils/generateColor'; +import { CalendarClock, Timer } from 'lucide-react'; +import { useMemo } from 'react'; +import { Span } from 'types/api/trace/getTraceV2'; +import { formatEpochTimestamp } from 'utils/timeUtils'; + +interface IDrawerDescriptiveContentProps { + span: Span; +} + +function DrawerDescriptiveContent( + props: IDrawerDescriptiveContentProps, +): JSX.Element { + const { span } = props; + const isDarkMode = useIsDarkMode(); + + const color = generateColor( + span.serviceName, + isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor, + ); + + const statusCodeClassName = useMemo(() => { + if (span.statusCodeString === 'unset') { + return ''; + } + const statusCode = parseFloat(span.statusCodeString); + if (statusCode >= 200 && statusCode < 300) { + return 'success'; + } + if (statusCode >= 400) { + return 'error'; + } + + return ''; + }, [span.statusCodeString]); + + return ( +
+
+
+
+ Span name + {span.name} +
+
+ Status code + + {span.statusCodeString} + +
+
+
+ + + + {getYAxisFormattedValue(`${span.durationNano}`, 'ns')} + + + + + + {formatEpochTimestamp(span.timestamp)} + + +
+
+
+
+ Span ID + {span.spanId} +
+
+ Service + +
+ {span.serviceName} + +
+
+ Span kind + {span.spanKind} +
+
+
+ ); +} + +export default DrawerDescriptiveContent; diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/EventsTable/EventsTable.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/EventsTable/EventsTable.styles.scss new file mode 100644 index 0000000000..ccb5ebdbfd --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/EventsTable/EventsTable.styles.scss @@ -0,0 +1,168 @@ +.events-table { + .no-events { + display: flex; + justify-content: center; + align-items: center; + height: 40vh; + border-radius: 3px; + border: 1px solid var(--bg-slate-500); + } + .events-container { + display: flex; + flex-direction: column; + gap: 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); + } + } + + .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); + } + } + } + } + } +} + +.lightMode { + .events-table { + .no-events { + border: 1px solid var(--bg-vanilla-300); + } + .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-200); + color: var(--bg-ink-100); + } + .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); + } + } + + .attribute-value { + border: 1px solid var(--bg-vanilla-300); + background: var(--bg-vanilla-300); + } + } + } + } + } + } +} diff --git a/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/EventsTable/EventsTable.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/EventsTable/EventsTable.tsx new file mode 100644 index 0000000000..846c9900bf --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/EventsTable/EventsTable.tsx @@ -0,0 +1,95 @@ +import './EventsTable.styles.scss'; + +import { Collapse, Typography } from 'antd'; +import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; +import { Diamond } from 'lucide-react'; +import { useMemo } from 'react'; +import { Event, Span } from 'types/api/trace/getTraceV2'; + +import NoData from '../NoData/NoData'; + +interface IEventsTableProps { + span: Span; + startTime: number; +} + +function EventsTable(props: IEventsTableProps): JSX.Element { + const { span, startTime } = props; + 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 && ( +
+ +
+ )} +
+ {events.map((event) => ( +
+ + + + {span.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/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/NoData/NoData.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/NoData/NoData.styles.scss new file mode 100644 index 0000000000..e40fb340a9 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/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/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/NoData/NoData.tsx b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/NoData/NoData.tsx new file mode 100644 index 0000000000..df4e5f38c3 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/DrawerComponents/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/TraceWaterfall/TraceWaterfallStates/Success/Success.styles.scss b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.styles.scss new file mode 100644 index 0000000000..d3b971e9cd --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.styles.scss @@ -0,0 +1,362 @@ +.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(80vh - 200px); + overflow: auto; + overflow-x: hidden; + padding: 0px 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-th, + .div-td { + box-shadow: none; + padding: 0px !important; + } + + .div-th { + padding: 2px 4px; + position: relative; + font-weight: bold; + text-align: center; + height: 0px; + } + + .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; + + .span-overview-content { + display: flex; + flex-shrink: 0; + flex-direction: column; + align-items: flex-start; + gap: 5px; + width: 100%; + + .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: 'Space Mono'; + 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; + border-radius: 5px; + height: 12px; + top: 35%; + border-radius: 2px; + } + + .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); + } + } + + .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(80vh - 200px); + right: 0; + width: 2px; + background: var(--bg-slate-400); + cursor: col-resize; + user-select: none; + touch-action: none; + } + + .resizer.isResizing { + background: var(--bg-slate-300); + opacity: 1; + } + + @media (hover: hover) { + .resizer { + opacity: 0; + } + + *:hover > .resizer { + opacity: 1; + } + } + } +} + +.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 { + .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); + } + } + + .resizer { + background: var(--bg-vanilla-400); + } + + .resizer.isResizing { + background: blue; + opacity: 1; + } + + .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 0000000000..dde69f3a78 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/TraceWaterfallStates/Success/Success.tsx @@ -0,0 +1,442 @@ +/* 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, TabsProps, Tooltip, Typography } from 'antd'; +import cx from 'classnames'; +import DetailsDrawer from 'components/DetailsDrawer/DetailsDrawer'; +import { TableV3 } from 'components/TableV3/TableV3'; +import { QueryParams } from 'constants/query'; +import ROUTES from 'constants/routes'; +import { themeColors } from 'constants/theme'; +import { getTraceToLogsQuery } from 'container/TraceDetail/SelectedSpanDetails/config'; +import { convertTimeToRelevantUnit } from 'container/TraceDetail/utils'; +import { IInterestedSpan } from 'container/TraceWaterfall/TraceWaterfall'; +import { useIsDarkMode } from 'hooks/useDarkMode'; +import createQueryParams from 'lib/createQueryParams'; +import history from 'lib/history'; +import { generateColor } from 'lib/uPlotLib/utils/generateColor'; +import { + AlertCircle, + Anvil, + ArrowUpRight, + Bookmark, + ChevronDown, + ChevronRight, + Leaf, +} from 'lucide-react'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { Span } from 'types/api/trace/getTraceV2'; +import { toFixed } from 'utils/toFixed'; + +import AttributesTable from './DrawerComponents/AttributesTable/AttributesTable'; +import DrawerDescriptiveContent from './DrawerComponents/DrawerDescriptiveContent/DrawerDescriptiveContent'; +import EventsTable from './DrawerComponents/EventsTable/EventsTable'; + +// 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>; +} + +function SpanOverview({ + span, + isSpanCollapsed, + interestedSpanId, + handleCollapseUncollapse, + setTraceDetailsOpen, + setSpanDetails, +}: { + span: Span; + isSpanCollapsed: boolean; + interestedSpanId: string; + handleCollapseUncollapse: (id: string, collapse: boolean) => void; + setTraceDetailsOpen: Dispatch>; + setSpanDetails: Dispatch>; +}): JSX.Element { + const isRootSpan = span.parentSpanId === ''; + const spanRef = useRef(null); + const isDarkMode = useIsDarkMode(); + + let color = generateColor( + span.serviceName, + isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor, + ); + + if (span.hasError) { + color = `var(--bg-cherry-500)`; + } + + return ( +
{ + setSpanDetails(span); + setTraceDetailsOpen(true); + }} + > + {!isRootSpan && ( +
+ )} +
+
+
+ {span.hasChildren ? ( + + ) : ( + + )} + {span.name} +
+
+
+
+ + {span.serviceName} + +
+
+
+ ); +} + +function SpanDuration({ + span, + interestedSpanId, + traceMetadata, + setTraceDetailsOpen, + setSpanDetails, +}: { + span: Span; + interestedSpanId: string; + traceMetadata: ITraceMetadata; + setTraceDetailsOpen: Dispatch>; + setSpanDetails: 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); + const isDarkMode = useIsDarkMode(); + + let color = generateColor( + span.serviceName, + isDarkMode ? themeColors.chartcolors : themeColors.lightModeColor, + ); + + if (span.hasError) { + color = `var(--bg-cherry-500)`; + } + + return ( +
{ + setSpanDetails(span); + setTraceDetailsOpen(true); + }} + > +
+ + {`${toFixed(time, 2)} ${timeUnitName}`} + +
+ ); +} + +// table config +const columnDefHelper = createColumnHelper(); + +function getWaterfallColumns({ + handleCollapseUncollapse, + uncollapsedNodes, + interestedSpanId, + traceMetadata, + setTraceDetailsOpen, + setSpanDetails, +}: { + handleCollapseUncollapse: (id: string, collapse: boolean) => void; + uncollapsedNodes: string[]; + interestedSpanId: IInterestedSpan; + traceMetadata: ITraceMetadata; + setTraceDetailsOpen: Dispatch>; + setSpanDetails: 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 getItems(span: Span, startTime: number): TabsProps['items'] { + return [ + { + label: ( + + ), + key: 'attributes', + children: , + }, + { + label: ( + + ), + key: 'events', + children: , + }, + ]; +} + +function Success(props: ISuccessProps): JSX.Element { + const { + spans, + traceMetadata, + interestedSpanId, + uncollapsedNodes, + setInterestedSpanId, + } = props; + const virtualizerRef = useRef>(); + const [traceDetailsOpen, setTraceDetailsOpen] = useState(false); + const [spanDetails, setSpanDetails] = useState(); + + 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].parentSpanId !== '') { + 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, + interestedSpanId, + traceMetadata, + setTraceDetailsOpen, + setSpanDetails, + }), + [handleCollapseUncollapse, uncollapsedNodes, interestedSpanId, traceMetadata], + ); + + useEffect(() => { + if (interestedSpanId.spanId !== '' && virtualizerRef.current) { + const idx = spans.findIndex( + (span) => span.spanId === interestedSpanId.spanId, + ); + if (idx !== -1) + virtualizerRef.current.scrollToIndex(idx, { + align: 'center', + behavior: 'auto', + }); + } + }, [interestedSpanId, spans]); + + const handleGoToRelatedLogs = useCallback(() => { + const query = getTraceToLogsQuery( + traceMetadata.traceId, + traceMetadata.startTime, + traceMetadata.endTime, + ); + + 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]: traceMetadata.startTime - 1000, + // we add 1000 milliseconds to the end time for nano second duration traces + [QueryParams.endTime]: traceMetadata.endTime + 1000, + })}`, + ); + }, [traceMetadata.endTime, traceMetadata.startTime, traceMetadata.traceId]); + + return ( +
+ {traceMetadata.hasMissingSpans && ( +
+
+ + + This trace has missing spans + +
+ +
+ )} + + {spanDetails && ( + } + tabBarExtraContent={ + + } + /> + )} +
+ ); +} + +export default Success; diff --git a/frontend/src/container/TraceWaterfall/constants.ts b/frontend/src/container/TraceWaterfall/constants.ts new file mode 100644 index 0000000000..fc874d3542 --- /dev/null +++ b/frontend/src/container/TraceWaterfall/constants.ts @@ -0,0 +1,7 @@ +export enum TraceWaterfallStates { + LOADING = 'LOADING', + SUCCESS = 'SUCCSS', + 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 0000000000..c1a3cee018 --- /dev/null +++ b/frontend/src/hooks/trace/useGetTraceFlamegraph.tsx @@ -0,0 +1,27 @@ +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, 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 0000000000..612493f375 --- /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, + 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 0000000000..bf1696ebe4 --- /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 f936bd2d0c..3ab4856267 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 0000000000..f674b3ea97 --- /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 0000000000..990981f521 --- /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 0000000000..41d199c667 --- /dev/null +++ b/frontend/src/pages/TraceDetailV2/TraceDetailV2.styles.scss @@ -0,0 +1,186 @@ +.traces-module-container { + .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-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; + 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; + } + } + + .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 { + .ant-tabs-tab { + .tab-item { + color: var(--bg-ink-400); + } + } + + .ant-tabs-tab-active { + .tab-item { + color: var(--bg-ink-100); + } + } + + .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); + } + + .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 0000000000..e9580044a8 --- /dev/null +++ b/frontend/src/pages/TraceDetailV2/TraceDetailV2.tsx @@ -0,0 +1,123 @@ +import './TraceDetailV2.styles.scss'; + +import { Button, Tabs } from 'antd'; +import TraceFlamegraph from 'container/PaginatedTraceFlamegraph/PaginatedTraceFlamegraph'; +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 { DraftingCompass } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { useParams } from 'react-router-dom'; +import { 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') !== '', + }), + ); + + 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]); + + 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 0000000000..2027794233 --- /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/trace/getTraceFlamegraph.ts b/frontend/src/types/api/trace/getTraceFlamegraph.ts new file mode 100644 index 0000000000..df199dd4e5 --- /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 0000000000..e11a662851 --- /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 67e668dc3c..10e1f938bf 100644 --- a/frontend/src/utils/timeUtils.ts +++ b/frontend/src/utils/timeUtils.ts @@ -27,6 +27,16 @@ export const getFormattedDateWithMinutes = (epochTimestamp: number): string => { return date.format('DD MMM YYYY HH:mm'); }; +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 bc745e556d..bfa1f03cd7 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 0a8a5acead..110625c60b 100644 --- a/pkg/query-service/app/clickhouseReader/reader.go +++ b/pkg/query-service/app/clickhouseReader/reader.go @@ -33,6 +33,8 @@ import ( "github.com/ClickHouse/clickhouse-go/v2" "github.com/ClickHouse/clickhouse-go/v2/lib/driver" "github.com/jmoiron/sqlx" + "go.signoz.io/signoz/pkg/cache" + cacheV2 "go.signoz.io/signoz/pkg/cache" promModel "github.com/prometheus/common/model" "go.uber.org/zap" @@ -156,6 +158,9 @@ type ClickHouseReader struct { traceLocalTableName string traceResourceTableV3 string traceSummaryTable string + + fluxIntervalForTraceDetail time.Duration + cacheV2 cacheV2.Cache } // NewTraceReader returns a TraceReader for the database @@ -169,6 +174,8 @@ func NewReader( cluster string, useLogsNewSchema bool, useTraceNewSchema bool, + fluxIntervalForTraceDetail time.Duration, + cacheV2 cacheV2.Cache, ) *ClickHouseReader { datasource := os.Getenv("ClickHouseUrl") @@ -179,7 +186,7 @@ func NewReader( zap.L().Fatal("failed to initialize ClickHouse", zap.Error(err)) } - return NewReaderFromClickhouseConnection(db, options, localDB, configFile, featureFlag, cluster, useLogsNewSchema, useTraceNewSchema) + return NewReaderFromClickhouseConnection(db, options, localDB, configFile, featureFlag, cluster, useLogsNewSchema, useTraceNewSchema, fluxIntervalForTraceDetail, cacheV2) } func NewReaderFromClickhouseConnection( @@ -191,6 +198,8 @@ func NewReaderFromClickhouseConnection( cluster string, useLogsNewSchema bool, useTraceNewSchema bool, + fluxIntervalForTraceDetail time.Duration, + cacheV2 cacheV2.Cache, ) *ClickHouseReader { alertManager, err := am.New() if err != nil { @@ -277,6 +286,9 @@ func NewReaderFromClickhouseConnection( traceTableName: traceTableName, traceResourceTableV3: options.primary.TraceResourceTableV3, traceSummaryTable: options.primary.TraceSummaryTable, + + fluxIntervalForTraceDetail: fluxIntervalForTraceDetail, + cacheV2: cacheV2, } } @@ -1442,6 +1454,516 @@ func (r *ClickHouseReader) SearchTraces(ctx context.Context, params *model.Searc return &searchSpansResult, nil } +type Interval struct { + StartTime uint64 + Duration uint64 + Service string +} + +func calculateServiceTime(serviceIntervals map[string][]Interval) map[string]uint64 { + totalTimes := make(map[string]uint64) + + for service, serviceIntervals := range serviceIntervals { + sort.Slice(serviceIntervals, func(i, j int) bool { + return serviceIntervals[i].StartTime < serviceIntervals[j].StartTime + }) + mergedIntervals := mergeIntervals(serviceIntervals) + totalTime := uint64(0) + for _, interval := range mergedIntervals { + totalTime += interval.Duration + } + totalTimes[service] = totalTime + } + + return totalTimes +} + +func mergeIntervals(intervals []Interval) []Interval { + if len(intervals) == 0 { + return nil + } + + var merged []Interval + current := intervals[0] + + for i := 1; i < len(intervals); i++ { + next := intervals[i] + if current.StartTime+current.Duration >= next.StartTime { + endTime := max(current.StartTime+current.Duration, next.StartTime+next.Duration) + current.Duration = endTime - current.StartTime + } else { + merged = append(merged, current) + current = next + } + } + // Add the last interval + merged = append(merged, current) + + return merged +} + +func max(a, b uint64) uint64 { + if a > b { + return a + } + return b +} + +func (r *ClickHouseReader) GetWaterfallSpansForTraceWithMetadata(ctx context.Context, traceID string, req *model.GetWaterfallSpansForTraceWithMetadataParams) (*model.GetWaterfallSpansForTraceWithMetadataResponse, *model.ApiError) { + response := new(model.GetWaterfallSpansForTraceWithMetadataResponse) + var startTime, endTime, durationNano, totalErrorSpans uint64 + var spanIdToSpanNodeMap = map[string]*model.Span{} + var traceRoots []*model.Span + var serviceNameToTotalDurationMap = map[string]uint64{} + var useCache bool = true + + cachedTraceData := new(model.GetWaterfallSpansForTraceWithMetadataCache) + cacheStatus, err := r.cacheV2.Retrieve(ctx, fmt.Sprintf("getWaterfallSpansForTraceWithMetadata-%v", traceID), cachedTraceData, false) + if err != nil { + zap.L().Debug("error in retrieving getWaterfallSpansForTraceWithMetadata cache", zap.Error(err)) + useCache = false + } + if cacheStatus != cache.RetrieveStatusHit { + useCache = false + } + + if err == nil && cacheStatus == cache.RetrieveStatusHit { + + if time.Since(time.UnixMilli(int64(cachedTraceData.EndTime))) < r.fluxIntervalForTraceDetail { + useCache = false + } + + if useCache { + zap.L().Info("cache is successfully hit, applying cache for getWaterfallSpansForTraceWithMetadata", zap.String("traceID", traceID)) + startTime = cachedTraceData.StartTime + endTime = cachedTraceData.EndTime + durationNano = cachedTraceData.DurationNano + spanIdToSpanNodeMap = cachedTraceData.SpanIdToSpanNodeMap + serviceNameToTotalDurationMap = cachedTraceData.ServiceNameToTotalDurationMap + traceRoots = cachedTraceData.TraceRoots + response.TotalSpansCount = cachedTraceData.TotalSpans + totalErrorSpans = cachedTraceData.TotalErrorSpans + } + + } + + if !useCache { + zap.L().Info("cache miss for getWaterfallSpansForTraceWithMetadata", zap.String("traceID", traceID)) + + var traceSummary model.TraceSummary + summaryQuery := fmt.Sprintf("SELECT * from %s.%s WHERE trace_id=$1", r.TraceDB, r.traceSummaryTable) + err := r.db.QueryRow(ctx, summaryQuery, traceID).Scan(&traceSummary.TraceID, &traceSummary.Start, &traceSummary.End, &traceSummary.NumSpans) + if err != nil { + if err == sql.ErrNoRows { + return response, nil + } + zap.L().Error("Error in processing sql query", zap.Error(err)) + return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query: %w", err)} + } + response.TotalSpansCount = traceSummary.NumSpans + + var searchScanResponses []model.SpanItemV2 + query := fmt.Sprintf("SELECT timestamp, duration_nano, span_id, trace_id, has_error, kind, resource_string_service$$name, name, references, attributes_string, attributes_number, attributes_bool, resources_string, events, status_message, status_code_string, kind_string , parent_span_id FROM %s.%s WHERE trace_id=$1 and ts_bucket_start>=$2 and ts_bucket_start<=$3 ORDER BY timestamp ASC, name ASC", r.TraceDB, r.traceTableName) + start := time.Now() + err = r.db.Select(ctx, &searchScanResponses, query, traceID, strconv.FormatInt(traceSummary.Start.Unix()-1800, 10), strconv.FormatInt(traceSummary.End.Unix(), 10)) + zap.L().Info(query) + if err != nil { + zap.L().Error("Error in processing sql query", zap.Error(err)) + return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query: %w", err)} + } + end := time.Now() + zap.L().Debug("GetWaterfallSpansForTraceWithMetadata took: ", zap.Duration("duration", end.Sub(start))) + + var serviceNameIntervalMap = map[string][]Interval{} + for _, item := range searchScanResponses { + ref := []model.OtelSpanRef{} + err := json.Unmarshal([]byte(item.References), &ref) + if err != nil { + zap.L().Error("Error unmarshalling references", zap.Error(err)) + return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error in unmarshalling references: %w", err)} + } + + // merge attributes_number and attributes_bool to attributes_string + for k, v := range item.Attributes_bool { + item.Attributes_string[k] = fmt.Sprintf("%v", v) + } + for k, v := range item.Attributes_number { + item.Attributes_string[k] = fmt.Sprintf("%v", v) + } + for k, v := range item.Resources_string { + item.Attributes_string[k] = v + } + + jsonItem := model.Span{ + SpanID: item.SpanID, + TraceID: item.TraceID, + ServiceName: item.ServiceName, + Name: item.Name, + Kind: int32(item.Kind), + DurationNano: item.DurationNano, + HasError: item.HasError, + StatusMessage: item.StatusMessage, + StatusCodeString: item.StatusCodeString, + SpanKind: item.SpanKind, + References: ref, + Events: item.Events, + TagMap: item.Attributes_string, + ParentSpanId: item.ParentSpanId, + Children: make([]*model.Span, 0), + } + + jsonItem.TimeUnixNano = uint64(item.TimeUnixNano.UnixNano() / 1000000) + + serviceNameIntervalMap[jsonItem.ServiceName] = + append(serviceNameIntervalMap[jsonItem.ServiceName], Interval{StartTime: jsonItem.TimeUnixNano, Duration: jsonItem.DurationNano / 1000000, Service: jsonItem.ServiceName}) + + spanIdToSpanNodeMap[jsonItem.SpanID] = &jsonItem + + if startTime == 0 || jsonItem.TimeUnixNano < startTime { + startTime = jsonItem.TimeUnixNano + } + if endTime == 0 || (jsonItem.TimeUnixNano+(jsonItem.DurationNano/1000000)) > endTime { + endTime = jsonItem.TimeUnixNano + (jsonItem.DurationNano / 1000000) + } + if durationNano == 0 || jsonItem.DurationNano > durationNano { + durationNano = jsonItem.DurationNano + } + if jsonItem.HasError { + totalErrorSpans = totalErrorSpans + 1 + } + } + + serviceNameToTotalDurationMap = calculateServiceTime(serviceNameIntervalMap) + + // traverse through the map and append each node to the children array of the parent node + // capture the root nodes as well + for _, spanNode := range spanIdToSpanNodeMap { + hasParentRelationship := false + for _, reference := range spanNode.References { + if reference.RefType == "CHILD_OF" && reference.SpanId != "" { + hasParentRelationship = true + if parentNode, exists := spanIdToSpanNodeMap[reference.SpanId]; exists { + parentNode.Children = append(parentNode.Children, spanNode) + } else { + // insert the missing spans + missingSpan := model.Span{ + SpanID: reference.SpanId, + TraceID: spanNode.TraceID, + ServiceName: "", + Name: "Missing Span", + TimeUnixNano: spanNode.TimeUnixNano, + Kind: 0, + DurationNano: spanNode.DurationNano, + HasError: false, + StatusMessage: "", + StatusCodeString: "", + SpanKind: "", + Children: make([]*model.Span, 0), + } + missingSpan.Children = append(missingSpan.Children, spanNode) + spanIdToSpanNodeMap[missingSpan.SpanID] = &missingSpan + traceRoots = append(traceRoots, &missingSpan) + } + } + } + if !hasParentRelationship { + traceRoots = append(traceRoots, spanNode) + } + } + + sort.Slice(traceRoots, func(i, j int) bool { + if traceRoots[i].TimeUnixNano == traceRoots[j].TimeUnixNano { + return traceRoots[i].Name < traceRoots[j].Name + } + return traceRoots[i].TimeUnixNano < traceRoots[j].TimeUnixNano + }) + + traceCache := model.GetWaterfallSpansForTraceWithMetadataCache{ + StartTime: startTime, + EndTime: endTime, + DurationNano: durationNano, + TotalSpans: traceSummary.NumSpans, + TotalErrorSpans: totalErrorSpans, + SpanIdToSpanNodeMap: spanIdToSpanNodeMap, + ServiceNameToTotalDurationMap: serviceNameToTotalDurationMap, + TraceRoots: traceRoots, + } + + err = r.cacheV2.Store(ctx, fmt.Sprintf("getWaterfallSpansForTraceWithMetadata-%v", traceID), &traceCache, time.Minute*5) + if err != nil { + zap.L().Debug("failed to store cache fpr getWaterfallSpansForTraceWithMetadata", zap.String("traceID", traceID), zap.Error(err)) + } + } + + var preOrderTraversal = []*model.Span{} + uncollapsedSpans := req.UncollapsedSpans + + selectedSpanIndex := -1 + for _, rootSpanID := range traceRoots { + if rootNode, exists := spanIdToSpanNodeMap[rootSpanID.SpanID]; exists { + _, _spansFromRootToNode := getPathFromRootToSelectedSpanId(rootNode, req.SelectedSpanID, uncollapsedSpans, req.IsSelectedSpanIDUnCollapsed) + uncollapsedSpans = append(uncollapsedSpans, _spansFromRootToNode...) + _preOrderTraversal := traverseTraceAndAddRequiredMetadata(rootNode, uncollapsedSpans, 0, true, false, req.SelectedSpanID) + + _selectedSpanIndex := findIndexForSelectedSpanFromPreOrder(_preOrderTraversal, req.SelectedSpanID) + if _selectedSpanIndex != -1 { + selectedSpanIndex = _selectedSpanIndex + len(preOrderTraversal) + } + preOrderTraversal = append(preOrderTraversal, _preOrderTraversal...) + + if response.RootServiceName == "" { + response.RootServiceName = rootNode.ServiceName + } + + if response.RootServiceEntryPoint == "" { + response.RootServiceEntryPoint = rootNode.Name + } + } + } + + // the index of the interested span id shouldn't be -1 as the span should exist + if selectedSpanIndex == -1 && req.SelectedSpanID != "" { + return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("selected span ID not found in the traversal")} + } + // get the 0.4*[span limit] before the interested span index + startIndex := selectedSpanIndex - 200 + // get the 0.6*[span limit] after the intrested span index + endIndex := selectedSpanIndex + 300 + // adjust the sliding window according to the available left and right spaces. + if startIndex < 0 { + endIndex = endIndex - startIndex + startIndex = 0 + } + if endIndex > len(preOrderTraversal) { + startIndex = startIndex - (endIndex - len(preOrderTraversal)) + endIndex = len(preOrderTraversal) + } + + if startIndex < 0 { + startIndex = 0 + } + selectedSpans := preOrderTraversal[startIndex:endIndex] + + // generate the response [ spans , metadata ] + response.Spans = selectedSpans + response.UncollapsedSpans = uncollapsedSpans + response.StartTimestampMillis = startTime + response.EndTimestampMillis = endTime + response.TotalErrorSpansCount = totalErrorSpans + response.ServiceNameToTotalDurationMap = serviceNameToTotalDurationMap + response.HasMissingSpans = len(traceRoots) > 1 + return response, nil +} + +func (r *ClickHouseReader) GetFlamegraphSpansForTrace(ctx context.Context, traceID string, req *model.GetFlamegraphSpansForTraceParams) (*model.GetFlamegraphSpansForTraceResponse, *model.ApiError) { + trace := new(model.GetFlamegraphSpansForTraceResponse) + var startTime, endTime, durationNano uint64 + var spanIdToSpanNodeMap = map[string]*model.FlamegraphSpan{} + // map[traceID][level]span + var traceIdLevelledFlamegraph = map[string]map[int64][]*model.FlamegraphSpan{} + var selectedSpans = [][]*model.FlamegraphSpan{} + var traceRoots []*model.FlamegraphSpan + var useCache bool = true + + // get the trace tree from cache! + cachedTraceData := new(model.GetFlamegraphSpansForTraceCache) + cacheStatus, err := r.cacheV2.Retrieve(ctx, fmt.Sprintf("getFlamegraphSpansForTrace-%v", traceID), cachedTraceData, false) + if err != nil { + zap.L().Debug("error in retrieving getFlamegraphSpansForTrace cache", zap.Error(err)) + useCache = false + } + + if cacheStatus != cache.RetrieveStatusHit { + useCache = false + } + + if err == nil && cacheStatus == cache.RetrieveStatusHit { + + if time.Since(time.UnixMilli(int64(cachedTraceData.EndTime))) < r.fluxIntervalForTraceDetail { + useCache = false + } + + if useCache { + zap.L().Info("cache is successfully hit, applying cache for getFlamegraphSpansForTrace", zap.String("traceID", traceID)) + startTime = cachedTraceData.StartTime + endTime = cachedTraceData.EndTime + durationNano = cachedTraceData.DurationNano + selectedSpans = cachedTraceData.SelectedSpans + traceRoots = cachedTraceData.TraceRoots + } + } + + if !useCache { + zap.L().Info("cache miss for getFlamegraphSpansForTrace", zap.String("traceID", traceID)) + + // fetch the start, end and number of spans from the summary table, start and end are required for the trace query + var traceSummary model.TraceSummary + summaryQuery := fmt.Sprintf("SELECT * from %s.%s WHERE trace_id=$1", r.TraceDB, r.traceSummaryTable) + err := r.db.QueryRow(ctx, summaryQuery, traceID).Scan(&traceSummary.TraceID, &traceSummary.Start, &traceSummary.End, &traceSummary.NumSpans) + if err != nil { + if err == sql.ErrNoRows { + return trace, nil + } + zap.L().Error("Error in processing sql query", zap.Error(err)) + return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query: %w", err)} + } + + // fetch all the spans belonging to the trace from the main table + var searchScanResponses []model.SpanItemV2 + query := fmt.Sprintf("SELECT timestamp, duration_nano, span_id, trace_id, has_error,references, resource_string_service$$name, name,parent_span_id FROM %s.%s WHERE trace_id=$1 and ts_bucket_start>=$2 and ts_bucket_start<=$3 ORDER BY timestamp ASC, name ASC", r.TraceDB, r.traceTableName) + start := time.Now() + err = r.db.Select(ctx, &searchScanResponses, query, traceID, strconv.FormatInt(traceSummary.Start.Unix()-1800, 10), strconv.FormatInt(traceSummary.End.Unix(), 10)) + zap.L().Info(query) + if err != nil { + zap.L().Error("Error in processing sql query", zap.Error(err)) + return nil, &model.ApiError{Typ: model.ErrorExec, Err: fmt.Errorf("error in processing sql query: %w", err)} + } + end := time.Now() + zap.L().Debug("getFlamegraphSpansForTrace took: ", zap.Duration("duration", end.Sub(start))) + + // create the trace tree based on the spans fetched above + // create a map of [spanId]: spanNode + for _, item := range searchScanResponses { + ref := []model.OtelSpanRef{} + err := json.Unmarshal([]byte(item.References), &ref) + if err != nil { + zap.L().Error("Error unmarshalling references", zap.Error(err)) + return nil, &model.ApiError{Typ: model.ErrorBadData, Err: fmt.Errorf("error in unmarshalling references: %w", err)} + } + // create the span node + jsonItem := model.FlamegraphSpan{ + SpanID: item.SpanID, + TraceID: item.TraceID, + ServiceName: item.ServiceName, + Name: item.Name, + DurationNano: (item.DurationNano), + HasError: item.HasError, + ParentSpanId: item.ParentSpanId, + References: ref, + Children: make([]*model.FlamegraphSpan, 0), + } + + jsonItem.TimeUnixNano = uint64(item.TimeUnixNano.UnixNano() / 1000000) + // assign the span node to the span map + spanIdToSpanNodeMap[jsonItem.SpanID] = &jsonItem + + // metadata calculation + if startTime == 0 || jsonItem.TimeUnixNano < startTime { + startTime = jsonItem.TimeUnixNano + } + if endTime == 0 || (jsonItem.TimeUnixNano+(jsonItem.DurationNano/1000000)) > endTime { + endTime = jsonItem.TimeUnixNano + (jsonItem.DurationNano / 1000000) + } + if durationNano == 0 || uint64(jsonItem.DurationNano) > durationNano { + durationNano = uint64(jsonItem.DurationNano) + } + } + + // traverse through the map and append each node to the children array of the parent node + for _, spanNode := range spanIdToSpanNodeMap { + hasParentRelationship := false + for _, reference := range spanNode.References { + if reference.RefType == "CHILD_OF" && reference.SpanId != "" { + hasParentRelationship = true + if parentNode, exists := spanIdToSpanNodeMap[reference.SpanId]; exists { + parentNode.Children = append(parentNode.Children, spanNode) + } else { + // insert the missing spans + missingSpan := model.FlamegraphSpan{ + SpanID: reference.SpanId, + TraceID: spanNode.TraceID, + ServiceName: "", + Name: "Missing Span", + TimeUnixNano: spanNode.TimeUnixNano, + DurationNano: spanNode.DurationNano, + HasError: false, + Children: make([]*model.FlamegraphSpan, 0), + } + missingSpan.Children = append(missingSpan.Children, spanNode) + spanIdToSpanNodeMap[missingSpan.SpanID] = &missingSpan + traceRoots = append(traceRoots, &missingSpan) + } + } + } + if !hasParentRelationship { + traceRoots = append(traceRoots, spanNode) + } + } + + sort.Slice(traceRoots, func(i, j int) bool { + if traceRoots[i].TimeUnixNano == traceRoots[j].TimeUnixNano { + return traceRoots[i].Name < traceRoots[j].Name + } + return traceRoots[i].TimeUnixNano < traceRoots[j].TimeUnixNano + }) + + var bfsMapForTrace = map[int64][]*model.FlamegraphSpan{} + for _, rootSpanID := range traceRoots { + if rootNode, exists := spanIdToSpanNodeMap[rootSpanID.SpanID]; exists { + bfsMapForTrace = map[int64][]*model.FlamegraphSpan{} + bfsTraversalForTrace(rootNode, 0, &bfsMapForTrace) + traceIdLevelledFlamegraph[rootSpanID.SpanID] = bfsMapForTrace + } + } + + for _, trace := range traceRoots { + keys := make([]int64, 0, len(traceIdLevelledFlamegraph[trace.SpanID])) + for key := range traceIdLevelledFlamegraph[trace.SpanID] { + keys = append(keys, key) + } + sort.Slice(keys, func(i, j int) bool { + return keys[i] < keys[j] + }) + + for _, level := range keys { + if ok, exists := traceIdLevelledFlamegraph[trace.SpanID][level]; exists { + selectedSpans = append(selectedSpans, ok) + } + } + } + + traceCache := model.GetFlamegraphSpansForTraceCache{ + StartTime: startTime, + EndTime: endTime, + DurationNano: durationNano, + SelectedSpans: selectedSpans, + TraceRoots: traceRoots, + } + + err = r.cacheV2.Store(ctx, fmt.Sprintf("getFlamegraphSpansForTrace-%v", traceID), &traceCache, time.Minute*5) + if err != nil { + zap.L().Debug("failed to store cache for getFlamegraphSpansForTrace", zap.String("traceID", traceID), zap.Error(err)) + } + } + + var selectedIndex int64 = 0 + if req.SelectedSpanID != "" { + selectedIndex = findIndexForSelectedSpan(selectedSpans, req.SelectedSpanID) + } + + lowerLimit := selectedIndex - 20 + upperLimit := selectedIndex + 30 + + if lowerLimit < 0 { + upperLimit = upperLimit - lowerLimit + lowerLimit = 0 + } + + if upperLimit > int64(len(selectedSpans)) { + lowerLimit = lowerLimit - (upperLimit - int64(len(selectedSpans))) + upperLimit = int64(len(selectedSpans)) + } + + if lowerLimit < 0 { + lowerLimit = 0 + } + + trace.Spans = selectedSpans[lowerLimit:upperLimit] + trace.StartTimestampMillis = startTime + trace.EndTimestampMillis = endTime + return trace, nil +} + func (r *ClickHouseReader) GetDependencyGraph(ctx context.Context, queryParams *model.GetServicesParams) (*[]model.ServiceMapDependencyResponseItem, error) { response := []model.ServiceMapDependencyResponseItem{} diff --git a/pkg/query-service/app/clickhouseReader/utils.go b/pkg/query-service/app/clickhouseReader/utils.go new file mode 100644 index 0000000000..19f3203063 --- /dev/null +++ b/pkg/query-service/app/clickhouseReader/utils.go @@ -0,0 +1,125 @@ +package clickhouseReader + +import ( + "sort" + + "go.signoz.io/signoz/pkg/query-service/model" +) + +func contains(slice []string, item string) bool { + for _, v := range slice { + if v == item { + return true + } + } + return false +} + +func getPathFromRootToSelectedSpanId(node *model.Span, selectedSpanId string, uncollapsedSpans []string, isSelectedSpanIDUnCollapsed bool) (bool, []string) { + spansFromRootToNode := []string{} + if node.SpanID == selectedSpanId { + if isSelectedSpanIDUnCollapsed { + spansFromRootToNode = append(spansFromRootToNode, node.SpanID) + } + return true, spansFromRootToNode + } + isPresentInSubtreeForTheNode := false + for _, child := range node.Children { + isPresentInThisSubtree, _spansFromRootToNode := getPathFromRootToSelectedSpanId(child, selectedSpanId, uncollapsedSpans, isSelectedSpanIDUnCollapsed) + // if the interested node is present in the given subtree then add the span node to uncollapsed node list + if isPresentInThisSubtree { + if !contains(uncollapsedSpans, node.SpanID) { + spansFromRootToNode = append(spansFromRootToNode, node.SpanID) + } + isPresentInSubtreeForTheNode = true + spansFromRootToNode = append(spansFromRootToNode, _spansFromRootToNode...) + } + } + return isPresentInSubtreeForTheNode, spansFromRootToNode +} + +func traverseTraceAndAddRequiredMetadata(span *model.Span, uncollapsedSpans []string, level uint64, isPartOfPreorder bool, hasSibling bool, selectedSpanId string) []*model.Span { + preOrderTraversal := []*model.Span{} + sort.Slice(span.Children, func(i, j int) bool { + if span.Children[i].TimeUnixNano == span.Children[j].TimeUnixNano { + return span.Children[i].Name < span.Children[j].Name + } + return span.Children[i].TimeUnixNano < span.Children[j].TimeUnixNano + }) + span.SubTreeNodeCount = 0 + nodeWithoutChildren := model.Span{ + SpanID: span.SpanID, + TraceID: span.TraceID, + ServiceName: span.ServiceName, + TimeUnixNano: span.TimeUnixNano, + Name: span.Name, + Kind: int32(span.Kind), + DurationNano: span.DurationNano, + HasError: span.HasError, + StatusMessage: span.StatusMessage, + StatusCodeString: span.StatusCodeString, + SpanKind: span.SpanKind, + References: span.References, + Events: span.Events, + TagMap: span.TagMap, + ParentSpanId: span.ParentSpanId, + Children: make([]*model.Span, 0), + HasChildren: len(span.Children) > 0, + Level: level, + HasSiblings: hasSibling, + SubTreeNodeCount: 0, + } + if isPartOfPreorder { + preOrderTraversal = append(preOrderTraversal, &nodeWithoutChildren) + } + + for index, child := range span.Children { + _childTraversal := traverseTraceAndAddRequiredMetadata(child, uncollapsedSpans, level+1, isPartOfPreorder && contains(uncollapsedSpans, span.SpanID), index != (len(span.Children)-1), selectedSpanId) + preOrderTraversal = append(preOrderTraversal, _childTraversal...) + nodeWithoutChildren.SubTreeNodeCount += child.SubTreeNodeCount + 1 + span.SubTreeNodeCount += child.SubTreeNodeCount + 1 + } + + return preOrderTraversal + +} + +func bfsTraversalForTrace(span *model.FlamegraphSpan, level int64, bfsMap *map[int64][]*model.FlamegraphSpan) { + ok, exists := (*bfsMap)[level] + span.Level = level + if exists { + (*bfsMap)[level] = append(ok, span) + } else { + (*bfsMap)[level] = []*model.FlamegraphSpan{span} + } + for _, child := range span.Children { + bfsTraversalForTrace(child, level+1, bfsMap) + } + span.Children = make([]*model.FlamegraphSpan, 0) +} + +func findIndexForSelectedSpan(spans [][]*model.FlamegraphSpan, selectedSpanId string) int64 { + var selectedSpanLevel int64 = 0 + + for index, _spans := range spans { + if len(_spans) > 0 && _spans[0].SpanID == selectedSpanId { + selectedSpanLevel = int64(index) + break + } + } + + return selectedSpanLevel +} + +func findIndexForSelectedSpanFromPreOrder(spans []*model.Span, selectedSpanId string) int { + var selectedSpanIndex = -1 + + for index, span := range spans { + if span.SpanID == selectedSpanId { + selectedSpanIndex = index + break + } + } + + return selectedSpanIndex +} diff --git a/pkg/query-service/app/http_handler.go b/pkg/query-service/app/http_handler.go index fff4fded57..6a764c51de 100644 --- a/pkg/query-service/app/http_handler.go +++ b/pkg/query-service/app/http_handler.go @@ -546,6 +546,8 @@ func (aH *APIHandler) RegisterRoutes(router *mux.Router, am *AuthMiddleware) { router.HandleFunc("/api/v2/traces/fields", am.ViewAccess(aH.traceFields)).Methods(http.MethodGet) router.HandleFunc("/api/v2/traces/fields", am.EditAccess(aH.updateTraceField)).Methods(http.MethodPost) + router.HandleFunc("/api/v2/traces/flamegraph/{traceId}", am.ViewAccess(aH.GetFlamegraphSpansForTrace)).Methods(http.MethodPost) + router.HandleFunc("/api/v2/traces/{traceId}", am.ViewAccess(aH.GetWaterfallSpansForTraceWithMetadata)).Methods(http.MethodPost) router.HandleFunc("/api/v1/version", am.OpenAccess(aH.getVersion)).Methods(http.MethodGet) router.HandleFunc("/api/v1/featureFlags", am.OpenAccess(aH.getFeatureFlags)).Methods(http.MethodGet) @@ -1777,6 +1779,52 @@ func (aH *APIHandler) SearchTraces(w http.ResponseWriter, r *http.Request) { } +func (aH *APIHandler) GetWaterfallSpansForTraceWithMetadata(w http.ResponseWriter, r *http.Request) { + traceID := mux.Vars(r)["traceId"] + if traceID == "" { + RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: errors.New("traceID is required")}, nil) + return + } + + req := new(model.GetWaterfallSpansForTraceWithMetadataParams) + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + RespondError(w, model.BadRequest(err), nil) + return + } + + result, apiErr := aH.reader.GetWaterfallSpansForTraceWithMetadata(r.Context(), traceID, req) + if apiErr != nil { + RespondError(w, apiErr, nil) + return + } + + aH.WriteJSON(w, r, result) +} + +func (aH *APIHandler) GetFlamegraphSpansForTrace(w http.ResponseWriter, r *http.Request) { + traceID := mux.Vars(r)["traceId"] + if traceID == "" { + RespondError(w, &model.ApiError{Typ: model.ErrorBadData, Err: errors.New("traceID is required")}, nil) + return + } + + req := new(model.GetFlamegraphSpansForTraceParams) + err := json.NewDecoder(r.Body).Decode(&req) + if err != nil { + RespondError(w, model.BadRequest(err), nil) + return + } + + result, apiErr := aH.reader.GetFlamegraphSpansForTrace(r.Context(), traceID, req) + if apiErr != nil { + RespondError(w, apiErr, nil) + return + } + + aH.WriteJSON(w, r, result) +} + func (aH *APIHandler) listErrors(w http.ResponseWriter, r *http.Request) { query, err := parseListErrorsRequest(r) diff --git a/pkg/query-service/app/querier/querier_test.go b/pkg/query-service/app/querier/querier_test.go index fcaf13624f..e0caf6d8c1 100644 --- a/pkg/query-service/app/querier/querier_test.go +++ b/pkg/query-service/app/querier/querier_test.go @@ -1384,6 +1384,8 @@ func Test_querier_runWindowBasedListQuery(t *testing.T) { "", true, true, + time.Duration(time.Second), + nil, ) q := &querier{ diff --git a/pkg/query-service/app/querier/v2/querier_test.go b/pkg/query-service/app/querier/v2/querier_test.go index 75defa88ef..800a684e6c 100644 --- a/pkg/query-service/app/querier/v2/querier_test.go +++ b/pkg/query-service/app/querier/v2/querier_test.go @@ -1438,6 +1438,8 @@ func Test_querier_runWindowBasedListQuery(t *testing.T) { "", true, true, + time.Duration(time.Second), + nil, ) q := &querier{ diff --git a/pkg/query-service/app/server.go b/pkg/query-service/app/server.go index f03e96a874..3ad81b8223 100644 --- a/pkg/query-service/app/server.go +++ b/pkg/query-service/app/server.go @@ -62,18 +62,19 @@ type ServerOptions struct { HTTPHostPort string PrivateHostPort string // alert specific params - DisableRules bool - RuleRepoURL string - PreferSpanMetrics bool - MaxIdleConns int - MaxOpenConns int - DialTimeout time.Duration - CacheConfigPath string - FluxInterval string - Cluster string - UseLogsNewSchema bool - UseTraceNewSchema bool - SigNoz *signoz.SigNoz + DisableRules bool + RuleRepoURL string + PreferSpanMetrics bool + MaxIdleConns int + MaxOpenConns int + DialTimeout time.Duration + CacheConfigPath string + FluxInterval string + FluxIntervalForTraceDetail string + Cluster string + UseLogsNewSchema bool + UseTraceNewSchema bool + SigNoz *signoz.SigNoz } // Server runs HTTP, Mux and a grpc server @@ -123,6 +124,25 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { readerReady := make(chan bool) + var c cache.Cache + if serverOptions.CacheConfigPath != "" { + cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath) + if err != nil { + return nil, err + } + c = cache.NewCache(cacheOpts) + } + + fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval) + if err != nil { + return nil, err + } + + fluxIntervalForTraceDetail, err := time.ParseDuration(serverOptions.FluxIntervalForTraceDetail) + if err != nil { + return nil, err + } + var reader interfaces.Reader storage := os.Getenv("STORAGE") if storage == "clickhouse" { @@ -137,6 +157,8 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { serverOptions.Cluster, serverOptions.UseLogsNewSchema, serverOptions.UseTraceNewSchema, + fluxIntervalForTraceDetail, + nil, ) go clickhouseReader.Start(readerReady) reader = clickhouseReader @@ -151,14 +173,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } } - var c cache.Cache - if serverOptions.CacheConfigPath != "" { - cacheOpts, err := cache.LoadFromYAMLCacheConfigFile(serverOptions.CacheConfigPath) - if err != nil { - return nil, err - } - c = cache.NewCache(cacheOpts) - } <-readerReady rm, err := makeRulesManager( @@ -169,11 +183,6 @@ func NewServer(serverOptions *ServerOptions) (*Server, error) { return nil, err } - fluxInterval, err := time.ParseDuration(serverOptions.FluxInterval) - if err != nil { - return nil, err - } - integrationsController, err := integrations.NewController(serverOptions.SigNoz.SQLStore.SQLxDB()) if err != nil { return nil, fmt.Errorf("couldn't create integrations controller: %w", err) diff --git a/pkg/query-service/interfaces/interface.go b/pkg/query-service/interfaces/interface.go index ac4ab91f9e..0199113a74 100644 --- a/pkg/query-service/interfaces/interface.go +++ b/pkg/query-service/interfaces/interface.go @@ -41,6 +41,8 @@ type Reader interface { // Search Interfaces SearchTraces(ctx context.Context, params *model.SearchTracesParams, smartTraceAlgorithm func(payload []model.SearchSpanResponseItem, targetSpanId string, levelUp int, levelDown int, spanLimit int) ([]model.SearchSpansResult, error)) (*[]model.SearchSpansResult, error) + GetWaterfallSpansForTraceWithMetadata(ctx context.Context, traceID string, req *model.GetWaterfallSpansForTraceWithMetadataParams) (*model.GetWaterfallSpansForTraceWithMetadataResponse, *model.ApiError) + GetFlamegraphSpansForTrace(ctx context.Context, traceID string, req *model.GetFlamegraphSpansForTraceParams) (*model.GetFlamegraphSpansForTraceResponse, *model.ApiError) // Setter Interfaces SetTTL(ctx context.Context, ttlParams *model.TTLParams) (*model.SetTTLResponseItem, *model.ApiError) diff --git a/pkg/query-service/main.go b/pkg/query-service/main.go index 76498d7372..7fd7c35792 100644 --- a/pkg/query-service/main.go +++ b/pkg/query-service/main.go @@ -45,7 +45,7 @@ func main() { var useLogsNewSchema bool var useTraceNewSchema bool // the url used to build link in the alert messages in slack and other systems - var ruleRepoURL, cacheConfigPath, fluxInterval string + var ruleRepoURL, cacheConfigPath, fluxInterval, fluxIntervalForTraceDetail string var cluster string var preferSpanMetrics bool @@ -63,6 +63,7 @@ func main() { flag.StringVar(&ruleRepoURL, "rules.repo-url", constants.AlertHelpPage, "(host address used to build rule link in alert messages)") flag.StringVar(&cacheConfigPath, "experimental.cache-config", "", "(cache config to use)") flag.StringVar(&fluxInterval, "flux-interval", "5m", "(the interval to exclude data from being cached to avoid incorrect cache for data in motion)") + flag.StringVar(&fluxIntervalForTraceDetail, "flux-interval-trace-detail", "2m", "(the interval to exclude data from being cached to avoid incorrect cache for trace data in motion)") flag.StringVar(&cluster, "cluster", "cluster", "(cluster name - defaults to 'cluster')") // Allow using the consistent naming with the signoz collector flag.StringVar(&cluster, "cluster-name", "cluster", "(cluster name - defaults to 'cluster')") @@ -95,23 +96,24 @@ func main() { } serverOptions := &app.ServerOptions{ - Config: config, - HTTPHostPort: constants.HTTPHostPort, - PromConfigPath: promConfigPath, - SkipTopLvlOpsPath: skipTopLvlOpsPath, - PreferSpanMetrics: preferSpanMetrics, - PrivateHostPort: constants.PrivateHostPort, - DisableRules: disableRules, - RuleRepoURL: ruleRepoURL, - MaxIdleConns: maxIdleConns, - MaxOpenConns: maxOpenConns, - DialTimeout: dialTimeout, - CacheConfigPath: cacheConfigPath, - FluxInterval: fluxInterval, - Cluster: cluster, - UseLogsNewSchema: useLogsNewSchema, - UseTraceNewSchema: useTraceNewSchema, - SigNoz: signoz, + Config: config, + HTTPHostPort: constants.HTTPHostPort, + PromConfigPath: promConfigPath, + SkipTopLvlOpsPath: skipTopLvlOpsPath, + PreferSpanMetrics: preferSpanMetrics, + PrivateHostPort: constants.PrivateHostPort, + DisableRules: disableRules, + RuleRepoURL: ruleRepoURL, + MaxIdleConns: maxIdleConns, + MaxOpenConns: maxOpenConns, + DialTimeout: dialTimeout, + CacheConfigPath: cacheConfigPath, + FluxInterval: fluxInterval, + FluxIntervalForTraceDetail: fluxIntervalForTraceDetail, + Cluster: cluster, + UseLogsNewSchema: useLogsNewSchema, + UseTraceNewSchema: useTraceNewSchema, + SigNoz: signoz, } // Read the jwt secret key diff --git a/pkg/query-service/model/queryParams.go b/pkg/query-service/model/queryParams.go index 342f8f10f0..e2717fbd87 100644 --- a/pkg/query-service/model/queryParams.go +++ b/pkg/query-service/model/queryParams.go @@ -315,6 +315,16 @@ type SearchTracesParams struct { MaxSpansInTrace int `json:"maxSpansInTrace"` } +type GetWaterfallSpansForTraceWithMetadataParams struct { + SelectedSpanID string `json:"selectedSpanId"` + IsSelectedSpanIDUnCollapsed bool `json:"isSelectedSpanIDUnCollapsed"` + UncollapsedSpans []string `json:"uncollapsedSpans"` +} + +type GetFlamegraphSpansForTraceParams struct { + SelectedSpanID string `json:"selectedSpanId"` +} + type SpanFilterParams struct { TraceID []string `json:"traceID"` Status []string `json:"status"` diff --git a/pkg/query-service/model/response.go b/pkg/query-service/model/response.go index fe8aec6765..7ef1c1d948 100644 --- a/pkg/query-service/model/response.go +++ b/pkg/query-service/model/response.go @@ -269,6 +269,102 @@ type SearchSpanResponseItem struct { SpanKind string `json:"spanKind"` } +type Span struct { + TimeUnixNano uint64 `json:"timestamp"` + DurationNano uint64 `json:"durationNano"` + SpanID string `json:"spanId"` + RootSpanID string `json:"rootSpanId"` + ParentSpanId string `json:"parentSpanId"` + TraceID string `json:"traceId"` + HasError bool `json:"hasError"` + Kind int32 `json:"kind"` + ServiceName string `json:"serviceName"` + Name string `json:"name"` + References []OtelSpanRef `json:"references,omitempty"` + TagMap map[string]string `json:"tagMap"` + Events []string `json:"event"` + RootName string `json:"rootName"` + StatusMessage string `json:"statusMessage"` + StatusCodeString string `json:"statusCodeString"` + SpanKind string `json:"spanKind"` + Children []*Span `json:"children"` + + // the below two fields are for frontend to render the spans + SubTreeNodeCount uint64 `json:"subTreeNodeCount"` + HasChildren bool `json:"hasChildren"` + HasSiblings bool `json:"hasSiblings"` + Level uint64 `json:"level"` +} + +type FlamegraphSpan struct { + TimeUnixNano uint64 `json:"timestamp"` + DurationNano uint64 `json:"durationNano"` + SpanID string `json:"spanId"` + ParentSpanId string `json:"parentSpanId"` + TraceID string `json:"traceId"` + HasError bool `json:"hasError"` + ServiceName string `json:"serviceName"` + Name string `json:"name"` + Level int64 `json:"level"` + References []OtelSpanRef `json:"references,omitempty"` + Children []*FlamegraphSpan `json:"children"` +} + +type GetWaterfallSpansForTraceWithMetadataCache struct { + StartTime uint64 `json:"startTime"` + EndTime uint64 `json:"endTime"` + DurationNano uint64 `json:"durationNano"` + TotalSpans uint64 `json:"totalSpans"` + TotalErrorSpans uint64 `json:"totalErrorSpans"` + ServiceNameToTotalDurationMap map[string]uint64 `json:"serviceNameToTotalDurationMap"` + SpanIdToSpanNodeMap map[string]*Span `json:"spanIdToSpanNodeMap"` + TraceRoots []*Span `json:"traceRoots"` +} + +func (c *GetWaterfallSpansForTraceWithMetadataCache) MarshalBinary() (data []byte, err error) { + return json.Marshal(c) +} +func (c *GetWaterfallSpansForTraceWithMetadataCache) UnmarshalBinary(data []byte) error { + return json.Unmarshal(data, c) +} + +type GetFlamegraphSpansForTraceCache struct { + StartTime uint64 `json:"startTime"` + EndTime uint64 `json:"endTime"` + DurationNano uint64 `json:"durationNano"` + SelectedSpans [][]*FlamegraphSpan `json:"selectedSpans"` + TraceRoots []*FlamegraphSpan `json:"traceRoots"` +} + +func (c *GetFlamegraphSpansForTraceCache) MarshalBinary() (data []byte, err error) { + return json.Marshal(c) +} +func (c *GetFlamegraphSpansForTraceCache) UnmarshalBinary(data []byte) error { + return json.Unmarshal(data, c) +} + +type GetWaterfallSpansForTraceWithMetadataResponse struct { + StartTimestampMillis uint64 `json:"startTimestampMillis"` + EndTimestampMillis uint64 `json:"endTimestampMillis"` + DurationNano uint64 `json:"durationNano"` + RootServiceName string `json:"rootServiceName"` + RootServiceEntryPoint string `json:"rootServiceEntryPoint"` + TotalSpansCount uint64 `json:"totalSpansCount"` + TotalErrorSpansCount uint64 `json:"totalErrorSpansCount"` + ServiceNameToTotalDurationMap map[string]uint64 `json:"serviceNameToTotalDurationMap"` + Spans []*Span `json:"spans"` + HasMissingSpans bool `json:"hasMissingSpans"` + // this is needed for frontend and query service sync + UncollapsedSpans []string `json:"uncollapsedSpans"` +} + +type GetFlamegraphSpansForTraceResponse struct { + StartTimestampMillis uint64 `json:"startTimestampMillis"` + EndTimestampMillis uint64 `json:"endTimestampMillis"` + DurationNano uint64 `json:"durationNano"` + Spans [][]*FlamegraphSpan `json:"spans"` +} + type OtelSpanRef struct { TraceId string `json:"traceId,omitempty"` SpanId string `json:"spanId,omitempty"` diff --git a/pkg/query-service/model/trace.go b/pkg/query-service/model/trace.go index e8d3d70ac2..ccee5a83d2 100644 --- a/pkg/query-service/model/trace.go +++ b/pkg/query-service/model/trace.go @@ -20,6 +20,7 @@ type SpanItemV2 struct { StatusMessage string `ch:"status_message"` StatusCodeString string `ch:"status_code_string"` SpanKind string `ch:"kind_string"` + ParentSpanId string `ch:"parent_span_id"` } type TraceSummary struct { diff --git a/pkg/query-service/rules/threshold_rule_test.go b/pkg/query-service/rules/threshold_rule_test.go index 5a4ab9c970..7218ca2167 100644 --- a/pkg/query-service/rules/threshold_rule_test.go +++ b/pkg/query-service/rules/threshold_rule_test.go @@ -1241,7 +1241,7 @@ func TestThresholdRuleUnitCombinations(t *testing.T) { } options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace") - reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true) + reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true, time.Duration(time.Second), nil) rule, err := NewThresholdRule("69", &postableRule, fm, reader, true, true) rule.TemporalityMap = map[string]map[v3.Temporality]bool{ @@ -1340,7 +1340,7 @@ func TestThresholdRuleNoData(t *testing.T) { } options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace") - reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true) + reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true, time.Duration(time.Second), nil) rule, err := NewThresholdRule("69", &postableRule, fm, reader, true, true) rule.TemporalityMap = map[string]map[v3.Temporality]bool{ @@ -1448,7 +1448,7 @@ func TestThresholdRuleTracesLink(t *testing.T) { } options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace") - reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true) + reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true, time.Duration(time.Second), nil) rule, err := NewThresholdRule("69", &postableRule, fm, reader, true, true) rule.TemporalityMap = map[string]map[v3.Temporality]bool{ @@ -1573,7 +1573,7 @@ func TestThresholdRuleLogsLink(t *testing.T) { } options := clickhouseReader.NewOptions("", 0, 0, 0, "", "archiveNamespace") - reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true) + reader := clickhouseReader.NewReaderFromClickhouseConnection(mock, options, nil, "", fm, "", true, true, time.Duration(time.Second), nil) rule, err := NewThresholdRule("69", &postableRule, fm, reader, true, true) rule.TemporalityMap = map[string]map[v3.Temporality]bool{ diff --git a/pkg/query-service/tests/integration/test_utils.go b/pkg/query-service/tests/integration/test_utils.go index c1c71808a9..e2db99d019 100644 --- a/pkg/query-service/tests/integration/test_utils.go +++ b/pkg/query-service/tests/integration/test_utils.go @@ -47,6 +47,8 @@ func NewMockClickhouseReader( "", true, true, + time.Duration(time.Second), + nil, ) return reader, mockDB