diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js
index d0eb11d797..94c46fe8d2 100644
--- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js
+++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js
@@ -21,20 +21,22 @@ import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right';
import * as markers from './AccordianKeyValues.markers';
import KeyValuesTable from './KeyValuesTable';
+import type { KeyValuePair, Link } from '../../../../types';
import './AccordianKeyValues.css';
type AccordianKeyValuesProps = {
className?: ?string,
- data: { key: string, value: any }[],
+ data: KeyValuePair[],
highContrast?: boolean,
isOpen: boolean,
label: string,
+ linksGetter: ?(KeyValuePair[], number) => Link[],
onToggle: () => void,
};
// export for tests
-export function KeyValuesSummary(props: { data?: { key: string, value: any }[] }) {
+export function KeyValuesSummary(props: { data?: KeyValuePair[] }) {
const { data } = props;
if (!Array.isArray(data) || !data.length) {
return null;
@@ -59,7 +61,7 @@ KeyValuesSummary.defaultProps = {
};
export default function AccordianKeyValues(props: AccordianKeyValuesProps) {
- const { className, data, highContrast, isOpen, label, onToggle } = props;
+ const { className, data, highContrast, isOpen, label, linksGetter, onToggle } = props;
const isEmpty = !Array.isArray(data) || !data.length;
const iconCls = cx('u-align-icon', { 'AccordianKeyValues--emptyIcon': isEmpty });
return (
@@ -80,7 +82,7 @@ export default function AccordianKeyValues(props: AccordianKeyValuesProps) {
{!isOpen && }
- {isOpen && }
+ {isOpen && }
);
}
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.js
index a3df52a8bc..5f103977a8 100644
--- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.js
+++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.js
@@ -21,12 +21,13 @@ import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right';
import AccordianKeyValues from './AccordianKeyValues';
import { formatDuration } from '../utils';
-import type { Log } from '../../../../types';
+import type { Log, KeyValuePair, Link } from '../../../../types';
import './AccordianLogs.css';
type AccordianLogsProps = {
isOpen: boolean,
+ linksGetter: ?(KeyValuePair[], number) => Link[],
logs: Log[],
onItemToggle: Log => void,
onToggle: () => void,
@@ -35,7 +36,7 @@ type AccordianLogsProps = {
};
export default function AccordianLogs(props: AccordianLogsProps) {
- const { isOpen, logs, openedItems, onItemToggle, onToggle, timestamp } = props;
+ const { isOpen, linksGetter, logs, openedItems, onItemToggle, onToggle, timestamp } = props;
return (
@@ -59,6 +60,7 @@ export default function AccordianLogs(props: AccordianLogsProps) {
// compact
highContrast
isOpen={openedItems.has(log)}
+ linksGetter={linksGetter}
data={log.fields || []}
label={`${formatDuration(log.timestamp - timestamp)}`}
onToggle={() => onItemToggle(log)}
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.css
index 1add328273..826cd97a05 100644
--- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.css
+++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.css
@@ -39,3 +39,13 @@ limitations under the License.
white-space: pre;
width: 125px;
}
+
+.KeyValueTable--body > tr > td {
+ padding: 0.25rem 0.5rem;
+ vertical-align: top;
+}
+
+.KeyValueTable--linkIcon {
+ vertical-align: middle;
+ font-weight: bold;
+}
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.js
index 8abffdeb78..42728dc685 100644
--- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.js
+++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.js
@@ -14,8 +14,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-import React from 'react';
+import * as React from 'react';
import jsonMarkup from 'json-markup';
+import { Dropdown, Icon, Menu } from 'antd';
+import type { KeyValuePair, Link } from '../../../../types';
import './KeyValuesTable.css';
@@ -30,27 +32,70 @@ function parseIfJson(value) {
return value;
}
+const LinkValue = (props: { href: string, title?: string, children: React.Node }) => (
+
+ {props.children}
+
+);
+
+const linkValueList = (links: Link[]) => (
+
+);
+
type KeyValuesTableProps = {
- data: { key: string, value: any }[],
+ data: KeyValuePair[],
+ linksGetter: ?(KeyValuePair[], number) => Link[],
};
export default function KeyValuesTable(props: KeyValuesTableProps) {
- const { data } = props;
+ const { data, linksGetter } = props;
return (
{data.map((row, i) => {
- const jsonTable = (
- // eslint-disable-next-line react/no-danger
-
- );
+ const markup = {
+ __html: jsonMarkup(parseIfJson(row.value)),
+ };
+ // eslint-disable-next-line react/no-danger
+ const jsonTable = ;
+ const links = linksGetter ? linksGetter(data, i) : null;
+ let valueMarkup;
+ if (links && links.length === 1) {
+ valueMarkup = (
+
+
+ {jsonTable}
+
+
+ );
+ } else if (links && links.length > 1) {
+ valueMarkup = (
+
+ );
+ } else {
+ valueMarkup = jsonTable;
+ }
return (
// `i` is necessary in the key because row.key can repeat
// eslint-disable-next-line react/no-array-index-key
{row.key} |
- {jsonTable} |
+ {valueMarkup} |
);
})}
@@ -59,3 +104,5 @@ export default function KeyValuesTable(props: KeyValuesTableProps) {
);
}
+
+KeyValuesTable.LinkValue = LinkValue;
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js
index f82e062d1f..653c8104af 100644
--- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js
+++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.test.js
@@ -14,6 +14,7 @@
import React from 'react';
import { shallow } from 'enzyme';
+import { Dropdown } from 'antd';
import KeyValuesTable from './KeyValuesTable';
@@ -38,4 +39,59 @@ describe('', () => {
expect(tr.find('.KeyValueTable--keyColumn').text()).toMatch(data[i].key);
});
});
+
+ it('renders a single link correctly', () => {
+ wrapper.setProps({
+ linksGetter: (array, i) =>
+ array[i].key === 'span.kind'
+ ? [
+ {
+ url: `http://example.com/?kind=${encodeURIComponent(array[i].value)}`,
+ text: `More info about ${array[i].value}`,
+ },
+ ]
+ : [],
+ });
+
+ const anchor = wrapper.find(KeyValuesTable.LinkValue);
+ expect(anchor).toHaveLength(1);
+ expect(anchor.prop('href')).toBe('http://example.com/?kind=client');
+ expect(anchor.prop('title')).toBe('More info about client');
+ expect(
+ anchor
+ .closest('tr')
+ .find('td')
+ .first()
+ .text()
+ ).toBe('span.kind');
+ });
+
+ it('renders multiple links correctly', () => {
+ wrapper.setProps({
+ linksGetter: (array, i) =>
+ array[i].key === 'span.kind'
+ ? [
+ { url: `http://example.com/1?kind=${encodeURIComponent(array[i].value)}`, text: 'Example 1' },
+ { url: `http://example.com/2?kind=${encodeURIComponent(array[i].value)}`, text: 'Example 2' },
+ ]
+ : [],
+ });
+ const dropdown = wrapper.find(Dropdown);
+ const menu = shallow(dropdown.prop('overlay'));
+ const anchors = menu.find(KeyValuesTable.LinkValue);
+ expect(anchors).toHaveLength(2);
+ const firstAnchor = anchors.first();
+ expect(firstAnchor.prop('href')).toBe('http://example.com/1?kind=client');
+ expect(firstAnchor.children().text()).toBe('Example 1');
+ const secondAnchor = anchors.last();
+ expect(secondAnchor.prop('href')).toBe('http://example.com/2?kind=client');
+ expect(secondAnchor.children().text()).toBe('Example 2');
+ expect(
+ dropdown
+ .closest('tr')
+ .find('td')
+ .first()
+ .text()
+ ).toBe('span.kind');
+ });
});
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js
index cec43234ad..f97995783b 100644
--- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js
+++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js
@@ -22,12 +22,13 @@ import AccordianLogs from './AccordianLogs';
import DetailState from './DetailState';
import { formatDuration } from '../utils';
import LabeledList from '../../../common/LabeledList';
-import type { Log, Span } from '../../../../types';
+import type { Log, Span, KeyValuePair, Link } from '../../../../types';
import './index.css';
type SpanDetailProps = {
detailState: DetailState,
+ linksGetter: ?(KeyValuePair[], number) => Link[],
logItemToggle: (string, Log) => void,
logsToggle: string => void,
processToggle: string => void,
@@ -37,7 +38,16 @@ type SpanDetailProps = {
};
export default function SpanDetail(props: SpanDetailProps) {
- const { detailState, logItemToggle, logsToggle, processToggle, span, tagsToggle, traceStartTime } = props;
+ const {
+ detailState,
+ linksGetter,
+ logItemToggle,
+ logsToggle,
+ processToggle,
+ span,
+ tagsToggle,
+ traceStartTime,
+ } = props;
const { isTagsOpen, isProcessOpen, logs: logsState } = detailState;
const { operationName, process, duration, relativeStartTime, spanID, logs, tags } = span;
const overviewItems = [
@@ -73,6 +83,7 @@ export default function SpanDetail(props: SpanDetailProps) {
tagsToggle(spanID)}
/>
@@ -81,6 +92,7 @@ export default function SpanDetail(props: SpanDetailProps) {
className="ub-mb1"
data={process.tags}
label="Process"
+ linksGetter={linksGetter}
isOpen={isProcessOpen}
onToggle={() => processToggle(spanID)}
/>
@@ -89,6 +101,7 @@ export default function SpanDetail(props: SpanDetailProps) {
{logs &&
logs.length > 0 && (
void,
isFilteredOut: boolean,
+ linksGetter: ?(Span, KeyValuePair[], number) => Link[],
logItemToggle: (string, Log) => void,
logsToggle: string => void,
processToggle: string => void,
@@ -45,6 +46,11 @@ export default class SpanDetailRow extends React.PureComponent {
+ const { linksGetter, span } = this.props;
+ return linksGetter ? linksGetter(span, items, itemIndex) : [];
+ };
+
render() {
const {
color,
@@ -76,6 +82,7 @@ export default class SpanDetailRow extends React.PureComponent
', () => {
columnDivision: 0.5,
detailState: new DetailState(),
onDetailToggled: jest.fn(),
+ linksGetter: jest.fn(),
isFilteredOut: false,
logItemToggle: jest.fn(),
logsToggle: jest.fn(),
@@ -40,6 +41,7 @@ describe('', () => {
beforeEach(() => {
props.onDetailToggled.mockReset();
+ props.linksGetter.mockReset();
props.logItemToggle.mockReset();
props.logsToggle.mockReset();
props.processToggle.mockReset();
@@ -72,6 +74,7 @@ describe('', () => {
const spanDetail = (
', () => {
);
expect(wrapper.contains(spanDetail)).toBe(true);
});
+
+ it('adds span when calling linksGetter', () => {
+ const spanDetail = wrapper.find(SpanDetail);
+ const linksGetter = spanDetail.prop('linksGetter');
+ const tags = [{ key: 'myKey', value: 'myValue' }];
+ const linksGetterResponse = {};
+ props.linksGetter.mockReturnValueOnce(linksGetterResponse);
+ const result = linksGetter(tags, 0);
+ expect(result).toBe(linksGetterResponse);
+ expect(props.linksGetter).toHaveBeenCalledTimes(1);
+ expect(props.linksGetter).toHaveBeenCalledWith(props.span, tags, 0);
+ });
});
diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js
index c19e87d099..cd3046df10 100644
--- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js
+++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/VirtualizedTraceView.js
@@ -31,8 +31,9 @@ import {
isErrorSpan,
spanContainsErredSpan,
} from './utils';
+import getLinks from '../../../model/link-patterns';
import type { Accessors } from '../ScrollManager';
-import type { Log, Span, Trace } from '../../../types';
+import type { Log, Span, Trace, KeyValuePair } from '../../../types';
import colorGenerator from '../../../utils/color-generator';
import './VirtualizedTraceView.css';
@@ -264,6 +265,8 @@ export class VirtualizedTraceViewImpl extends React.PureComponent getLinks(span, items, itemIndex);
+
renderRow = (key: string, style: Style, index: number, attrs: {}) => {
const { isDetail, span, spanIndex } = this.rowStates[index];
return isDetail
@@ -380,6 +383,7 @@ export class VirtualizedTraceViewImpl extends React.PureComponent string,
+};
+
+type ProcessedLinkPattern = {
+ object: any,
+ type: string => boolean,
+ key: string => boolean,
+ value: any => boolean,
+ url: ProcessedTemplate,
+ text: ProcessedTemplate,
+ parameters: string[],
+};
+
+function getParamNames(str) {
+ const names = new Set();
+ str.replace(parameterRegExp, (match, name) => {
+ names.add(name);
+ return match;
+ });
+ return Array.from(names);
+}
+
+function stringSupplant(str, encodeFn: any => string, map) {
+ return str.replace(parameterRegExp, (_, name) => {
+ const value = map[name];
+ return value == null ? '' : encodeFn(value);
+ });
+}
+
+export function processTemplate(template: any, encodeFn: any => string): ProcessedTemplate {
+ if (typeof template !== 'string') {
+ /*
+
+ // kept on ice until #123 is implemented:
+ if (template && Array.isArray(template.parameters) && (typeof template.template === 'function')) {
+ return template;
+ }
+
+ */
+ throw new Error('Invalid template');
+ }
+ return {
+ parameters: getParamNames(template),
+ template: stringSupplant.bind(null, template, encodeFn),
+ };
+}
+
+export function createTestFunction(entry: any) {
+ if (typeof entry === 'string') {
+ return (arg: any) => arg === entry;
+ }
+ if (Array.isArray(entry)) {
+ return (arg: any) => entry.indexOf(arg) > -1;
+ }
+ /*
+
+ // kept on ice until #123 is implemented:
+ if (entry instanceof RegExp) {
+ return (arg: any) => entry.test(arg);
+ }
+ if (typeof entry === 'function') {
+ return entry;
+ }
+
+ */
+ if (entry == null) {
+ return () => true;
+ }
+ throw new Error(`Invalid value: ${entry}`);
+}
+
+const identity = a => a;
+
+export function processLinkPattern(pattern: any): ?ProcessedLinkPattern {
+ try {
+ const url = processTemplate(pattern.url, encodeURIComponent);
+ const text = processTemplate(pattern.text, identity);
+ return {
+ object: pattern,
+ type: createTestFunction(pattern.type),
+ key: createTestFunction(pattern.key),
+ value: createTestFunction(pattern.value),
+ url,
+ text,
+ parameters: _uniq(url.parameters.concat(text.parameters)),
+ };
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.error(`Ignoring invalid link pattern: ${error}`, pattern);
+ return null;
+ }
+}
+
+export function getParameterInArray(name: string, array: KeyValuePair[]) {
+ if (array) {
+ return array.find(entry => entry.key === name);
+ }
+ return undefined;
+}
+
+export function getParameterInAncestor(name: string, span: Span) {
+ let currentSpan = span;
+ while (currentSpan) {
+ const result =
+ getParameterInArray(name, currentSpan.tags) || getParameterInArray(name, currentSpan.process.tags);
+ if (result) {
+ return result;
+ }
+ currentSpan = getParent(currentSpan);
+ }
+ return undefined;
+}
+
+function callTemplate(template, data) {
+ return template.template(data);
+}
+
+export function computeLinks(
+ linkPatterns: ProcessedLinkPattern[],
+ span: Span,
+ items: KeyValuePair[],
+ itemIndex: number
+) {
+ const item = items[itemIndex];
+ let type = 'logs';
+ const processTags = span.process.tags === items;
+ if (processTags) {
+ type = 'process';
+ }
+ const spanTags = span.tags === items;
+ if (spanTags) {
+ type = 'tags';
+ }
+ const result = [];
+ linkPatterns.forEach(pattern => {
+ if (pattern.type(type) && pattern.key(item.key) && pattern.value(item.value)) {
+ const parameterValues = {};
+ 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);
+ }
+ 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}.`,
+ pattern.object
+ );
+ return false;
+ });
+ if (allParameters) {
+ result.push({
+ url: callTemplate(pattern.url, parameterValues),
+ text: callTemplate(pattern.text, parameterValues),
+ });
+ }
+ }
+ });
+ return result;
+}
+
+export function createGetLinks(linkPatterns: ProcessedLinkPattern[], cache: WeakMap) {
+ return (span: Span, items: KeyValuePair[], itemIndex: number) => {
+ if (linkPatterns.length === 0) {
+ return [];
+ }
+ const item = items[itemIndex];
+ let result = cache.get(item);
+ if (!result) {
+ result = computeLinks(linkPatterns, span, items, itemIndex);
+ cache.set(item, result);
+ }
+ return result;
+ };
+}
+
+export default createGetLinks(
+ (getConfigValue('linkPatterns') || []).map(processLinkPattern).filter(Boolean),
+ new WeakMap()
+);
diff --git a/packages/jaeger-ui/src/model/link-patterns.test.js b/packages/jaeger-ui/src/model/link-patterns.test.js
new file mode 100644
index 0000000000..8694c8f0ff
--- /dev/null
+++ b/packages/jaeger-ui/src/model/link-patterns.test.js
@@ -0,0 +1,396 @@
+// Copyright (c) 2017 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 {
+ processTemplate,
+ createTestFunction,
+ getParameterInArray,
+ getParameterInAncestor,
+ processLinkPattern,
+ computeLinks,
+ createGetLinks,
+} from './link-patterns';
+
+describe('processTemplate()', () => {
+ it('correctly replaces variables', () => {
+ const processedTemplate = processTemplate(
+ 'this is a test with #{oneVariable}#{anotherVariable} and the same #{oneVariable}',
+ a => a
+ );
+ expect(processedTemplate.parameters).toEqual(['oneVariable', 'anotherVariable']);
+ expect(processedTemplate.template({ oneVariable: 'MYFIRSTVAR', anotherVariable: 'SECOND' })).toBe(
+ 'this is a test with MYFIRSTVARSECOND and the same MYFIRSTVAR'
+ );
+ });
+
+ it('correctly uses the encoding function', () => {
+ const processedTemplate = processTemplate(
+ 'this is a test with #{oneVariable}#{anotherVariable} and the same #{oneVariable}',
+ e => `/${e}\\`
+ );
+ expect(processedTemplate.parameters).toEqual(['oneVariable', 'anotherVariable']);
+ expect(processedTemplate.template({ oneVariable: 'MYFIRSTVAR', anotherVariable: 'SECOND' })).toBe(
+ 'this is a test with /MYFIRSTVAR\\/SECOND\\ and the same /MYFIRSTVAR\\'
+ );
+ });
+
+ /*
+ // kept on ice until #123 is implemented:
+
+ it('correctly returns the same object when passing an already processed template', () => {
+ const alreadyProcessed = {
+ parameters: ['b'],
+ template: data => `a${data.b}c`,
+ };
+ const processedTemplate = processTemplate(alreadyProcessed, a => a);
+ expect(processedTemplate).toBe(alreadyProcessed);
+ });
+
+ */
+
+ it('reports an error when passing an object that does not look like an already processed template', () => {
+ expect(() =>
+ processTemplate(
+ {
+ template: data => `a${data.b}c`,
+ },
+ a => a
+ )
+ ).toThrow();
+ expect(() =>
+ processTemplate(
+ {
+ parameters: ['b'],
+ },
+ a => a
+ )
+ ).toThrow();
+ expect(() => processTemplate({}, a => a)).toThrow();
+ });
+});
+
+describe('createTestFunction()', () => {
+ it('accepts a string', () => {
+ const testFn = createTestFunction('myValue');
+ expect(testFn('myValue')).toBe(true);
+ expect(testFn('myFirstValue')).toBe(false);
+ expect(testFn('mySecondValue')).toBe(false);
+ expect(testFn('otherValue')).toBe(false);
+ });
+
+ it('accepts an array', () => {
+ const testFn = createTestFunction(['myFirstValue', 'mySecondValue']);
+ expect(testFn('myValue')).toBe(false);
+ expect(testFn('myFirstValue')).toBe(true);
+ expect(testFn('mySecondValue')).toBe(true);
+ expect(testFn('otherValue')).toBe(false);
+ });
+
+ /*
+ // kept on ice until #123 is implemented:
+
+ it('accepts a regular expression', () => {
+ const testFn = createTestFunction(/^my.*Value$/);
+ expect(testFn('myValue')).toBe(true);
+ expect(testFn('myFirstValue')).toBe(true);
+ expect(testFn('mySecondValue')).toBe(true);
+ expect(testFn('otherValue')).toBe(false);
+ });
+
+ it('accepts a function', () => {
+ const mockCallback = jest.fn();
+ mockCallback
+ .mockReturnValueOnce(true)
+ .mockReturnValueOnce(false)
+ .mockReturnValueOnce(true)
+ .mockReturnValue(false);
+ const testFn = createTestFunction(mockCallback);
+ expect(testFn('myValue')).toBe(true);
+ expect(mockCallback).toHaveBeenCalledTimes(1);
+ expect(mockCallback).toHaveBeenCalledWith('myValue');
+ expect(testFn('myFirstValue')).toBe(false);
+ expect(mockCallback).toHaveBeenCalledTimes(2);
+ expect(mockCallback).toHaveBeenCalledWith('myFirstValue');
+ expect(testFn('mySecondValue')).toBe(true);
+ expect(mockCallback).toHaveBeenCalledTimes(3);
+ expect(mockCallback).toHaveBeenCalledWith('mySecondValue');
+ expect(testFn('otherValue')).toBe(false);
+ expect(mockCallback).toHaveBeenCalledTimes(4);
+ expect(mockCallback).toHaveBeenCalledWith('otherValue');
+ });
+
+ */
+
+ it('accepts undefined', () => {
+ const testFn = createTestFunction();
+ expect(testFn('myValue')).toBe(true);
+ expect(testFn('myFirstValue')).toBe(true);
+ expect(testFn('mySecondValue')).toBe(true);
+ expect(testFn('otherValue')).toBe(true);
+ });
+
+ it('rejects unknown values', () => {
+ expect(() => createTestFunction({})).toThrow();
+ expect(() => createTestFunction(true)).toThrow();
+ expect(() => createTestFunction(false)).toThrow();
+ expect(() => createTestFunction(0)).toThrow();
+ expect(() => createTestFunction(5)).toThrow();
+ });
+});
+
+describe('getParameterInArray()', () => {
+ const data = [{ key: 'mykey', value: 'ok' }, { key: 'otherkey', value: 'v' }];
+
+ it('returns an entry that is present', () => {
+ expect(getParameterInArray('mykey', data)).toBe(data[0]);
+ expect(getParameterInArray('otherkey', data)).toBe(data[1]);
+ });
+
+ it('returns undefined when the entry cannot be found', () => {
+ expect(getParameterInArray('myotherkey', data)).toBeUndefined();
+ });
+
+ it('returns undefined when there is no array', () => {
+ expect(getParameterInArray('otherkey')).toBeUndefined();
+ expect(getParameterInArray('otherkey', null)).toBeUndefined();
+ });
+});
+
+describe('getParameterInAncestor()', () => {
+ const spans = [
+ {
+ depth: 0,
+ process: {
+ tags: [
+ { key: 'a', value: 'a7' },
+ { key: 'b', value: 'b7' },
+ { key: 'c', value: 'c7' },
+ { key: 'd', value: 'd7' },
+ { key: 'e', value: 'e7' },
+ { key: 'f', value: 'f7' },
+ { key: 'g', value: 'g7' },
+ { key: 'h', value: 'h7' },
+ ],
+ },
+ tags: [
+ { key: 'a', value: 'a6' },
+ { key: 'b', value: 'b6' },
+ { key: 'c', value: 'c6' },
+ { key: 'd', value: 'd6' },
+ { key: 'e', value: 'e6' },
+ { key: 'f', value: 'f6' },
+ { key: 'g', value: 'g6' },
+ ],
+ },
+ {
+ depth: 1,
+ process: {
+ tags: [
+ { key: 'a', value: 'a5' },
+ { key: 'b', value: 'b5' },
+ { key: 'c', value: 'c5' },
+ { key: 'd', value: 'd5' },
+ { key: 'e', value: 'e5' },
+ { key: 'f', value: 'f5' },
+ ],
+ },
+ tags: [
+ { key: 'a', value: 'a4' },
+ { key: 'b', value: 'b4' },
+ { key: 'c', value: 'c4' },
+ { key: 'd', value: 'd4' },
+ { key: 'e', value: 'e4' },
+ ],
+ },
+ {
+ depth: 1,
+ process: {
+ tags: [
+ { key: 'a', value: 'a3' },
+ { key: 'b', value: 'b3' },
+ { key: 'c', value: 'c3' },
+ { key: 'd', value: 'd3' },
+ ],
+ },
+ tags: [{ key: 'a', value: 'a2' }, { key: 'b', value: 'b2' }, { key: 'c', value: 'c2' }],
+ },
+ {
+ depth: 2,
+ process: {
+ tags: [{ key: 'a', value: 'a1' }, { key: 'b', value: 'b1' }],
+ },
+ tags: [{ key: 'a', value: 'a0' }],
+ },
+ ];
+ spans[1].references = [
+ {
+ refType: 'CHILD_OF',
+ span: spans[0],
+ },
+ ];
+ spans[2].references = [
+ {
+ refType: 'CHILD_OF',
+ span: spans[0],
+ },
+ ];
+ spans[3].references = [
+ {
+ refType: 'CHILD_OF',
+ span: spans[2],
+ },
+ ];
+
+ it('uses current span tags', () => {
+ expect(getParameterInAncestor('a', spans[3])).toEqual({ key: 'a', value: 'a0' });
+ expect(getParameterInAncestor('a', spans[2])).toEqual({ key: 'a', value: 'a2' });
+ expect(getParameterInAncestor('a', spans[1])).toEqual({ key: 'a', value: 'a4' });
+ expect(getParameterInAncestor('a', spans[0])).toEqual({ key: 'a', value: 'a6' });
+ });
+
+ it('uses current span process tags', () => {
+ expect(getParameterInAncestor('b', spans[3])).toEqual({ key: 'b', value: 'b1' });
+ expect(getParameterInAncestor('d', spans[2])).toEqual({ key: 'd', value: 'd3' });
+ expect(getParameterInAncestor('f', spans[1])).toEqual({ key: 'f', value: 'f5' });
+ expect(getParameterInAncestor('h', spans[0])).toEqual({ key: 'h', value: 'h7' });
+ });
+
+ it('uses parent span tags', () => {
+ expect(getParameterInAncestor('c', spans[3])).toEqual({ key: 'c', value: 'c2' });
+ expect(getParameterInAncestor('e', spans[2])).toEqual({ key: 'e', value: 'e6' });
+ expect(getParameterInAncestor('f', spans[2])).toEqual({ key: 'f', value: 'f6' });
+ expect(getParameterInAncestor('g', spans[2])).toEqual({ key: 'g', value: 'g6' });
+ expect(getParameterInAncestor('g', spans[1])).toEqual({ key: 'g', value: 'g6' });
+ });
+
+ it('uses parent span process tags', () => {
+ expect(getParameterInAncestor('d', spans[3])).toEqual({ key: 'd', value: 'd3' });
+ expect(getParameterInAncestor('h', spans[2])).toEqual({ key: 'h', value: 'h7' });
+ expect(getParameterInAncestor('h', spans[1])).toEqual({ key: 'h', value: 'h7' });
+ });
+
+ it('uses grand-parent span tags', () => {
+ expect(getParameterInAncestor('e', spans[3])).toEqual({ key: 'e', value: 'e6' });
+ expect(getParameterInAncestor('f', spans[3])).toEqual({ key: 'f', value: 'f6' });
+ expect(getParameterInAncestor('g', spans[3])).toEqual({ key: 'g', value: 'g6' });
+ });
+
+ it('uses grand-parent process tags', () => {
+ expect(getParameterInAncestor('h', spans[3])).toEqual({ key: 'h', value: 'h7' });
+ });
+
+ it('returns undefined when the entry cannot be found', () => {
+ expect(getParameterInAncestor('i', spans[3])).toBeUndefined();
+ });
+
+ it('does not break if some tags are not defined', () => {
+ const spansWithUndefinedTags = [
+ {
+ depth: 0,
+ process: {},
+ },
+ ];
+ expect(getParameterInAncestor('a', spansWithUndefinedTags[0])).toBeUndefined();
+ });
+});
+
+describe('computeLinks()', () => {
+ const linkPatterns = [
+ {
+ type: 'tags',
+ key: 'myKey',
+ url: 'http://example.com/?myKey=#{myKey}',
+ text: 'first link (#{myKey})',
+ },
+ {
+ key: 'myOtherKey',
+ url: 'http://example.com/?myKey=#{myOtherKey}&myKey=#{myKey}',
+ text: 'second link (#{myOtherKey})',
+ },
+ ].map(processLinkPattern);
+
+ const spans = [
+ { depth: 0, process: {}, tags: [{ key: 'myKey', value: 'valueOfMyKey' }] },
+ { depth: 1, process: {}, logs: [{ fields: [{ key: 'myOtherKey', value: 'valueOfMy+Other+Key' }] }] },
+ ];
+ spans[1].references = [
+ {
+ refType: 'CHILD_OF',
+ span: spans[0],
+ },
+ ];
+
+ it('correctly computes links', () => {
+ expect(computeLinks(linkPatterns, spans[0], spans[0].tags, 0)).toEqual([
+ {
+ url: 'http://example.com/?myKey=valueOfMyKey',
+ text: 'first link (valueOfMyKey)',
+ },
+ ]);
+ expect(computeLinks(linkPatterns, spans[1], spans[1].logs[0].fields, 0)).toEqual([
+ {
+ url: 'http://example.com/?myKey=valueOfMy%2BOther%2BKey&myKey=valueOfMyKey',
+ text: 'second link (valueOfMy+Other+Key)',
+ },
+ ]);
+ });
+});
+
+describe('getLinks()', () => {
+ const linkPatterns = [
+ {
+ key: 'mySpecialKey',
+ url: 'http://example.com/?mySpecialKey=#{mySpecialKey}',
+ text: 'special key link (#{mySpecialKey})',
+ },
+ ].map(processLinkPattern);
+ const template = jest.spyOn(linkPatterns[0].url, 'template');
+
+ const span = { depth: 0, process: {}, tags: [{ key: 'mySpecialKey', value: 'valueOfMyKey' }] };
+
+ let cache;
+
+ beforeEach(() => {
+ cache = new WeakMap();
+ template.mockClear();
+ });
+
+ it('does not access the cache if there is no link pattern', () => {
+ cache.get = jest.fn();
+ const getLinks = createGetLinks([], cache);
+ expect(getLinks(span, span.tags, 0)).toEqual([]);
+ expect(cache.get).not.toHaveBeenCalled();
+ });
+
+ it('returns the result from the cache', () => {
+ const result = [];
+ cache.set(span.tags[0], result);
+ const getLinks = createGetLinks(linkPatterns, cache);
+ expect(getLinks(span, span.tags, 0)).toBe(result);
+ expect(template).not.toHaveBeenCalled();
+ });
+
+ it('adds the result to the cache', () => {
+ const getLinks = createGetLinks(linkPatterns, cache);
+ const result = getLinks(span, span.tags, 0);
+ expect(template).toHaveBeenCalledTimes(1);
+ expect(result).toEqual([
+ {
+ url: 'http://example.com/?mySpecialKey=valueOfMyKey',
+ text: 'special key link (valueOfMyKey)',
+ },
+ ]);
+ expect(cache.get(span.tags[0])).toBe(result);
+ });
+});
diff --git a/packages/jaeger-ui/src/model/span.js b/packages/jaeger-ui/src/model/span.js
new file mode 100644
index 0000000000..370d5945cd
--- /dev/null
+++ b/packages/jaeger-ui/src/model/span.js
@@ -0,0 +1,27 @@
+// @flow
+
+// Copyright (c) 2017 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 type { Span } from '../types';
+
+/**
+ * Searches the span.references to find 'CHILD_OF' reference type or returns null.
+ * @param {Span} span The span whose parent is to be returned.
+ * @return {Span|null} The parent span if there is one, null otherwise.
+ */
+export function getParent(span: Span) {
+ const parentRef = span.references ? span.references.find(ref => ref.refType === 'CHILD_OF') : null;
+ return parentRef ? parentRef.span : null;
+}
diff --git a/packages/jaeger-ui/src/model/transform-trace-data.js b/packages/jaeger-ui/src/model/transform-trace-data.js
index 4b1a5fe1ed..8595261b20 100644
--- a/packages/jaeger-ui/src/model/transform-trace-data.js
+++ b/packages/jaeger-ui/src/model/transform-trace-data.js
@@ -80,27 +80,23 @@ export default function transfromTraceData(data: TraceData & { spans: SpanWithPr
if (spanID === '__root__') {
return;
}
- const span: ?SpanWithProcess = spanMap.get(spanID);
+ const span: ?Span = (spanMap.get(spanID): any);
if (!span) {
return;
}
- spans.push({
- relativeStartTime: span.startTime - traceStartTime,
- depth: depth - 1,
- hasChildren: node.children.length > 0,
- // spread fails with union types
- duration: span.duration,
- logs: span.logs,
- operationName: span.operationName,
- process: span.process,
- processID: span.processID,
- references: span.references,
- spanID: span.spanID,
- startTime: span.startTime,
- tags: span.tags,
- traceID: span.traceID,
+ span.relativeStartTime = span.startTime - traceStartTime;
+ span.depth = depth - 1;
+ span.hasChildren = node.children.length > 0;
+ span.references.forEach(ref => {
+ const refSpan: ?Span = (spanMap.get(ref.spanID): any);
+ if (refSpan) {
+ // eslint-disable-next-line no-param-reassign
+ ref.span = refSpan;
+ }
});
+ spans.push(span);
});
+
return {
spans,
traceID,
diff --git a/packages/jaeger-ui/src/types/index.js b/packages/jaeger-ui/src/types/index.js
index 55ca7d2028..09b6f100b1 100644
--- a/packages/jaeger-ui/src/types/index.js
+++ b/packages/jaeger-ui/src/types/index.js
@@ -18,11 +18,16 @@
* All timestamps are in microseconds
*/
-type KeyValuePair = {
+export type KeyValuePair = {
key: string,
value: any,
};
+export type Link = {
+ url: string,
+ text: string,
+};
+
export type Log = {
timestamp: number,
fields: Array,
@@ -35,6 +40,8 @@ export type Process = {
export type SpanReference = {
refType: 'CHILD_OF' | 'FOLLOWS_FROM',
+ // eslint-disable-next-line no-use-before-define
+ span: ?Span,
spanID: string,
traceID: string,
};