diff --git a/.cypress/integration/app_analytics_test/app_analytics.spec.js b/.cypress/integration/app_analytics_test/app_analytics.spec.js index 5fd11999c..fbc9abb0b 100644 --- a/.cypress/integration/app_analytics_test/app_analytics.spec.js +++ b/.cypress/integration/app_analytics_test/app_analytics.spec.js @@ -322,7 +322,7 @@ describe('Viewing application', () => { cy.get('[title="03f9c770db5ee2f1caac0afc36db49ba"]').click(); cy.get('[data-test-subj="traceDetailFlyoutTitle"]').should('be.visible'); cy.get('[data-test-subj="traceDetailFlyout"]').within(($flyout) => { - cy.get('[data-test-subj="LatencyDescriptionList"]').should('contain', '224.99'); + cy.get('[data-test-subj="LatencyDescriptionList"]').should('contain', '225.00'); }); cy.get('[data-test-subj="euiFlyoutCloseButton"]').click(); cy.get('[data-test-subj="traceDetailFlyout"]').should('not.exist'); diff --git a/.cypress/integration/trace_analytics_test/trace_analytics_traces.spec.js b/.cypress/integration/trace_analytics_test/trace_analytics_traces.spec.js index a923f1548..da660e521 100644 --- a/.cypress/integration/trace_analytics_test/trace_analytics_traces.spec.js +++ b/.cypress/integration/trace_analytics_test/trace_analytics_traces.spec.js @@ -282,4 +282,21 @@ describe('Testing switch mode to jaeger', () => { cy.contains('Time spent by service').should('exist'); cy.get("[data-test-subj='span-gantt-chart-panel']").should('exist'); }); + + it('Checks tree view for specific traceId in Jaeger mode', () => { + cy.contains('15b0b4004a651c4c').click(); + cy.get('[data-test-subj="globalLoadingIndicator"]').should('not.exist'); + + cy.get('.euiButtonGroup').contains('Tree view').click(); + cy.get("[data-test-subj='treeExpandAll']").should('exist'); + cy.get("[data-test-subj='treeCollapseAll']").should('exist'); + + // Waiting time for render to complete + cy.get("[data-test-subj='treeExpandAll']").click(); + cy.get("[data-test-subj='treeCollapseAll']").click(); + + cy.get("[data-test-subj='treeViewExpandArrow']").should('have.length', 1); + cy.get("[data-test-subj='treeExpandAll']").click(); + cy.get("[data-test-subj='treeViewExpandArrow']").should('have.length.greaterThan', 1); + }); }); diff --git a/public/components/application_analytics/__tests__/__snapshots__/flyout.test.tsx.snap b/public/components/application_analytics/__tests__/__snapshots__/flyout.test.tsx.snap index 354ce94ee..c40e78b76 100644 --- a/public/components/application_analytics/__tests__/__snapshots__/flyout.test.tsx.snap +++ b/public/components/application_analytics/__tests__/__snapshots__/flyout.test.tsx.snap @@ -750,6 +750,7 @@ exports[`Trace Detail Render Flyout component render trace detail 1`] = ` mode="jaeger" openSpanFlyout={[Function]} page="app" + payloadData="" traceId="mockTrace" >
- + - + - + @@ -1166,30 +1178,298 @@ exports[`Trace Detail Render Flyout component render trace detail 1`] = ` -
+
+ + - - - - - - - - -
+ +
+ + +
+ + +
+ + +
+ + +
+
diff --git a/public/components/application_analytics/components/flyout_components/trace_detail_render.tsx b/public/components/application_analytics/components/flyout_components/trace_detail_render.tsx index 9f353ca92..b8521dc13 100644 --- a/public/components/application_analytics/components/flyout_components/trace_detail_render.tsx +++ b/public/components/application_analytics/components/flyout_components/trace_detail_render.tsx @@ -9,12 +9,12 @@ import { HttpStart } from '../../../../../../../src/core/public'; import { TraceAnalyticsMode } from '../../../../../common/types/trace_analytics'; import { ServiceBreakdownPanel } from '../../../trace_analytics/components/traces/service_breakdown_panel'; import { SpanDetailPanel } from '../../../trace_analytics/components/traces/span_detail_panel'; -import { - handlePayloadRequest, - handleServicesPieChartRequest, - handleTraceViewRequest, -} from '../../../trace_analytics/requests/traces_request_handler'; +import { handlePayloadRequest } from '../../../trace_analytics/requests/traces_request_handler'; import { getListItem } from '../../helpers/utils'; +import { + getOverviewFields, + getServiceBreakdownData, +} from '../../../trace_analytics/components/traces/trace_view_helpers'; interface TraceDetailRenderProps { traceId: string; @@ -78,6 +78,7 @@ export const TraceDetailRender = ({ mode={mode} dataSourceMDSId={dataSourceMDSId} isApplicationFlyout={true} + payloadData={payloadData} /> @@ -95,10 +96,29 @@ export const TraceDetailRender = ({ }, [traceId, fields, serviceBreakdownData, colorMap, payloadData]); useEffect(() => { - handleTraceViewRequest(traceId, http, fields, setFields, mode); - handleServicesPieChartRequest(traceId, http, setServiceBreakdownData, setColorMap, mode); handlePayloadRequest(traceId, http, payloadData, setPayloadData, mode); }, [traceId]); + useEffect(() => { + if (!payloadData) return; + + try { + const parsedPayload = JSON.parse(payloadData); + const overview = getOverviewFields(parsedPayload, mode); + if (overview) { + setFields(overview); + } + + const { + serviceBreakdownData: queryServiceBreakdownData, + colorMap: queryColorMap, + } = getServiceBreakdownData(parsedPayload, mode); + setServiceBreakdownData(queryServiceBreakdownData); + setColorMap(queryColorMap); + } catch (error) { + console.error('Error processing payloadData:', error); + } + }, [payloadData, mode]); + return renderContent; }; diff --git a/public/components/trace_analytics/components/common/constants.tsx b/public/components/trace_analytics/components/common/constants.tsx new file mode 100644 index 000000000..9c723c5d6 --- /dev/null +++ b/public/components/trace_analytics/components/common/constants.tsx @@ -0,0 +1,61 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +// Conversion factor for nanoseconds to milliseconds +export const NANOS_TO_MS = 1e6; + +export const MILI_TO_SEC = 1000; + +export const pieChartColors = [ + '#7492e7', + '#c33d69', + '#2ea597', + '#8456ce', + '#e07941', + '#3759ce', + '#ce567c', + '#9469d6', + '#4066df', + '#da7596', +]; + +export interface Span { + traceId: string; + spanId: string; + traceState: string; + parentSpanId: string; + name: string; + kind: string; + startTime: string; + endTime: string; + durationInNanos: number; + serviceName: string; + events: any[]; + links: any[]; + droppedAttributesCount: number; + droppedEventsCount: number; + droppedLinksCount: number; + traceGroup: string; + traceGroupFields: { + endTime: string; + durationInNanos: number; + statusCode: number; + }; + status: { + code: number; + }; + instrumentationLibrary: { + name: string; + version: string; + }; +} + +export interface ParsedHit { + _index: string; + _id: string; + _score: number; + _source: Span; + sort?: any[]; +} diff --git a/public/components/trace_analytics/components/common/helper_functions.tsx b/public/components/trace_analytics/components/common/helper_functions.tsx index 4b91ebf7e..4a3f2852c 100644 --- a/public/components/trace_analytics/components/common/helper_functions.tsx +++ b/public/components/trace_analytics/components/common/helper_functions.tsx @@ -28,6 +28,7 @@ import { FieldCapResponse } from '../../../common/types'; import { serviceMapColorPalette } from './color_palette'; import { FilterType } from './filters/filters'; import { ServiceObject } from './plots/service_map'; +import { NANOS_TO_MS, ParsedHit } from './constants'; const missingJaegerTracesConfigurationMessage = `The indices required for trace analytics (${JAEGER_INDEX_NAME} and ${JAEGER_SERVICE_INDEX_NAME}) do not exist or you do not have permission to access them.`; @@ -615,3 +616,39 @@ export const generateServiceUrl = ( return url; }; + +/* + * Parse an ISO timestamp with up to nanosecond precision. + * For example, "2025-01-28T03:12:37.293990144Z" will be converted + * to a number representing the total nanoseconds since the Unix epoch. + */ +export function parseIsoToNano(iso: string): number { + const match = iso.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2})(?:\.(\d+))?Z$/); + if (!match) { + throw new Error(`Invalid ISO timestamp: ${iso}`); + } + // Parse the base part using Date.parse (which gives ms) + const baseMs = new Date(match[1] + 'Z').getTime(); + // Get the fractional part (if any), pad to 9 digits for nanosecond precision + let fraction = match[2] || '0'; + fraction = fraction.padEnd(9, '0'); // ensure it has 9 digits + return baseMs * NANOS_TO_MS + Number(fraction); +} + +export const parseHits = (payloadData: string): ParsedHit[] => { + try { + const parsed = JSON.parse(payloadData); + let hits: ParsedHit[] = []; + + if (parsed.hits && Array.isArray(parsed.hits.hits)) { + hits = parsed.hits.hits; + } else if (Array.isArray(parsed)) { + hits = parsed; + } + + return hits; + } catch (error) { + console.error('Error processing payloadData:', error); + return []; + } +}; diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/service_breakdown_panel.test.tsx.snap b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/service_breakdown_panel.test.tsx.snap index aa3f958c2..2cc0f5ec4 100644 --- a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/service_breakdown_panel.test.tsx.snap +++ b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/service_breakdown_panel.test.tsx.snap @@ -135,6 +135,7 @@ exports[`Service breakdown panel component renders service breakdown panel 1`] = data={ Array [ Object { + "hoverinfo": "label+percent", "hovertemplate": "%{label}
%{value:.2f}%", "labels": Array [ "inventory", @@ -177,6 +178,7 @@ exports[`Service breakdown panel component renders service breakdown panel 1`] = data={ Array [ Object { + "hoverinfo": "label+percent", "hovertemplate": "%{label}
%{value:.2f}%", "labels": Array [ "inventory", diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_panel.test.tsx.snap b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_panel.test.tsx.snap index b5304fe4f..7c1cf818c 100644 --- a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_panel.test.tsx.snap +++ b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/span_detail_panel.test.tsx.snap @@ -16,7 +16,7 @@ exports[`SpanDetailPanel component renders correctly with default props 1`] = ` -
+
+
+
+
+
+
+
+
+
+

+ No matches +

- - - - - - - - - - +
- - +
+ No data matches the selected filter. Clear the filter and/or increase the time range to see more results. +
+
+
`; exports[`SpanDetailTableHierarchy renders the empty component 1`] = ` -
- +
- + Full screen + , + + Expand all + , + + Collapse all + , + ], + "showColumnSelector": true, + "showFullScreenSelector": false, + "showSortSelector": false, + } + } > - - - - + + +
+ +
+
+
+ +
+ + + + + +
+ + + +
+ + - - - + } + title={ +

+ No matches +

+ } + > +
+ - - - + No matches + + + - - - -
+ className="euiTextColor euiTextColor--subdued" + > + +
+ + +
+ +
+ No data matches the selected filter. Clear the filter and/or increase the time range to see more results. +
+
+
+
+ + +
+
+ +
+ + `; exports[`SpanDetailTableHierarchy renders the jaeger component with data 1`] = `
-
+
+
+
+
+
+
+
+
+
+

+ No matches +

- - - - - - - - - - +
- - +
+ No data matches the selected filter. Clear the filter and/or increase the time range to see more results. +
+
+
`; diff --git a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/trace_view.test.tsx.snap b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/trace_view.test.tsx.snap index de6abbe6c..1cf21dfba 100644 --- a/public/components/trace_analytics/components/traces/__tests__/__snapshots__/trace_view.test.tsx.snap +++ b/public/components/trace_analytics/components/traces/__tests__/__snapshots__/trace_view.test.tsx.snap @@ -142,8 +142,11 @@ exports[`Trace view component renders trace view 1`] = ` dataSourceMDSId="" dataSourceMDSLabel="" http={[MockFunction]} + isGanttChartLoading={false} mode="data_prepper" - setData={[Function]} + payloadData="" + setGanttChartLoading={[Function]} + setGanttData={[Function]} traceId="test" /> diff --git a/public/components/trace_analytics/components/traces/__tests__/span_detail_panel.test.tsx b/public/components/trace_analytics/components/traces/__tests__/span_detail_panel.test.tsx index 73864af24..bb3061776 100644 --- a/public/components/trace_analytics/components/traces/__tests__/span_detail_panel.test.tsx +++ b/public/components/trace_analytics/components/traces/__tests__/span_detail_panel.test.tsx @@ -87,16 +87,19 @@ describe('SpanDetailPanel component', () => { expect(wrapper).toMatchSnapshot(); }); - it('displays loading chart initially', () => { - const wrapper = mount(); + it('displays loading chart when isGanttChartLoading is true', () => { + const wrapper = mount(); expect(wrapper.find('EuiLoadingChart')).toHaveLength(1); }); + it('does not display loading chart when isGanttChartLoading is false', () => { + const wrapper = mount(); + expect(wrapper.find('EuiLoadingChart')).toHaveLength(0); + }); + it('renders gantt chart and mini-map correctly', async () => { const wrapper = mount(); - expect(wrapper.find('EuiLoadingChart')).toHaveLength(1); // Ensure loading state appears - await waitFor(() => { wrapper.update(); expect(wrapper.find(Plt)).toHaveLength(2); // Gantt chart and mini-map appear diff --git a/public/components/trace_analytics/components/traces/__tests__/span_detail_table.test.tsx b/public/components/trace_analytics/components/traces/__tests__/span_detail_table.test.tsx index a2edecddc..87a47bec8 100644 --- a/public/components/trace_analytics/components/traces/__tests__/span_detail_table.test.tsx +++ b/public/components/trace_analytics/components/traces/__tests__/span_detail_table.test.tsx @@ -36,7 +36,6 @@ describe('SpanDetailTable', () => { {}} mode="data_prepper" dataSourceMDSId="testDataSource" @@ -60,7 +59,6 @@ describe('SpanDetailTable', () => { setCurrentSpan(spanId)} mode="data_prepper" dataSourceMDSId="testDataSource" @@ -81,7 +79,6 @@ describe('SpanDetailTable', () => { setCurrentSpan(spanId)} mode="jaeger" dataSourceMDSId="testDataSource" @@ -247,23 +244,6 @@ describe('SpanDetailTable', () => { expect(updatedSorting.columns).toEqual(newSorting); }); }); - - it('should disable sorting in Jaeger mode', async () => { - const wrapper = mount( - - ); - - await waitFor(() => { - wrapper.update(); - expect(wrapper.find('EuiDataGrid').prop('sorting')).toBeUndefined(); - }); - }); }); }); @@ -278,7 +258,6 @@ describe('SpanDetailTableHierarchy', () => { {}} mode="data_prepper" dataSourceMDSId="testDataSource" @@ -299,7 +278,6 @@ describe('SpanDetailTableHierarchy', () => { setCurrentSpan(spanId)} mode="data_prepper" dataSourceMDSId="testDataSource" @@ -319,7 +297,6 @@ describe('SpanDetailTableHierarchy', () => { setCurrentSpan(spanId)} mode="jaeger" dataSourceMDSId="testDataSource" diff --git a/public/components/trace_analytics/components/traces/__tests__/trace_view_helpers.test.tsx b/public/components/trace_analytics/components/traces/__tests__/trace_view_helpers.test.tsx new file mode 100644 index 000000000..62ae04793 --- /dev/null +++ b/public/components/trace_analytics/components/traces/__tests__/trace_view_helpers.test.tsx @@ -0,0 +1,147 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import moment from 'moment'; +import { getOverviewFields, getServiceBreakdownData } from '../trace_view_helpers'; +import { normalizePayload } from '../../../requests/traces_request_handler'; + +const dataPrepperPayload = { + hits: { + hits: [ + { + _source: { + traceId: 'def', + traceGroup: 'TestGroup', + traceGroupFields: { + endTime: '2023-02-05T12:00:00Z', + durationInNanos: 5000000, + statusCode: 0, + }, + serviceName: 'serviceA', + startTime: '2023-02-05T11:59:59Z', + }, + }, + { + _source: { + traceId: 'def', + traceGroup: 'TestGroup', + traceGroupFields: { + endTime: '2023-02-05T12:00:10Z', + durationInNanos: 10000000, + statusCode: 0, + }, + serviceName: 'serviceB', + startTime: '2023-02-05T11:59:50Z', + }, + }, + ], + }, +}; + +const jaegerPayload = { + hits: { + hits: [ + { + _source: { + traceID: 'abc', + operationName: 'opA', + startTime: 1000000, + duration: 2000000, + tag: { error: false }, + process: { serviceName: 'serviceX' }, + }, + }, + { + _source: { + traceID: 'abc', + operationName: 'opB', + startTime: 1500000, + duration: 3000000, + tag: { error: true }, + process: { serviceName: 'serviceY' }, + }, + }, + ], + }, +}; + +describe('overviewAndPieHelpers', () => { + describe('normalizePayload', () => { + it('should return hits.hits if payload is an object with that structure', () => { + const obj = { hits: { hits: [4, 5, 6] } }; + expect(normalizePayload(obj)).toEqual([4, 5, 6]); + }); + it('should return an empty array for unexpected input', () => { + const obj = { foo: 'bar' }; + expect(normalizePayload(obj)).toEqual([]); + }); + }); + + describe('getOverviewFields', () => { + it('should return correct overview fields for jaeger mode', () => { + // Sorting as handlePayloadRequest does in descending order + const sortedJaegerPayload = jaegerPayload.hits.hits.sort( + (a, b) => b._source.startTime - a._source.startTime + ); + const overview = getOverviewFields(sortedJaegerPayload, 'jaeger'); + expect(overview).toBeTruthy(); + expect(overview?.trace_id).toBe('abc'); + expect(overview?.trace_group).toBe('opA'); + const startTimeMillis = 1000000 / 1000; + const durationMillis = 2000000 / 1000; + const lastUpdated = startTimeMillis + durationMillis; + const expectedLastUpdated = moment(lastUpdated).format('MM/DD/YYYY HH:mm:ss.SSS'); + const latencyInMilliseconds = durationMillis.toFixed(2); + expect(overview?.last_updated).toBe(expectedLastUpdated); + expect(overview?.latency).toBe(`${latencyInMilliseconds} ms`); + expect(overview?.error_count).toBe(1); + }); + + it('should return correct overview fields for data prepper mode', () => { + // Sorting as handlePayloadRequest does in descending order + const sortedDataPrepperPayload = dataPrepperPayload.hits.hits.sort( + (a, b) => + new Date(b._source.traceGroupFields.endTime).getTime() - + new Date(a._source.traceGroupFields.endTime).getTime() + ); + const overview = getOverviewFields(sortedDataPrepperPayload, 'data_prepper'); + expect(overview).toBeTruthy(); + expect(overview?.trace_id).toBe('def'); + expect(overview?.trace_group).toBe('TestGroup'); + const expectedLastUpdated = moment('2023-02-05T12:00:00Z').format('MM/DD/YYYY HH:mm:ss.SSS'); + expect(overview?.last_updated).toBe(expectedLastUpdated); + expect(overview?.latency).toBe('5.00 ms'); + expect(overview?.error_count).toBe(0); + }); + }); + + describe('getServiceBreakdownData', () => { + it('should return correct service breakdown data for data prepper mode', () => { + const { serviceBreakdownData, colorMap } = getServiceBreakdownData( + dataPrepperPayload.hits.hits, // Pass hits.hits directly + 'data_prepper' + ); + expect(serviceBreakdownData).toBeDefined(); + expect(Array.isArray(serviceBreakdownData)).toBe(true); + expect(colorMap).toBeDefined(); + const labels = serviceBreakdownData[0].labels; + expect(labels).toContain('serviceA'); + expect(labels).toContain('serviceB'); + }); + + it('should return correct service breakdown data for jaeger mode', () => { + const { serviceBreakdownData, colorMap } = getServiceBreakdownData( + jaegerPayload.hits.hits, + 'jaeger' + ); + expect(serviceBreakdownData).toBeDefined(); + expect(Array.isArray(serviceBreakdownData)).toBe(true); + expect(colorMap).toBeDefined(); + const labels = serviceBreakdownData[0].labels; + expect(labels).toContain('serviceX'); + expect(labels).toContain('serviceY'); + }); + }); +}); diff --git a/public/components/trace_analytics/components/traces/service_breakdown_panel.tsx b/public/components/trace_analytics/components/traces/service_breakdown_panel.tsx index 199397917..fd25ea3c9 100644 --- a/public/components/trace_analytics/components/traces/service_breakdown_panel.tsx +++ b/public/components/trace_analytics/components/traces/service_breakdown_panel.tsx @@ -77,6 +77,19 @@ export function ServiceBreakdownPanel(props: { data: ServiceBreakdownData[]; isL ); }; + const pieChartData = useMemo(() => { + if (props.data.length === 0) return []; + + return [ + { + ...props.data[0], + type: 'pie', + textinfo: 'none', + hoverinfo: 'label+percent', + }, + ]; + }, [props.data]); + const stats = useMemo(() => renderStats(), [props.data]); return ( @@ -91,7 +104,7 @@ export function ServiceBreakdownPanel(props: { data: ServiceBreakdownData[]; isL - {props.data?.length > 0 ? : null} + {pieChartData.length > 0 ? : null} {stats} diff --git a/public/components/trace_analytics/components/traces/span_detail_panel.tsx b/public/components/trace_analytics/components/traces/span_detail_panel.tsx index 13e8c2b0f..82e6dfc52 100644 --- a/public/components/trace_analytics/components/traces/span_detail_panel.tsx +++ b/public/components/trace_analytics/components/traces/span_detail_panel.tsx @@ -15,18 +15,16 @@ import { EuiSmallButton, EuiSpacer, } from '@elastic/eui'; -import debounce from 'lodash/debounce'; -import isEmpty from 'lodash/isEmpty'; import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import useObservable from 'react-use/lib/useObservable'; import { HttpSetup } from '../../../../../../../src/core/public'; import { TraceAnalyticsMode } from '../../../../../common/types/trace_analytics'; import { coreRefs } from '../../../../framework/core_refs'; import { Plt } from '../../../visualizations/plotly/plot'; -import { handleSpansGanttRequest } from '../../requests/traces_request_handler'; -import { PanelTitle } from '../common/helper_functions'; +import { PanelTitle, parseHits } from '../common/helper_functions'; import { SpanDetailFlyout } from './span_detail_flyout'; import { SpanDetailTable, SpanDetailTableHierarchy } from './span_detail_table'; +import { hitsToSpanDetailData } from '../../requests/traces_request_handler'; export function SpanDetailPanel(props: { http: HttpSetup; @@ -38,8 +36,11 @@ export function SpanDetailPanel(props: { page?: string; openSpanFlyout?: any; data?: { gantt: any[]; table: any[]; ganttMaxX: number }; - setData?: (data: { gantt: any[]; table: any[]; ganttMaxX: number }) => void; + setGanttData?: (data: { gantt: any[]; table: any[]; ganttMaxX: number }) => void; isApplicationFlyout?: boolean; + payloadData: string; + isGanttChartLoading?: boolean; + setGanttChartLoading?: (loading: boolean) => void; }) { const { chrome } = coreRefs; const { mode } = props; @@ -48,7 +49,6 @@ export function SpanDetailPanel(props: { const [spanFilters, setSpanFilters] = useState>( storedFilters ? JSON.parse(storedFilters) : [] ); - const [DSL, setDSL] = useState({}); let data: { gantt: any[]; table: any[]; ganttMaxX: number }; let setData: (data: { gantt: any[]; table: any[]; ganttMaxX: number }) => void; const [localData, localSetData] = useState<{ gantt: any[]; table: any[]; ganttMaxX: number }>({ @@ -56,8 +56,8 @@ export function SpanDetailPanel(props: { table: [], ganttMaxX: 0, }); - if (props.data && props.setData) { - [data, setData] = [props.data, props.setData]; + if (props.data && props.setGanttData) { + [data, setData] = [props.data, props.setGanttData]; } else { [data, setData] = [localData, localSetData]; } @@ -68,7 +68,6 @@ export function SpanDetailPanel(props: { const containerRef = useRef(null); const [availableWidth, setAvailableWidth] = useState(window.innerWidth); const newNavigation = coreRefs?.chrome?.navGroup.getNavGroupEnabled?.(); - const [isGanttChartLoading, setIsGanttChartLoading] = useState(false); const updateAvailableWidth = () => { if (containerRef.current) { @@ -135,72 +134,63 @@ export function SpanDetailPanel(props: { } }; - const refresh = debounce(() => { - if (isEmpty(props.colorMap)) return; - const refreshDSL = spanFiltersToDSL(); - setDSL(refreshDSL); - handleSpansGanttRequest( - props.traceId, - props.http, - setData, - props.colorMap, - refreshDSL, - mode, - props.dataSourceMDSId - ).finally(() => setIsGanttChartLoading(false)); - }, 150); - - const spanFiltersToDSL = () => { - const spanDSL: any = - mode === 'jaeger' - ? { - query: { - bool: { - must: [ - { - term: { - traceID: props.traceId, - }, - }, - ], - filter: [], - should: [], - must_not: [], - }, - }, - } - : { - query: { - bool: { - must: [ - { - term: { - traceId: props.traceId, - }, - }, - ], - filter: [], - should: [], - must_not: [], - }, - }, - }; - spanFilters.map(({ field, value }) => { - if (value != null) { - spanDSL.query.bool.filter.push({ - term: { - [field]: value, - }, + const parseAndFilterHits = ( + payloadData: string, + traceMode: string, + payloadSpanFilters: any[] + ) => { + try { + let hits = parseHits(props.payloadData); + + if (payloadSpanFilters.length > 0) { + hits = hits.filter((hit) => { + return payloadSpanFilters.every(({ field, value }) => { + const fieldValue = field.split('.').reduce((acc, part) => acc?.[part], hit._source); + return fieldValue === value; + }); }); } - }); - return spanDSL; + + hits = hits.filter((hit) => { + if (traceMode === 'jaeger') { + return Boolean(hit._source?.process?.serviceName); + } else { + return Boolean(hit._source?.serviceName); + } + }); + + return hits; + } catch (error) { + console.error('Error processing payloadData in parseAndFilterHits:', error); + return []; + } }; useEffect(() => { - setIsGanttChartLoading(true); - refresh(); - }, [props.colorMap, spanFilters]); + if (!props.payloadData) { + console.warn('No payloadData provided to SpanDetailPanel'); + return; + } + + const hits = parseAndFilterHits(props.payloadData, mode, spanFilters); + + if (hits.length === 0) { + return; + } + + hitsToSpanDetailData(hits, props.colorMap, mode) + .then((transformedData) => { + setData(transformedData); + }) + .catch((error) => { + console.error('Error in hitsToSpanDetailData:', error); + }) + .finally(() => { + if (props.setGanttChartLoading) { + props.setGanttChartLoading(false); + } + }); + }, [props.payloadData, props.colorMap, mode, spanFilters]); const getSpanDetailLayout = ( plotTraces: Plotly.Data[], @@ -386,7 +376,6 @@ export function SpanDetailPanel(props: { { if (fromApp) { @@ -397,10 +386,12 @@ export function SpanDetailPanel(props: { }} dataSourceMDSId={props.dataSourceMDSId} availableWidth={dynamicLayoutAdjustment} + payloadData={props.payloadData} + filters={spanFilters} />
), - [DSL, setCurrentSpan, dynamicLayoutAdjustment] + [setCurrentSpan, dynamicLayoutAdjustment, props.payloadData, spanFilters] ); const spanDetailTableHierarchy = useMemo( @@ -409,7 +400,6 @@ export function SpanDetailPanel(props: { { if (fromApp) { @@ -420,16 +410,26 @@ export function SpanDetailPanel(props: { }} dataSourceMDSId={props.dataSourceMDSId} availableWidth={dynamicLayoutAdjustment} + payloadData={props.payloadData} + filters={spanFilters} />
), - [DSL, setCurrentSpan, dynamicLayoutAdjustment] + [setCurrentSpan, dynamicLayoutAdjustment, props.payloadData, spanFilters] ); const ganttChart = useMemo( () => ( { + const hasError = trace.text && trace.text[0] && trace.text[0].includes('Error'); + + if (hasError) { + return { + ...trace, + }; + } + const duration = trace.x[0] ? trace.x[0].toFixed(2) : '0.00'; // Format duration to 2 decimal places return { @@ -474,7 +474,7 @@ export function SpanDetailPanel(props: { )} - {isGanttChartLoading ? ( + {props.isGanttChartLoading ? (
diff --git a/public/components/trace_analytics/components/traces/span_detail_table.tsx b/public/components/trace_analytics/components/traces/span_detail_table.tsx index 7a4e7ef8f..83c36b5ef 100644 --- a/public/components/trace_analytics/components/traces/span_detail_table.tsx +++ b/public/components/trace_analytics/components/traces/span_detail_table.tsx @@ -11,9 +11,9 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { HttpSetup } from '../../../../../../../src/core/public'; import { TRACE_ANALYTICS_DATE_FORMAT } from '../../../../../common/constants/trace_analytics'; import { TraceAnalyticsMode } from '../../../../../common/types/trace_analytics'; -import { handleSpansRequest } from '../../requests/traces_request_handler'; -import { microToMilliSec, nanoToMilliSec } from '../common/helper_functions'; +import { microToMilliSec, nanoToMilliSec, parseHits } from '../common/helper_functions'; import { RenderCustomDataGrid } from '../common/shared_components/custom_datagrid'; +import { handleSpansRequest } from '../../requests/traces_request_handler'; interface SpanDetailTableProps { http: HttpSetup; @@ -24,6 +24,8 @@ interface SpanDetailTableProps { setTotal?: (total: number) => void; dataSourceMDSId: string; availableWidth?: number; + payloadData: string; + filters: Array<{ field: string; value: any }>; } interface Span { @@ -173,6 +175,7 @@ export function SpanDetailTable(props: SpanDetailTableProps) { const [total, setTotal] = useState(0); const [isSpansTableDataLoading, setIsSpansTableDataLoading] = useState(false); + // For application_analytics const fetchData = async () => { setIsSpansTableDataLoading(true); const spanSearchParams: SpanSearchParams = { @@ -192,13 +195,82 @@ export function SpanDetailTable(props: SpanDetailTableProps) { ).finally(() => setIsSpansTableDataLoading(false)); }; + // For application_analytics useEffect(() => { - fetchData(); + if (!props.payloadData) { + fetchData(); + } }, [tableParams, props.DSL]); useEffect(() => { if (props.setTotal) props.setTotal(total); }, [total]); + + useEffect(() => { + if (!props.payloadData) { + return; + } + try { + const hitsArray = parseHits(props.payloadData); + + // Map each hit to its _source + let spans = hitsArray.map((hit: any) => hit._source); + + // Apply filters passed as a prop. + if (props.filters.length > 0) { + spans = spans.filter((span: any) => { + return props.filters.every(({ field, value }) => { + return span[field] === value; + }); + }); + } + + if (tableParams.sortingColumns.length > 0) { + spans = applySorting(spans); + } + + setItems(spans); + setTotal(spans.length); + } catch (error) { + console.error('Error parsing payloadData in SpanDetailTable:', error); + } finally { + setIsSpansTableDataLoading(false); + } + }, [props.payloadData, props.DSL, props.filters, tableParams]); + + const applySorting = (spans: Span[]) => { + return spans.sort((a, b) => { + for (const { id, direction } of tableParams.sortingColumns) { + let aValue = a[id]; + let bValue = b[id]; + + // Handle sorting for "Errors" column in Jaeger mode + if (id === 'tag' && props.mode === 'jaeger') { + const aHasError = a.tag?.error === true ? 1 : 0; + const bHasError = b.tag?.error === true ? 1 : 0; + aValue = aHasError; + bValue = bHasError; + } + + if (aValue < bValue) return direction === 'asc' ? -1 : 1; + if (aValue > bValue) return direction === 'asc' ? 1 : -1; + } + return 0; + }); + }; + + const onSort = (sortingColumns) => { + setTableParams((prev) => ({ ...prev, sortingColumns })); + }; + + const onChangePage = (page) => { + setTableParams((prev) => ({ ...prev, page })); + }; + + const onChangeItemsPerPage = (size) => { + setTableParams((prev) => ({ ...prev, size, page: 0 })); + }; + const columns = useMemo(() => getColumns(props.mode), [props.mode]); const renderCellValue = useCallback( ({ rowIndex, columnId, disableInteractions }) => @@ -213,18 +285,6 @@ export function SpanDetailTable(props: SpanDetailTableProps) { [items] ); - const onSort = (sortingColumns) => { - setTableParams((prev) => ({ ...prev, sortingColumns })); - }; - - const onChangePage = (page) => { - setTableParams((prev) => ({ ...prev, page })); - }; - - const onChangeItemsPerPage = (size) => { - setTableParams((prev) => ({ ...prev, size, page: 0 })); - }; - const visibleColumns = useMemo( () => getColumns(props.mode) @@ -237,7 +297,7 @@ export function SpanDetailTable(props: SpanDetailTableProps) { columns, renderCellValue, rowCount: total, - sorting: props.mode === 'jaeger' ? undefined : { columns: tableParams.sortingColumns, onSort }, + sorting: { columns: tableParams.sortingColumns, onSort }, pagination: { pageIndex: tableParams.page, pageSize: tableParams.size, @@ -253,50 +313,95 @@ export function SpanDetailTable(props: SpanDetailTableProps) { } export function SpanDetailTableHierarchy(props: SpanDetailTableProps) { - const { http, hiddenColumns, mode, DSL, dataSourceMDSId, availableWidth, openFlyout } = props; + const { hiddenColumns, mode, availableWidth, openFlyout } = props; const [items, setItems] = useState([]); const [_total, setTotal] = useState(0); const [expandedRows, setExpandedRows] = useState(new Set()); const [isSpansTableDataLoading, setIsSpansTableDataLoading] = useState(false); useEffect(() => { - setIsSpansTableDataLoading(true); - const spanSearchParams = { - from: 0, - size: 10000, - sortingColumns: [], - }; - handleSpansRequest( - http, - (data) => { - const hierarchy = buildHierarchy(data); - setItems(hierarchy); - }, - setTotal, - spanSearchParams, - DSL, - mode, - dataSourceMDSId - ).finally(() => setIsSpansTableDataLoading(false)); - }, [DSL, http, mode, dataSourceMDSId]); + if (!props.payloadData) return; + try { + const hitsArray = parseHits(props.payloadData); + + let spans = hitsArray.map((hit: any) => hit._source); + + if (props.filters.length > 0) { + spans = spans.filter((span: any) => { + return props.filters.every( + ({ field, value }: { field: string; value: any }) => span[field] === value + ); + }); + } + + const hierarchy = buildHierarchy(spans); + setItems(hierarchy); + setTotal(hierarchy.length); + } catch (error) { + console.error('Error parsing payloadData in SpanDetailTableHierarchy:', error); + } finally { + setIsSpansTableDataLoading(false); + } + }, [props.payloadData, props.DSL, props.mode, props.dataSourceMDSId, props.filters]); type SpanMap = Record; + interface SpanReference { + refType: 'CHILD_OF' | 'FOLLOWS_FROM'; + spanID: string; + } + + const addRootSpan = ( + spanId: string, + spanMap: SpanMap, + rootSpans: Span[], + alreadyAddedRootSpans: Set + ) => { + if (!alreadyAddedRootSpans.has(spanId)) { + rootSpans.push(spanMap[spanId]); + alreadyAddedRootSpans.add(spanId); + } + }; + const buildHierarchy = (spans: Span[]): Span[] => { const spanMap: SpanMap = {}; spans.forEach((span) => { - spanMap[span.spanId] = { ...span, children: [] }; + const spanIdKey = props.mode === 'jaeger' ? 'spanID' : 'spanId'; + spanMap[span[spanIdKey]] = { ...span, children: [] }; }); const rootSpans: Span[] = []; + const alreadyAddedRootSpans: Set = new Set(); // Track added root spans spans.forEach((span) => { - if (span.parentSpanId && spanMap[span.parentSpanId]) { - // If the parent span exists, add this span to its children array - spanMap[span.parentSpanId].children.push(spanMap[span.spanId]); + const spanIdKey = props.mode === 'jaeger' ? 'spanID' : 'spanId'; + const references: SpanReference[] = span.references || []; + + if (props.mode === 'jaeger') { + references.forEach((ref: SpanReference) => { + if (ref.refType === 'CHILD_OF') { + const parentSpan = spanMap[ref.spanID]; + if (parentSpan) { + parentSpan.children.push(spanMap[span[spanIdKey]]); + } + } + + if (ref.refType === 'FOLLOWS_FROM' && !alreadyAddedRootSpans.has(span[spanIdKey])) { + addRootSpan(span[spanIdKey], spanMap, rootSpans, alreadyAddedRootSpans); + } + }); + + if (references.length === 0 || references.every((ref) => ref.refType === 'FOLLOWS_FROM')) { + addRootSpan(span[spanIdKey], spanMap, rootSpans, alreadyAddedRootSpans); + } } else { - rootSpans.push(spanMap[span.spanId]); + // Data Prepper + if (span.parentSpanId && spanMap[span.parentSpanId]) { + spanMap[span.parentSpanId].children.push(spanMap[span[spanIdKey]]); + } else { + addRootSpan(span[spanIdKey], spanMap, rootSpans, alreadyAddedRootSpans); + } } }); @@ -305,15 +410,16 @@ export function SpanDetailTableHierarchy(props: SpanDetailTableProps) { const flattenHierarchy = (spans: Span[], level = 0, isParentExpanded = true): Span[] => { return spans.flatMap((span) => { - const isExpanded = expandedRows.has(span.spanId); + const isExpanded = expandedRows.has(span.spanId || span.spanID); const shouldShow = level === 0 || isParentExpanded; + const row = shouldShow ? [{ ...span, level }] : []; const children = flattenHierarchy(span.children || [], level + 1, isExpanded && shouldShow); return [...row, ...children]; }); }; - const flattenedItems = useMemo(() => flattenHierarchy(items), [items, expandedRows]); + const flattenedItems = useMemo(() => flattenHierarchy(items), [items, expandedRows, mode]); const columns = useMemo(() => getColumns(mode), [mode]); const visibleColumns = useMemo( @@ -325,7 +431,7 @@ export function SpanDetailTableHierarchy(props: SpanDetailTableProps) { const allSpanIds = new Set(); const gather = (spanList: Span[]) => { spanList.forEach((span) => { - allSpanIds.add(span.spanId); + allSpanIds.add(span.spanId || span.spanID); if (span.children.length > 0) { gather(span.children); } @@ -340,21 +446,23 @@ export function SpanDetailTableHierarchy(props: SpanDetailTableProps) { const item = flattenedItems[rowIndex]; const value = item[columnId]; - if (columnId === 'spanId') { + const spanIdKey = props.mode === 'jaeger' ? 'spanID' : 'spanId'; + + if (columnId === 'spanId' || columnId === 'spanID') { const indentation = `${(item.level || 0) * 20}px`; - const isExpanded = expandedRows.has(item.spanId); + const isExpanded = expandedRows.has(item[spanIdKey]); return (
- {item.children.length > 0 ? ( + {item.children && item.children.length > 0 ? ( { setExpandedRows((prev) => { const newSet = new Set(prev); - if (newSet.has(item.spanId)) { - newSet.delete(item.spanId); + if (newSet.has(item[spanIdKey])) { + newSet.delete(item[spanIdKey]); } else { - newSet.add(item.spanId); + newSet.add(item[spanIdKey]); } return newSet; }); @@ -369,7 +477,7 @@ export function SpanDetailTableHierarchy(props: SpanDetailTableProps) { {value} ) : ( openFlyout(value)} + onClick={() => openFlyout(item[spanIdKey])} color="primary" data-test-subj="spanId-flyout-button" > @@ -389,7 +497,7 @@ export function SpanDetailTableHierarchy(props: SpanDetailTableProps) { props, }); }, - [flattenedItems, expandedRows, openFlyout] + [flattenedItems, expandedRows, openFlyout, props.mode] ); const toolbarButtons = [ diff --git a/public/components/trace_analytics/components/traces/trace_view.tsx b/public/components/trace_analytics/components/traces/trace_view.tsx index a7d52eb01..62f28f48a 100644 --- a/public/components/trace_analytics/components/traces/trace_view.tsx +++ b/public/components/trace_analytics/components/traces/trace_view.tsx @@ -10,6 +10,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, + EuiIconTip, EuiLoadingContent, EuiPage, EuiPageBody, @@ -20,6 +21,7 @@ import { } from '@elastic/eui'; import round from 'lodash/round'; import React, { useEffect, useState } from 'react'; +import { i18n } from '@osd/i18n'; import { MountPoint } from '../../../../../../../src/core/public'; import { DataSourceManagementPluginSetup } from '../../../../../../../src/plugins/data_source_management/public'; import { DataSourceOption } from '../../../../../../../src/plugins/data_source_management/public/components/data_source_menu/types'; @@ -28,15 +30,12 @@ import { setNavBreadCrumbs } from '../../../../../common/utils/set_nav_bread_cru import { coreRefs } from '../../../../framework/core_refs'; import { TraceAnalyticsCoreDeps } from '../../home'; import { handleServiceMapRequest } from '../../requests/services_request_handler'; -import { - handlePayloadRequest, - handleServicesPieChartRequest, - handleTraceViewRequest, -} from '../../requests/traces_request_handler'; +import { handlePayloadRequest } from '../../requests/traces_request_handler'; import { PanelTitle, filtersToDsl, processTimeStamp } from '../common/helper_functions'; import { ServiceMap, ServiceObject } from '../common/plots/service_map'; import { ServiceBreakdownPanel } from './service_breakdown_panel'; import { SpanDetailPanel } from './span_detail_panel'; +import { getOverviewFields, getServiceBreakdownData } from './trace_view_helpers'; const newNavigation = coreRefs.chrome?.navGroup.getNavGroupEnabled(); @@ -84,6 +83,7 @@ export function TraceView(props: TraceViewProps) { const [isTraceOverViewLoading, setIsTraceOverViewLoading] = useState(false); const [isTracePayloadLoading, setTracePayloadLoading] = useState(false); const [isServicesPieChartLoading, setIsServicesPieChartLoading] = useState(false); + const [isGanttChartLoading, setIsGanttChartLoading] = useState(false); const renderOverview = (overviewFields: any) => { return ( @@ -142,6 +142,20 @@ export function TraceView(props: TraceViewProps) { Latency {overviewFields.latency} + {overviewFields.fallbackValueUsed && ( + + )} @@ -159,7 +173,7 @@ export function TraceView(props: TraceViewProps) { {overviewFields.error_count == null ? ( '-' - ) : fields.error_count > 0 ? ( + ) : overviewFields.error_count > 0 ? ( Yes @@ -187,17 +201,10 @@ export function TraceView(props: TraceViewProps) { page ); - setIsTraceOverViewLoading(true); - handleTraceViewRequest( - props.traceId, - props.http, - fields, - setFields, - mode, - props.dataSourceMDSId[0].id - ).finally(() => setIsTraceOverViewLoading(false)); - setTracePayloadLoading(true); + setIsTraceOverViewLoading(true); + setIsServicesPieChartLoading(true); + setIsGanttChartLoading(true); handlePayloadRequest( props.traceId, props.http, @@ -207,16 +214,6 @@ export function TraceView(props: TraceViewProps) { props.dataSourceMDSId[0].id ).finally(() => setTracePayloadLoading(false)); - setIsServicesPieChartLoading(true); - handleServicesPieChartRequest( - props.traceId, - props.http, - setServiceBreakdownData, - setColorMap, - mode, - props.dataSourceMDSId[0].id - ).finally(() => setIsServicesPieChartLoading(false)); - setIsServicesDataLoading(true); handleServiceMapRequest( props.http, @@ -227,6 +224,30 @@ export function TraceView(props: TraceViewProps) { ).finally(() => setIsServicesDataLoading(false)); }; + useEffect(() => { + if (!payloadData) return; + + try { + const parsedPayload = JSON.parse(payloadData); + const overview = getOverviewFields(parsedPayload, mode); + if (overview) { + setFields(overview); + } + + const { + serviceBreakdownData: queryServiceBreakdownData, + colorMap: queryColorMap, + } = getServiceBreakdownData(parsedPayload, mode); + setServiceBreakdownData(queryServiceBreakdownData); + setColorMap(queryColorMap); + } catch (error) { + console.error('Error processing payloadData:', error); + } finally { + setIsTraceOverViewLoading(false); + setIsServicesPieChartLoading(false); + } + }, [payloadData, mode]); + useEffect(() => { if (!Object.keys(serviceMap).length || !ganttData.table.length) return; const services: any = {}; @@ -306,9 +327,12 @@ export function TraceView(props: TraceViewProps) { colorMap={colorMap} mode={mode} data={ganttData} - setData={setGanttData} + setGanttData={setGanttData} dataSourceMDSId={props.dataSourceMDSId[0].id} dataSourceMDSLabel={props.dataSourceMDSId[0].label} + payloadData={payloadData} + isGanttChartLoading={isGanttChartLoading} + setGanttChartLoading={setIsGanttChartLoading} /> diff --git a/public/components/trace_analytics/components/traces/trace_view_helpers.tsx b/public/components/trace_analytics/components/traces/trace_view_helpers.tsx new file mode 100644 index 000000000..32b5b12f3 --- /dev/null +++ b/public/components/trace_analytics/components/traces/trace_view_helpers.tsx @@ -0,0 +1,123 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +import moment from 'moment'; +import { MILI_TO_SEC, NANOS_TO_MS, pieChartColors } from '../common/constants'; + +export function getOverviewFields(parsed: any, mode: string) { + if (parsed.length === 0) return null; + const firstSpan = parsed[parsed.length - 1]._source; + if (!firstSpan) return null; + + let fallbackValueUsed = false; + + if (mode === 'jaeger') { + const lastUpdated = + Number(firstSpan.startTime) / MILI_TO_SEC + Number(firstSpan.duration) / MILI_TO_SEC; + + let errorCount = 0; + parsed.some((span: any) => { + if (span._source.tag?.['error'] === true) { + errorCount++; + return true; + } + return false; + }); + + return { + trace_id: firstSpan.traceID || '-', + trace_group: firstSpan.operationName || '-', + last_updated: moment(lastUpdated).format('MM/DD/YYYY HH:mm:ss.SSS'), + latency: firstSpan.duration + ? `${(Number(firstSpan.duration) / MILI_TO_SEC).toFixed(2)} ms` + : 'N/A', + error_count: errorCount, + fallbackValueUsed: false, + }; + } else { + let computedLatency: number | null = null; + const traceGroupFields = firstSpan.traceGroupFields; + if (traceGroupFields && traceGroupFields.durationInNanos != null) { + computedLatency = Number(traceGroupFields.durationInNanos) / NANOS_TO_MS; + } else if (firstSpan.traceGroupFields?.durationInNanos != null) { + computedLatency = Number(firstSpan.traceGroupFields?.durationInNanos) / NANOS_TO_MS; + } else { + const startTimes = parsed.map((span: any) => moment(span._source.startTime)); + const endTimes = parsed.map((span: any) => moment(span._source.endTime)); + + const minStartTime = moment.min(startTimes); + const maxEndTime = moment.max(endTimes); + + if (minStartTime && maxEndTime) { + computedLatency = maxEndTime.diff(minStartTime, 'milliseconds', true); + fallbackValueUsed = true; + } + } + + const computedLastUpdated = traceGroupFields?.endTime || firstSpan.endTime || null; + const tgStatus = traceGroupFields?.statusCode; + const errorCount = tgStatus != null ? (Number(tgStatus) === 2 ? 1 : 0) : 0; + + return { + trace_id: firstSpan.traceId || '-', + trace_group: firstSpan.traceGroup || '-', + last_updated: computedLastUpdated + ? moment(computedLastUpdated).format('MM/DD/YYYY HH:mm:ss.SSS') + : 'N/A', + latency: computedLatency != null ? `${computedLatency.toFixed(2)} ms` : 'N/A', + error_count: errorCount, + fallbackValueUsed, + }; + } +} + +export function getServiceBreakdownData(parsed: any, mode: string) { + const serviceLatencyMap = new Map(); + let totalLatency = 0; + + parsed.forEach((hit) => { + const source = hit._source; + const serviceName = mode === 'jaeger' ? source.process?.serviceName : source.serviceName; + let latency = 0; + if (mode === 'jaeger') { + latency = source.duration ? Number(source.duration) / 1000 : 0; + } else { + if (source.durationInNanos) { + latency = Number(source.durationInNanos) / NANOS_TO_MS; + } else if (source.traceGroupFields?.durationInNanos) { + latency = Number(source.traceGroupFields?.durationInNanos) / NANOS_TO_MS; + } + } + + if (serviceName) { + const current = serviceLatencyMap.get(serviceName) || 0; + serviceLatencyMap.set(serviceName, current + latency); + totalLatency += latency; + } + }); + + const serviceBreakdownArray = [...serviceLatencyMap.entries()].sort((a, b) => b[1] - a[1]); + + const serviceBreakdownData = [ + { + labels: serviceBreakdownArray.map(([name]) => name), + values: serviceBreakdownArray.map(([, value]) => + totalLatency === 0 ? 100 : (value / totalLatency) * 100 + ), + type: 'pie', + textinfo: 'none', + marker: { colors: pieChartColors.slice(0, serviceBreakdownArray.length) }, + }, + ]; + + const colorMap = Object.fromEntries( + serviceBreakdownArray.map(([name], index) => [ + name, + pieChartColors[index % pieChartColors.length], + ]) + ); + + return { serviceBreakdownData, colorMap }; +} diff --git a/public/components/trace_analytics/requests/queries/traces_queries.ts b/public/components/trace_analytics/requests/queries/traces_queries.ts index 84571a9f2..503378c1b 100644 --- a/public/components/trace_analytics/requests/queries/traces_queries.ts +++ b/public/components/trace_analytics/requests/queries/traces_queries.ts @@ -194,186 +194,6 @@ export const getTracesQuery = ( return mode === 'jaeger' ? jaegerQuery : dataPrepperQuery; }; -export const getServiceBreakdownQuery = (traceId: string, mode: TraceAnalyticsMode) => { - const jaegerQuery = { - size: 0, - query: { - bool: { - must: [ - { - term: { - traceID: traceId, - }, - }, - ], - filter: [], - should: [], - must_not: [], - }, - }, - aggs: { - service_type: { - terms: { - field: 'process.serviceName', - order: [ - { - total_latency_nanos: 'desc', - }, - ], - }, - aggs: { - total_latency_nanos: { - sum: { - field: 'duration', - }, - }, - total_latency: { - bucket_script: { - buckets_path: { - count: '_count', - latency: 'total_latency_nanos.value', - }, - script: 'Math.round(params.latency / 10) / 100.0', - }, - }, - }, - }, - }, - }; - const dataPrepperQuery = { - size: 0, - query: { - bool: { - must: [ - { - term: { - traceId, - }, - }, - ], - filter: [], - should: [], - must_not: [], - }, - }, - aggs: { - service_type: { - terms: { - field: 'serviceName', - order: [ - { - total_latency_nanos: 'desc', - }, - ], - }, - aggs: { - total_latency_nanos: { - sum: { - field: 'durationInNanos', - }, - }, - total_latency: { - bucket_script: { - buckets_path: { - count: '_count', - latency: 'total_latency_nanos.value', - }, - script: 'Math.round(params.latency / 10000) / 100.0', - }, - }, - }, - }, - }, - }; - return mode === 'jaeger' ? jaegerQuery : dataPrepperQuery; -}; - -export const getSpanDetailQuery = (mode: TraceAnalyticsMode, traceId: string, size = 3000) => { - if (mode === 'jaeger') { - return { - size, - query: { - bool: { - must: [ - { - term: { - traceID: traceId, - }, - }, - { - exists: { - field: 'process.serviceName', - }, - }, - ], - filter: [], - should: [], - must_not: [], - }, - }, - sort: [ - { - startTime: { - order: 'desc', - }, - }, - ], - _source: { - includes: [ - 'process.serviceName', - 'operationName', - 'startTime', - 'endTime', - 'spanID', - 'tag', - 'duration', - 'references', - ], - }, - }; - } - return { - size, - query: { - bool: { - must: [ - { - term: { - traceId, - }, - }, - { - exists: { - field: 'serviceName', - }, - }, - ], - filter: [], - should: [], - must_not: [], - }, - }, - sort: [ - { - startTime: { - order: 'desc', - }, - }, - ], - _source: { - includes: [ - 'serviceName', - 'name', - 'startTime', - 'endTime', - 'spanId', - 'status.code', - 'durationInNanos', - ], - }, - }; -}; - export const getPayloadQuery = (mode: TraceAnalyticsMode, traceId: string, size = 1000) => { if (mode === 'jaeger') { return { diff --git a/public/components/trace_analytics/requests/traces_request_handler.ts b/public/components/trace_analytics/requests/traces_request_handler.ts index b0a0ab8f4..27575fe1d 100644 --- a/public/components/trace_analytics/requests/traces_request_handler.ts +++ b/public/components/trace_analytics/requests/traces_request_handler.ts @@ -19,19 +19,19 @@ import { getTimestampPrecision, microToMilliSec, nanoToMilliSec, + parseIsoToNano, } from '../components/common/helper_functions'; import { SpanSearchParams } from '../components/traces/span_detail_table'; import { getCustomIndicesTracesQuery, getPayloadQuery, - getServiceBreakdownQuery, - getSpanDetailQuery, getSpanFlyoutQuery, getSpansQuery, getTraceGroupPercentilesQuery, getTracesQuery, } from './queries/traces_queries'; import { handleDslRequest } from './request_handler'; +import { MILI_TO_SEC } from '../components/common/constants'; export const handleCustomIndicesTracesRequest = async ( http: HttpSetup, @@ -204,152 +204,6 @@ export const handleTracesRequest = async ( }); }; -export const handleTraceViewRequest = ( - traceId: string, - http: HttpSetup, - fields: {}, - setFields: (fields: any) => void, - mode: TraceAnalyticsMode, - dataSourceMDSId?: string -) => { - return handleDslRequest(http, null, getTracesQuery(mode, traceId), mode, dataSourceMDSId) - .then(async (response) => { - // Check if the mode hasn't been set first - if (mode === 'jaeger' && !response?.aggregations?.service_type?.buckets) { - console.warn('No traces or aggregations found.'); - return []; - } - const bucket = response.aggregations.traces.buckets[0]; - return { - trace_id: bucket.key, - trace_group: bucket.trace_group.buckets[0]?.key, - last_updated: moment(bucket.last_updated.value).format(TRACE_ANALYTICS_DATE_FORMAT), - user_id: 'N/A', - latency: bucket.latency.value, - latency_vs_benchmark: 'N/A', - percentile_in_trace_group: 'N/A', - error_count: bucket.error_count.doc_count, - errors_vs_benchmark: 'N/A', - }; - }) - .then((newFields) => { - setFields(newFields); - }) - .catch((error) => { - console.error('Error in handleTraceViewRequest:', error); - coreRefs.core?.notifications.toasts.addError(error, { - title: `Failed to retrieve trace view for trace ID: ${traceId}`, - toastLifeTimeMs: 10000, - }); - }); -}; - -// setColorMap sets serviceName to color mappings -export const handleServicesPieChartRequest = async ( - traceId: string, - http: HttpSetup, - setServiceBreakdownData: (serviceBreakdownData: any) => void, - setColorMap: (colorMap: any) => void, - mode: TraceAnalyticsMode, - dataSourceMDSId?: string -) => { - const colors = [ - '#7492e7', - '#c33d69', - '#2ea597', - '#8456ce', - '#e07941', - '#3759ce', - '#ce567c', - '#9469d6', - '#4066df', - '#da7596', - '#a783e1', - '#5978e3', - ]; - const colorMap: any = {}; - let index = 0; - await handleDslRequest(http, null, getServiceBreakdownQuery(traceId, mode), mode, dataSourceMDSId) - .then((response) => { - // Check if the mode hasn't been set first - if (mode === 'jaeger' && !response?.aggregations?.service_type?.buckets) { - console.warn(`No service breakdown found for trace ID: ${traceId}`); - return []; - } - - return Promise.all( - response.aggregations.service_type.buckets.map((bucket: any) => { - colorMap[bucket.key] = colors[index++ % colors.length]; - return { - name: bucket.key, - color: colorMap[bucket.key], - value: bucket.total_latency.value, - benchmark: 0, - }; - }) - ); - }) - .then((newItems) => { - if (!newItems.length) return; // No data to process - const latencySum = newItems.map((item) => item.value).reduce((a, b) => a + b, 0); - return [ - { - values: newItems.map((item) => - latencySum === 0 ? 100 : (item.value / latencySum) * 100 - ), - labels: newItems.map((item) => item.name), - benchmarks: newItems.map((item) => item.benchmark), - marker: { - colors: newItems.map((item) => item.color), - }, - type: 'pie', - textinfo: 'none', - hovertemplate: '%{label}
%{value:.2f}%', - }, - ]; - }) - .then((newItems) => { - if (newItems) { - setServiceBreakdownData(newItems); - setColorMap(colorMap); - } - }) - .catch((error) => { - console.error('Error in handleServicesPieChartRequest:', error); - coreRefs.core?.notifications.toasts.addError(error, { - title: `Failed to retrieve service breakdown for trace ID: ${traceId}`, - toastLifeTimeMs: 10000, - }); - }); -}; - -export const handleSpansGanttRequest = ( - traceId: string, - http: HttpSetup, - setSpanDetailData: (spanDetailData: any) => void, - colorMap: any, - spanFiltersDSL: any, - mode: TraceAnalyticsMode, - dataSourceMDSId?: string -) => { - return handleDslRequest( - http, - spanFiltersDSL, - getSpanDetailQuery(mode, traceId), - mode, - dataSourceMDSId - ) - .then((response) => hitsToSpanDetailData(response.hits.hits, colorMap, mode)) - .then((newItems) => setSpanDetailData(newItems)) - .catch((error) => { - console.error('Error in handleSpansGanttRequest:', error); - coreRefs.core?.notifications.toasts.addError(error, { - title: `Failed to retrieve spans Gantt chart for trace ID: ${traceId}`, - toastLifeTimeMs: 10000, - }); - }); -}; - export const handleSpansFlyoutRequest = ( http: HttpSetup, spanId: string, @@ -370,7 +224,7 @@ export const handleSpansFlyoutRequest = ( }); }; -const hitsToSpanDetailData = async (hits: any, colorMap: any, mode: TraceAnalyticsMode) => { +export const hitsToSpanDetailData = async (hits: any, colorMap: any, mode: TraceAnalyticsMode) => { const data: { gantt: any[]; table: any[]; ganttMaxX: number } = { gantt: [], table: [], @@ -472,6 +326,28 @@ const hitsToSpanDetailData = async (hits: any, colorMap: any, mode: TraceAnalyti return data; }; +interface Hit { + _index: string; + _id: string; + _score: number; + _source: any; + sort?: any[]; +} + +interface ParsedResponse { + hits?: { + hits: Hit[]; + }; + [key: string]: any; +} + +export function normalizePayload(parsed: ParsedResponse): Hit[] { + if (parsed.hits && Array.isArray(parsed.hits.hits)) { + return parsed.hits.hits; + } + return []; +} + export const handlePayloadRequest = ( traceId: string, http: HttpSetup, @@ -481,7 +357,24 @@ export const handlePayloadRequest = ( dataSourceMDSId?: string ) => { return handleDslRequest(http, null, getPayloadQuery(mode, traceId), mode, dataSourceMDSId) - .then((response) => setPayloadData(JSON.stringify(response.hits.hits, null, 2))) + .then((response) => { + const normalizedData = normalizePayload(response); + const sortedData = normalizedData + .map((hit) => { + const time = + mode === 'jaeger' + ? Number(hit._source.startTime) * MILI_TO_SEC + : parseIsoToNano(hit._source.startTime); + + return { + ...hit, + sort: hit.sort && hit.sort[0] ? hit.sort : [time], + }; + }) + .sort((a, b) => b.sort[0] - a.sort[0]); // Sort in descending order by the sort field + + setPayloadData(JSON.stringify(sortedData, null, 2)); + }) .catch((error) => { console.error('Error in handlePayloadRequest:', error); coreRefs.core?.notifications.toasts.addError(error, {