diff --git a/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.test.js b/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.test.js index 2b3942d8b9..3994a031b9 100644 --- a/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.test.js +++ b/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.test.js @@ -22,10 +22,18 @@ describe('transform traces to ddg paths', () => { })), attributes: [{ key: 'exemplar_trace_id', value: trace.data.traceID }], }); - const makeSpan = (spanName, parent, kind) => ({ + + const addExemplarTraceIDs = (path, traces) => { + traces.forEach(trace => { + path.attributes.push({ key: 'exemplar_trace_id', value: trace.data.traceID }); + }); + return path; + }; + + const makeSpan = (spanName, parent, kind, operationName, processID) => ({ hasChildren: true, - operationName: `${spanName} operation`, - processID: `${spanName} processID`, + operationName: operationName || `${spanName} operation`, + processID: processID || `${spanName} processID`, references: parent ? [ { @@ -52,7 +60,7 @@ describe('transform traces to ddg paths', () => { (result, span) => ({ ...result, [span.processID]: { - serviceName: `${span.spanID.split(' ')[0]} service`, + serviceName: `${span.processID}-name`, }, }), {} @@ -137,11 +145,6 @@ describe('transform traces to ddg paths', () => { ); }); - it('errors if span has ancestor id not in trace data', () => { - const traces = makeTraces(makeTrace([rootSpan, followsFocalSpan], missTraceID)); - expect(() => transformTracesToPaths(traces, focalSvc)).toThrowError(/Ancestor spanID.*not found/); - }); - it('skips trace without data', () => { const traces = { ...makeTraces(shortTrace), @@ -152,23 +155,63 @@ describe('transform traces to ddg paths', () => { expect(result.length).toBe(1); }); - it("omits span if tags does not have span.kind === 'server'", () => { - const badSpanName = 'test bad span name'; - - const clientSpan = makeSpan(badSpanName, focalSpan, 'client'); - clientSpan.hasChildren = false; - const clientTrace = makeTrace([rootSpan, focalSpan, clientSpan], 'clientTraceID'); - - const kindlessSpan = makeSpan(badSpanName, focalSpan, false); - kindlessSpan.hasChildren = false; - const kindlessTrace = makeTrace([rootSpan, focalSpan, kindlessSpan], 'kindlessTraceID'); - - const { dependencies: result } = transformTracesToPaths(makeTraces(clientTrace, kindlessTrace), focalSvc); + it("omits span if tags does not have span.kind === 'server' and is followed by the same service", () => { + const spanServiceAServer = makeSpan('SpanA1', focalSpan, 'server', 'opA', 'serviceA'); + const otherSpanServiceAServer = makeSpan('SpanA2', spanServiceAServer, 'server', 'opB', 'serviceA'); + otherSpanServiceAServer.hasChildren = false; + const spanServiceAClient = makeSpan('SpanA3', spanServiceAServer, 'client', 'opA', 'serviceA'); + spanServiceAClient.hasChildren = false; + const spanServiceAKindless = makeSpan('SpanA4', spanServiceAServer, false, 'opA', 'serviceA'); + spanServiceAKindless.hasChildren = false; + + const spanServiceBClient = makeSpan('SpanB1', focalSpan, 'client', 'opA', 'serviceB'); + const spanServiceBServer = makeSpan('SpanB2', spanServiceBClient, 'server', 'opB', 'serviceB'); + spanServiceBServer.hasChildren = false; + + const serverClientTrace = makeTrace( + [rootSpan, focalSpan, spanServiceAServer, spanServiceAClient], + 'serverClientTraceID' + ); + const clientServerTrace = makeTrace( + [rootSpan, focalSpan, spanServiceBClient, spanServiceBServer], + 'clientServerTraceID' + ); + const kindlessTrace = makeTrace( + [rootSpan, focalSpan, spanServiceAServer, spanServiceAKindless], + 'kindlessTraceID' + ); + const twoServersTrace = makeTrace( + [rootSpan, focalSpan, spanServiceAServer, otherSpanServiceAServer], + 'twoServersTraceID' + ); + const { dependencies: result } = transformTracesToPaths( + makeTraces(serverClientTrace, kindlessTrace, twoServersTrace, clientServerTrace), + focalSvc + ); - const path = makeExpectedPath([rootSpan, focalSpan], clientTrace); - path.attributes.push({ key: 'exemplar_trace_id', value: kindlessTrace.data.traceID }); + expect(new Set(result)).toEqual( + new Set([ + addExemplarTraceIDs(makeExpectedPath([rootSpan, focalSpan, spanServiceAServer], serverClientTrace), [ + kindlessTrace, + ]), + makeExpectedPath([rootSpan, focalSpan, spanServiceAServer, otherSpanServiceAServer], twoServersTrace), + makeExpectedPath([rootSpan, focalSpan, spanServiceBClient, spanServiceBServer], clientServerTrace), + ]) + ); + }); - expect(new Set(result)).toEqual(new Set([path])); + it('support client span root', () => { + const spanServiceAClient = makeSpan('SpanA2', undefined, 'client', 'opA', 'serviceA'); + const spanServiceAServer = makeSpan('SpanA1', spanServiceAClient, 'server', 'opA', 'serviceA'); + const clientServerTrace = makeTrace([spanServiceAClient, spanServiceAServer], 'clientServerTraceID'); + spanServiceAServer.hasChildren = false; + const { dependencies: result } = transformTracesToPaths( + makeTraces(clientServerTrace), + clientServerTrace.data.processes[spanServiceAClient.processID].serviceName + ); + expect(new Set(result)).toEqual( + new Set([makeExpectedPath([spanServiceAClient, spanServiceAServer], clientServerTrace)]) + ); }); it('dedupled paths', () => { @@ -177,8 +220,9 @@ describe('transform traces to ddg paths', () => { const trace1 = makeTrace([rootSpan, focalSpan, otherSpan], 'trace1'); const trace2 = makeTrace([rootSpan, focalSpan, otherSpan], 'trace2'); const { dependencies: result } = transformTracesToPaths(makeTraces(trace1, trace2), focalSvc); - const path = makeExpectedPath([rootSpan, focalSpan, otherSpan], trace1); - path.attributes.push({ key: 'exemplar_trace_id', value: trace2.data.traceID }); - expect(new Set(result)).toEqual(new Set([path])); + + expect(new Set(result)).toEqual( + new Set([addExemplarTraceIDs(makeExpectedPath([rootSpan, focalSpan, otherSpan], trace1), [trace2])]) + ); }); }); diff --git a/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.tsx b/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.tsx index 4f381d0b9e..a1a2a9bddc 100644 --- a/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.tsx +++ b/packages/jaeger-ui/src/model/ddg/transformTracesToPaths.tsx @@ -13,13 +13,15 @@ // limitations under the License. import memoizeOne from 'memoize-one'; - -import spanAncestorIds from '../../utils/span-ancestor-ids'; +import { getTraceSpanIdsAsTree, TREE_ROOT_ID } from '../../selectors/trace'; import { TDdgPayloadEntry, TDdgPayloadPath, TDdgPayload } from './types'; import { FetchedTrace } from '../../types'; import { Span } from '../../types/trace'; +const isKindServer = (span: Span) => + span.tags.find(({ key, value }) => key === 'span.kind' && value === 'server'); + function transformTracesToPaths( traces: Record, focalService: string, @@ -30,53 +32,55 @@ function transformTracesToPaths( if (data) { const spanMap: Map = new Map(); const { traceID } = data; - data.spans - .filter(span => { - spanMap.set(span.spanID, span); - return !span.hasChildren; - }) - .forEach(leaf => { - const spans = spanAncestorIds(leaf).map(id => { - const span = spanMap.get(id); - if (!span) throw new Error(`Ancestor spanID ${id} not found in trace ${traceID}`); - return span; - }); - spans.reverse(); - spans.push(leaf); - - const path: TDdgPayloadEntry[] = spans - .filter(span => span.tags.find(({ key, value }) => key === 'span.kind' && value === 'server')) - .map(({ processID, operationName: operation }) => ({ - service: data.processes[processID].serviceName, - operation, - })); - - if ( - path.some( - ({ service, operation }) => - service === focalService && (!focalOperation || operation === focalOperation) - ) + data.spans.forEach(span => spanMap.set(span.spanID, span)); + const tree = getTraceSpanIdsAsTree(data); + tree.paths((pathIds: string[]) => { + const paths = pathIds.reduce((reducedSpans: Span[], id: string): Span[] => { + if (id === TREE_ROOT_ID) { + return reducedSpans; + } + const span = spanMap.get(id); + if (!span) throw new Error(`Ancestor spanID ${id} not found in trace ${traceID}`); + if (reducedSpans.length === 0) { + reducedSpans.push(span); + } else if ( + reducedSpans[reducedSpans.length - 1].processID !== span.processID || + isKindServer(span) ) { - const pathKey = path.map(value => `${value.operation}:${value.service}`).join('/'); - const dependency = dependenciesMap.get(pathKey); - if (!dependency) { - dependenciesMap.set(pathKey, { - path, - attributes: [ - { - key: 'exemplar_trace_id', - value: traceID, - }, - ], - }); - } else { - dependency.attributes.push({ - key: 'exemplar_trace_id', - value: traceID, - }); - } + reducedSpans.push(span); + } + return reducedSpans; + }, []); + const path: TDdgPayloadEntry[] = paths.map(({ processID, operationName: operation }) => ({ + service: data.processes[processID].serviceName, + operation, + })); + if ( + path.some( + ({ service, operation }) => + service === focalService && (!focalOperation || operation === focalOperation) + ) + ) { + const pathKey = path.map(value => `${value.operation}:${value.service}`).join('/'); + const dependency = dependenciesMap.get(pathKey); + if (!dependency) { + dependenciesMap.set(pathKey, { + path, + attributes: [ + { + key: 'exemplar_trace_id', + value: traceID, + }, + ], + }); + } else { + dependency.attributes.push({ + key: 'exemplar_trace_id', + value: traceID, + }); } - }); + } + }); } }); const dependencies = Array.from(dependenciesMap.values()); diff --git a/packages/jaeger-ui/src/utils/TreeNode.js b/packages/jaeger-ui/src/utils/TreeNode.js index b3ef63c8cc..20a2dee793 100644 --- a/packages/jaeger-ui/src/utils/TreeNode.js +++ b/packages/jaeger-ui/src/utils/TreeNode.js @@ -97,4 +97,24 @@ export default class TreeNode { } } } + + paths(fn) { + const stack = []; + stack.push({ node: this, childIndex: 0 }); + const paths = []; + while (stack.length) { + const { node, childIndex } = stack[stack.length - 1]; + if (node.children.length >= childIndex + 1) { + stack[stack.length - 1].childIndex++; + stack.push({ node: node.children[childIndex], childIndex: 0 }); + } else { + if (node.children.length === 0) { + const path = stack.map(item => item.node.value); + fn(path); + } + stack.pop(); + } + } + return paths; + } }