diff --git a/packages/jaeger-ui-components/src/model/find-trace-name.test.js b/packages/jaeger-ui-components/src/model/find-trace-name.test.js new file mode 100644 index 00000000000..39c9653a528 --- /dev/null +++ b/packages/jaeger-ui-components/src/model/find-trace-name.test.js @@ -0,0 +1,244 @@ +// Copyright (c) 2020 The Jaeger Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { _getTraceNameImpl as getTraceName } from './trace-viewer'; + +describe('getTraceName', () => { + const firstSpanId = 'firstSpanId'; + const secondSpanId = 'secondSpanId'; + const thirdSpanId = 'thirdSpanId'; + const missingSpanId = 'missingSpanId'; + + const currentTraceId = 'currentTraceId'; + + const serviceName = 'serviceName'; + const operationName = 'operationName'; + + const t = 1583758670000; + + // Note: this trace has a loop S1 <- S2 <- S3 <- S1, which is the only way + // to make the algorithm return an empty string as trace name. + const spansWithNoRoots = [ + { + spanID: firstSpanId, + traceID: currentTraceId, + startTime: t + 200, + process: {}, + references: [ + { + spanID: secondSpanId, + traceID: currentTraceId, + }, + ], + }, + { + spanID: secondSpanId, + traceID: currentTraceId, + startTime: t + 100, + process: {}, + references: [ + { + spanID: thirdSpanId, + traceID: currentTraceId, + }, + ], + }, + { + spanID: thirdSpanId, + traceID: currentTraceId, + startTime: t, + process: {}, + references: [ + { + spanID: firstSpanId, + traceID: currentTraceId, + }, + ], + }, + ]; + const spansWithMultipleRootsDifferentByStartTime = [ + { + spanID: firstSpanId, + traceID: currentTraceId, + startTime: t + 200, + process: {}, + references: [ + { + spanID: thirdSpanId, + traceID: currentTraceId, + }, + ], + }, + { + spanID: secondSpanId, // may be a root span + traceID: currentTraceId, + startTime: t + 100, + process: {}, + references: [ + { + spanID: missingSpanId, + traceID: currentTraceId, + }, + ], + }, + { + spanID: thirdSpanId, // root span (as the earliest) + traceID: currentTraceId, + startTime: t, + operationName, + process: { + serviceName, + }, + references: [ + { + spanID: missingSpanId, + traceID: currentTraceId, + }, + ], + }, + ]; + const spansWithMultipleRootsWithOneWithoutRefs = [ + { + spanID: firstSpanId, + traceID: currentTraceId, + startTime: t + 200, + process: {}, + references: [ + { + spanID: thirdSpanId, + traceID: currentTraceId, + }, + ], + }, + { + spanID: secondSpanId, // root span (as a span without any refs) + traceID: currentTraceId, + startTime: t + 100, + operationName, + process: { + serviceName, + }, + }, + { + spanID: thirdSpanId, // may be a root span + traceID: currentTraceId, + startTime: t, + process: {}, + references: [ + { + spanID: missingSpanId, + traceID: currentTraceId, + }, + ], + }, + ]; + const spansWithOneRootWithRemoteRef = [ + { + spanID: firstSpanId, + traceID: currentTraceId, + startTime: t + 200, + process: {}, + references: [ + { + spanID: secondSpanId, + traceID: currentTraceId, + }, + ], + }, + { + spanID: secondSpanId, + traceID: currentTraceId, + startTime: t + 100, + process: {}, + references: [ + { + spanID: thirdSpanId, + traceID: currentTraceId, + }, + ], + }, + { + spanID: thirdSpanId, // effective root span, since its parent is missing + traceID: currentTraceId, + startTime: t, + operationName, + process: { + serviceName, + }, + references: [ + { + spanID: missingSpanId, + traceID: currentTraceId, + }, + ], + }, + ]; + const spansWithOneRootWithNoRefs = [ + { + spanID: firstSpanId, + traceID: currentTraceId, + startTime: t + 200, + process: {}, + references: [ + { + spanID: thirdSpanId, + traceID: currentTraceId, + }, + ], + }, + { + spanID: secondSpanId, // root span + traceID: currentTraceId, + startTime: t + 100, + operationName, + process: { + serviceName, + }, + }, + { + spanID: thirdSpanId, + traceID: currentTraceId, + startTime: t, + process: {}, + references: [ + { + spanID: secondSpanId, + traceID: currentTraceId, + }, + ], + }, + ]; + + const fullTraceName = `${serviceName}: ${operationName}`; + + it('returns an empty string if given spans with no root among them', () => { + expect(getTraceName(spansWithNoRoots)).toEqual(''); + }); + + it('returns an id of root span with the earliest startTime', () => { + expect(getTraceName(spansWithMultipleRootsDifferentByStartTime)).toEqual(fullTraceName); + }); + + it('returns an id of root span without any refs', () => { + expect(getTraceName(spansWithMultipleRootsWithOneWithoutRefs)).toEqual(fullTraceName); + }); + + it('returns an id of root span with remote ref', () => { + expect(getTraceName(spansWithOneRootWithRemoteRef)).toEqual(fullTraceName); + }); + + it('returns an id of root span with no refs', () => { + expect(getTraceName(spansWithOneRootWithNoRefs)).toEqual(fullTraceName); + }); +}); diff --git a/packages/jaeger-ui-components/src/model/trace-viewer.ts b/packages/jaeger-ui-components/src/model/trace-viewer.ts index 69107c80a21..105821920e6 100644 --- a/packages/jaeger-ui-components/src/model/trace-viewer.ts +++ b/packages/jaeger-ui-components/src/model/trace-viewer.ts @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Uber Technologies, Inc. +// Copyright (c) 2020 The Jaeger Authors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,9 +12,46 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { memoize } from 'lodash'; + import { TraceSpan } from '../types/trace'; -export function getTraceName(spans: TraceSpan[]): string { - const span = spans.filter((sp) => !sp.references || !sp.references.length)[0]; - return span ? `${span.process.serviceName}: ${span.operationName}` : ''; +export function _getTraceNameImpl(spans: TraceSpan[]) { + // Use a span with no references to another span in given array + // prefering the span with the fewest references + // using start time as a tie breaker + let candidateSpan: TraceSpan | undefined; + const allIDs: Set = new Set(spans.map(({ spanID }) => spanID)); + + for (let i = 0; i < spans.length; i++) { + const hasInternalRef = + spans[i].references && + spans[i].references.some(({ traceID, spanID }) => traceID === spans[i].traceID && allIDs.has(spanID)); + if (hasInternalRef) { + continue; + } + + if (!candidateSpan) { + candidateSpan = spans[i]; + continue; + } + + const thisRefLength = (spans[i].references && spans[i].references.length) || 0; + const candidateRefLength = (candidateSpan.references && candidateSpan.references.length) || 0; + + if ( + thisRefLength < candidateRefLength || + (thisRefLength === candidateRefLength && spans[i].startTime < candidateSpan.startTime) + ) { + candidateSpan = spans[i]; + } + } + return candidateSpan ? `${candidateSpan.process.serviceName}: ${candidateSpan.operationName}` : ''; } + +export const getTraceName = memoize(_getTraceNameImpl, (spans: TraceSpan[]) => { + if (!spans.length) { + return 0; + } + return spans[0].traceID; +}); diff --git a/packages/jaeger-ui-components/src/model/transform-trace-data.test.js b/packages/jaeger-ui-components/src/model/transform-trace-data.test.js index 99c2bbad258..8dfa857fdcf 100644 --- a/packages/jaeger-ui-components/src/model/transform-trace-data.test.js +++ b/packages/jaeger-ui-components/src/model/transform-trace-data.test.js @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { orderTags, deduplicateTags } from './transform-trace-data'; +import transformTraceData, { orderTags, deduplicateTags } from './transform-trace-data'; describe('orderTags()', () => { it('correctly orders tags', () => { @@ -53,3 +53,111 @@ describe('deduplicateTags()', () => { expect(tagsInfo.warnings).toEqual(['Duplicate tag "b.ip:8.8.4.4"']); }); }); + +describe('transformTraceData()', () => { + const startTime = 1586160015434000; + const duration = 34000; + const traceID = 'f77950feed55c1ce91dd8e87896623a6'; + const rootSpanID = 'd4dcb46e95b781f5'; + const rootOperationName = 'rootOperation'; + const serviceName = 'serviceName'; + + const spans = [ + { + traceID, + spanID: '41f71485ed2593e4', + operationName: 'someOperationName', + references: [ + { + refType: 'CHILD_OF', + traceID, + spanID: rootSpanID, + }, + ], + startTime, + duration, + tags: [], + processID: 'p1', + }, + { + traceID, + spanID: '4f623fd33c213cba', + operationName: 'anotherOperationName', + references: [ + { + refType: 'CHILD_OF', + traceID, + spanID: rootSpanID, + }, + ], + startTime: startTime + 100, + duration, + tags: [], + processID: 'p1', + }, + ]; + + const rootSpanWithMissingRef = { + traceID, + spanID: rootSpanID, + operationName: rootOperationName, + references: [ + { + refType: 'CHILD_OF', + traceID, + spanID: 'missingSpanId', + }, + ], + startTime: startTime + 50, + duration, + tags: [], + processID: 'p1', + }; + + const rootSpanWithoutRefs = { + traceID, + spanID: rootSpanID, + operationName: rootOperationName, + startTime: startTime + 50, + duration, + tags: [], + processID: 'p1', + }; + + const processes = { + p1: { + serviceName, + tags: [], + }, + }; + + it('should return null for trace without traceID', () => { + const traceData = { + traceID: undefined, + processes, + spans, + }; + + expect(transformTraceData(traceData)).toEqual(null); + }); + + it('should return trace data with correct traceName based on root span with missing ref', () => { + const traceData = { + traceID, + processes, + spans: [...spans, rootSpanWithMissingRef], + }; + + expect(transformTraceData(traceData).traceName).toEqual(`${serviceName}: ${rootOperationName}`); + }); + + it('should return trace data with correct traceName based on root span without any refs', () => { + const traceData = { + traceID, + processes, + spans: [...spans, rootSpanWithoutRefs], + }; + + expect(transformTraceData(traceData).traceName).toEqual(`${serviceName}: ${rootOperationName}`); + }); +}); diff --git a/packages/jaeger-ui-components/src/model/transform-trace-data.tsx b/packages/jaeger-ui-components/src/model/transform-trace-data.tsx index 5008faf7b8a..6652239bba1 100644 --- a/packages/jaeger-ui-components/src/model/transform-trace-data.tsx +++ b/packages/jaeger-ui-components/src/model/transform-trace-data.tsx @@ -20,6 +20,7 @@ import { getConfigValue } from '../utils/config/get-config'; import { TraceKeyValuePair, TraceSpan, Trace, TraceResponse } from '../types/trace'; // @ts-ignore import TreeNode from '../utils/TreeNode'; +import { getTraceName } from './trace-viewer'; // exported for tests export function deduplicateTags(spanTags: TraceKeyValuePair[]) { @@ -121,7 +122,6 @@ export default function transformTraceData(data: TraceResponse | undefined): Tra const tree = getTraceSpanIdsAsTree(data); const spans: TraceSpan[] = []; const svcCounts: Record = {}; - let traceName = ''; // Eslint complains about number type not needed but then TS complains it is implicitly any. // eslint-disable-next-line @typescript-eslint/no-inferrable-types @@ -135,9 +135,6 @@ export default function transformTraceData(data: TraceResponse | undefined): Tra } const { serviceName } = span.process; svcCounts[serviceName] = (svcCounts[serviceName] || 0) + 1; - if (!span.references || !span.references.length) { - traceName = `${serviceName}: ${span.operationName}`; - } span.relativeStartTime = span.startTime - traceStartTime; span.depth = depth - 1; span.hasChildren = node.children.length > 0; @@ -166,6 +163,7 @@ export default function transformTraceData(data: TraceResponse | undefined): Tra }); spans.push(span); }); + const traceName = getTraceName(spans); const services = Object.keys(svcCounts).map((name) => ({ name, numberOfSpans: svcCounts[name] })); return { services,