diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.test.js index 62e6d1b773..7716a4d581 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.test.js @@ -22,6 +22,7 @@ import { DEFAULT_HEIGHTS, VirtualizedTraceViewImpl } from './VirtualizedTraceVie import traceGenerator from '../../../demo/trace-generators'; import transformTraceData from '../../../model/transform-trace-data'; import updateUiFindSpy from '../../../utils/update-ui-find'; +import * as linkPatterns from '../../../model/link-patterns'; jest.mock('./SpanTreeOffset'); jest.mock('../../../utils/update-ui-find'); @@ -430,4 +431,40 @@ describe('', () => { expect(focusUiFindMatchesMock).toHaveBeenLastCalledWith(trace, spanName, false); }); }); + + describe('linksGetter()', () => { + const span = trace.spans[1]; + const key = span.tags[0].key; + const val = encodeURIComponent(span.tags[0].value); + const origLinkPatterns = [...linkPatterns.processedLinks]; + + beforeEach(() => { + linkPatterns.processedLinks.splice(0, linkPatterns.processedLinks.length); + }); + + afterAll(() => { + linkPatterns.processedLinks.splice(0, linkPatterns.processedLinks.length); + linkPatterns.processedLinks.push(...origLinkPatterns); + }); + + it('linksGetter is expected to receive url and text for a given link pattern', () => { + const linkPatternConfig = [ + { + key, + type: 'tags', + url: `http://example.com/?key1=#{${key}}&traceID=#{trace.traceID}&startTime=#{trace.startTime}`, + text: `For first link traceId is - #{trace.traceID}`, + }, + ].map(linkPatterns.processLinkPattern); + + linkPatterns.processedLinks.push(...linkPatternConfig); + + expect(instance.linksGetter(span, span.tags, 0)).toEqual([ + { + url: `http://example.com/?key1=${val}&traceID=${trace.traceID}&startTime=${trace.startTime}`, + text: `For first link traceId is - ${trace.traceID}`, + }, + ]); + }); + }); }); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.tsx index d139da234d..e111bbf7c3 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -316,7 +316,10 @@ export class VirtualizedTraceViewImpl extends React.Component getLinks(span, items, itemIndex); + linksGetter = (span: Span, items: KeyValuePair[], itemIndex: number) => { + const { trace } = this.props; + return getLinks(span, items, itemIndex, trace); + }; renderRow = (key: string, style: React.CSSProperties, index: number, attrs: {}) => { const { isDetail, span, spanIndex } = this.getRowStates()[index]; diff --git a/packages/jaeger-ui/src/model/link-patterns.test.js b/packages/jaeger-ui/src/model/link-patterns.test.js index 8ff42b5925..662466a0ee 100644 --- a/packages/jaeger-ui/src/model/link-patterns.test.js +++ b/packages/jaeger-ui/src/model/link-patterns.test.js @@ -17,6 +17,7 @@ import { createTestFunction, getParameterInArray, getParameterInAncestor, + getParameterInTrace, processLinkPattern, computeLinks, createGetLinks, @@ -296,6 +297,31 @@ describe('getParameterInAncestor()', () => { }); }); +describe('getParameterInTrace()', () => { + const trace = { + processes: [], + traceName: 'theTrace', + traceID: 'trc1', + spans: [], + startTime: 1000, + endTime: 3000, + duration: 2000, + services: [], + }; + + it('returns an entry that is present', () => { + expect(getParameterInTrace('startTime', trace)).toEqual({ key: 'startTime', value: trace.startTime }); + }); + + it('returns undefined when the entry cannot be found', () => { + expect(getParameterInTrace('someThingElse', trace)).toBeUndefined(); + }); + + it('returns undefined when there is no trace', () => { + expect(getParameterInTrace('traceID')).toBeUndefined(); + }); +}); + describe('computeTraceLink()', () => { const linkPatterns = [ { @@ -354,11 +380,29 @@ describe('computeLinks()', () => { url: 'http://example.com/?myKey=#{myOtherKey}&myKey=#{myKey}', text: 'second link (#{myOtherKey})', }, + { + type: 'logs', + key: 'myThirdKey', + url: + 'http://example.com/?myKey1=#{myKey}&myKey=#{myThirdKey}&traceID=#{trace.traceID}&startTime=#{trace.startTime}', + text: 'third link (#{myThirdKey}) for traceID - #{trace.traceID}', + }, ].map(processLinkPattern); const spans = [ { depth: 0, process: {}, tags: [{ key: 'myKey', value: 'valueOfMyKey' }] }, - { depth: 1, process: {}, logs: [{ fields: [{ key: 'myOtherKey', value: 'valueOfMy+Other+Key' }] }] }, + { + depth: 1, + process: {}, + logs: [ + { + fields: [ + { key: 'myOtherKey', value: 'valueOfMy+Other+Key' }, + { key: 'myThirdKey', value: 'valueOfThirdMyKey' }, + ], + }, + ], + }, ]; spans[1].references = [ { @@ -367,6 +411,17 @@ describe('computeLinks()', () => { }, ]; + const trace = { + processes: [], + traceName: 'theTrace', + traceID: 'trc1', + spans: [], + startTime: 1000, + endTime: 3000, + duration: 2000, + services: [], + }; + it('correctly computes links', () => { expect(computeLinks(linkPatterns, spans[0], spans[0].tags, 0)).toEqual([ { @@ -380,6 +435,12 @@ describe('computeLinks()', () => { text: 'second link (valueOfMy+Other+Key)', }, ]); + expect(computeLinks(linkPatterns, spans[1], spans[1].logs[0].fields, 1, trace)).toEqual([ + { + url: 'http://example.com/?myKey1=valueOfMyKey&myKey=valueOfThirdMyKey&traceID=trc1&startTime=1000', + text: 'third link (valueOfThirdMyKey) for traceID - trc1', + }, + ]); }); }); diff --git a/packages/jaeger-ui/src/model/link-patterns.tsx b/packages/jaeger-ui/src/model/link-patterns.tsx index d3bd328cbf..86164e1f16 100644 --- a/packages/jaeger-ui/src/model/link-patterns.tsx +++ b/packages/jaeger-ui/src/model/link-patterns.tsx @@ -110,6 +110,27 @@ export function getParameterInAncestor(name: string, span: Span) { } currentSpan = getParent(currentSpan); } + + return undefined; +} + +const getValidTraceKeys = memoize(10)((trace: Trace) => { + const validKeys = (Object.keys(trace) as (keyof Trace)[]).filter( + key => typeof trace[key] === 'string' || typeof trace[key] === 'number' + ); + return validKeys; +}); + +export function getParameterInTrace(name: string, trace: Trace | undefined) { + if (trace) { + const validTraceKeys = getValidTraceKeys(trace); + + const key = name as keyof Trace; + if (validTraceKeys.includes(key)) { + return { key, value: trace[key] }; + } + } + return undefined; } @@ -119,20 +140,17 @@ function callTemplate(template: ProcessedTemplate, data: any) { export function computeTraceLink(linkPatterns: ProcessedLinkPattern[], trace: Trace) { const result: TLinksRV = []; - const validKeys = (Object.keys(trace) as (keyof Trace)[]).filter( - key => typeof trace[key] === 'string' || typeof trace[key] === 'number' - ); linkPatterns .filter(pattern => pattern.type('traces')) .forEach(pattern => { const parameterValues: Record = {}; const allParameters = pattern.parameters.every(parameter => { - const key = parameter as keyof Trace; - if (validKeys.includes(key)) { + const traceKV = getParameterInTrace(parameter, trace); + if (traceKV) { // At this point is safe to access to trace object using parameter variable because // we validated parameter against validKeys, this implies that parameter a keyof Trace. - parameterValues[parameter] = trace[key]; + parameterValues[parameter] = traceKV.value; return true; } return false; @@ -157,7 +175,8 @@ export function computeLinks( linkPatterns: ProcessedLinkPattern[], span: Span, items: KeyValuePair[], - itemIndex: number + itemIndex: number, + trace: Trace | undefined ) { const item = items[itemIndex]; let type = 'logs'; @@ -174,16 +193,25 @@ export function computeLinks( if (pattern.type(type) && pattern.key(item.key) && pattern.value(item.value)) { const parameterValues: Record = {}; const allParameters = pattern.parameters.every(parameter => { - let entry = getParameterInArray(parameter, items); - if (!entry && !processTags) { - // do not look in ancestors for process tags because the same object may appear in different places in the hierarchy - // and the cache in getLinks uses that object as a key - entry = getParameterInAncestor(parameter, span); + let entry; + + if (parameter.startsWith('trace.')) { + entry = getParameterInTrace(parameter.split('trace.')[1], trace); + } else { + entry = getParameterInArray(parameter, items); + + if (!entry && !processTags) { + // do not look in ancestors for process tags because the same object may appear in different places in the hierarchy + // and the cache in getLinks uses that object as a key + entry = getParameterInAncestor(parameter, span); + } } + if (entry) { parameterValues[parameter] = entry.value; return true; } + // eslint-disable-next-line no-console console.warn( `Skipping link pattern, missing parameter ${parameter} for key ${item.key} in ${type}.`, @@ -203,21 +231,21 @@ export function computeLinks( } export function createGetLinks(linkPatterns: ProcessedLinkPattern[], cache: WeakMap) { - return (span: Span, items: KeyValuePair[], itemIndex: number) => { + return (span: Span, items: KeyValuePair[], itemIndex: number, trace: Trace | undefined) => { if (linkPatterns.length === 0) { return []; } const item = items[itemIndex]; let result = cache.get(item); if (!result) { - result = computeLinks(linkPatterns, span, items, itemIndex); + result = computeLinks(linkPatterns, span, items, itemIndex, trace); cache.set(item, result); } return result; }; } -const processedLinks: ProcessedLinkPattern[] = (getConfigValue('linkPatterns') || []) +export const processedLinks: ProcessedLinkPattern[] = (getConfigValue('linkPatterns') || []) .map(processLinkPattern) .filter(Boolean);