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..ec2c6e4845 100644
--- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js
+++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianKeyValues.js
@@ -30,6 +30,7 @@ type AccordianKeyValuesProps = {
highContrast?: boolean,
isOpen: boolean,
label: string,
+ linksGetter: ({ key: string, value: any }[], number) => { url: string, text: string }[],
onToggle: () => void,
};
@@ -59,7 +60,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 +81,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..3c037e4034 100644
--- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.js
+++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.js
@@ -27,6 +27,7 @@ import './AccordianLogs.css';
type AccordianLogsProps = {
isOpen: boolean,
+ linksGetter: ({ key: string, value: any }[], number) => { url: string, text: string }[],
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..eeb7e8b774 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--link {
+ display: block;
+ position: relative;
+}
+
+.KeyValueTable--linkIcon {
+ position: absolute;
+ right: 0px;
+}
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..e461125879 100644
--- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.js
+++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/KeyValuesTable.js
@@ -16,6 +16,7 @@
import React from 'react';
import jsonMarkup from 'json-markup';
+import { Dropdown, Icon, Menu } from 'antd';
import './KeyValuesTable.css';
@@ -32,10 +33,11 @@ function parseIfJson(value) {
type KeyValuesTableProps = {
data: { key: string, value: any }[],
+ linksGetter: ({ key: string, value: any }[], number) => { url: string, text: string }[],
};
export default function KeyValuesTable(props: KeyValuesTableProps) {
- const { data } = props;
+ const { data, linksGetter } = props;
return (
@@ -45,12 +47,53 @@ export default function KeyValuesTable(props: KeyValuesTableProps) {
// eslint-disable-next-line react/no-danger
);
+ let valueMarkup = jsonTable;
+ const links = linksGetter ? linksGetter(data, i) : null;
+ if (links && links.length === 1) {
+ valueMarkup = (
+
+
+ {jsonTable}
+
+ );
+ } else if (links && links.length > 1) {
+ const menuItems = (
+
+ );
+ 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} |
);
})}
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..27df34cbb4 100644
--- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js
+++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.js
@@ -28,6 +28,7 @@ import './index.css';
type SpanDetailProps = {
detailState: DetailState,
+ linksGetter: ({ key: string, value: any }[], number) => { url: string, text: string }[],
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: (number, { key: string, value: any }[], number) => { url: string, text: string }[],
logItemToggle: (string, Log) => void,
logsToggle: string => void,
processToggle: string => void,
span: Span,
+ spanIndex: number,
tagsToggle: string => void,
traceStartTime: number,
};
@@ -45,6 +47,9 @@ export default class SpanDetailRow extends React.PureComponent
+ this.props.linksGetter(this.props.spanIndex, items, itemIndex);
+
render() {
const {
color,
@@ -76,6 +81,7 @@ export default class SpanDetailRow extends React.PureComponent
+ getLinks(this.props.trace, spanIndex, items, itemIndex);
+
renderRow = (key: string, style: Style, index: number, attrs: {}) => {
const { isDetail, span, spanIndex } = this.rowStates[index];
return isDetail
- ? this.renderSpanDetailRow(span, key, style, attrs)
+ ? this.renderSpanDetailRow(span, spanIndex, key, style, attrs)
: this.renderSpanBarRow(span, spanIndex, key, style, attrs);
};
@@ -352,7 +356,7 @@ export class VirtualizedTraceViewImpl extends React.PureComponent
diff --git a/packages/jaeger-ui/src/constants/default-config.js b/packages/jaeger-ui/src/constants/default-config.js
index d526193fca..250d133761 100644
--- a/packages/jaeger-ui/src/constants/default-config.js
+++ b/packages/jaeger-ui/src/constants/default-config.js
@@ -24,6 +24,7 @@ export default deepFreeze(
dagMaxNumServices: FALLBACK_DAG_MAX_NUM_SERVICES,
menuEnabled: true,
},
+ linkPatterns: [],
tracking: {
gaID: null,
trackErrors: true,
diff --git a/packages/jaeger-ui/src/model/link-patterns.js b/packages/jaeger-ui/src/model/link-patterns.js
new file mode 100644
index 0000000000..cf7eeabec3
--- /dev/null
+++ b/packages/jaeger-ui/src/model/link-patterns.js
@@ -0,0 +1,183 @@
+// 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 _uniq from 'lodash/uniq';
+import { getConfigValue } from '../utils/config/get-config';
+
+const parameterRegExp = /\$\{([^{}]*)\}/;
+
+export function processTemplate(template, encodeFn) {
+ if (typeof template !== 'string') {
+ if (!template || !Array.isArray(template.parameters) || !(template.template instanceof Function)) {
+ throw new Error('Invalid template');
+ }
+ return template;
+ }
+ const templateSplit = template.split(parameterRegExp);
+ const templateSplitLength = templateSplit.length;
+ const parameters = [];
+ // odd indexes contain variable names
+ for (let i = 1; i < templateSplitLength; i += 2) {
+ const param = templateSplit[i];
+ let paramIndex = parameters.indexOf(param);
+ if (paramIndex === -1) {
+ paramIndex = parameters.length;
+ parameters.push(param);
+ }
+ templateSplit[i] = paramIndex;
+ }
+ return {
+ parameters,
+ template: (...args) => {
+ let text = '';
+ for (let i = 0; i < templateSplitLength; i++) {
+ if (i % 2 === 0) {
+ text += templateSplit[i];
+ } else {
+ text += encodeFn(args[templateSplit[i]]);
+ }
+ }
+ return text;
+ },
+ };
+}
+
+export function createTestFunction(entry) {
+ if (typeof entry === 'string') {
+ return arg => arg === entry;
+ }
+ if (Array.isArray(entry)) {
+ return arg => entry.indexOf(arg) > -1;
+ }
+ if (entry instanceof RegExp) {
+ return arg => entry.test(arg);
+ }
+ if (entry instanceof Function) {
+ return entry;
+ }
+ if (!entry) {
+ return () => true;
+ }
+ throw new Error(`Invalid value: ${entry}`);
+}
+
+const identity = a => a;
+
+export function processLinkPattern(pattern) {
+ 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, array) {
+ if (array) {
+ return array.find(entry => entry.key === name);
+ }
+ return null;
+}
+
+export function getParameterInAncestor(name, spans, startSpanIndex) {
+ let currentSpan = { depth: spans[startSpanIndex].depth + 1 };
+ for (let spanIndex = startSpanIndex; spanIndex >= 0; spanIndex--) {
+ const nextSpan = spans[spanIndex];
+ if (nextSpan.depth < currentSpan.depth) {
+ currentSpan = nextSpan;
+ const result =
+ getParameterInArray(name, currentSpan.tags) || getParameterInArray(name, currentSpan.process.tags);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ return null;
+}
+
+export function callTemplate(template, data) {
+ return template.template(...template.parameters.map(param => data[param]));
+}
+
+export function computeLinks(linkPatterns, trace, spanIndex, items, itemIndex) {
+ const item = items[itemIndex];
+ const span = trace.spans[spanIndex];
+ 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, item.value, type) && pattern.value(item.value)) {
+ let parameterValues = {};
+ 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
+ entry = getParameterInAncestor(parameter, trace.spans, spanIndex);
+ }
+ 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
+ );
+ parameterValues = null;
+ return false;
+ });
+ if (parameterValues) {
+ result.push({
+ url: callTemplate(pattern.url, parameterValues),
+ text: callTemplate(pattern.text, parameterValues),
+ });
+ }
+ }
+ });
+ return result;
+}
+
+const linkPatterns = (getConfigValue('linkPatterns') || []).map(processLinkPattern).filter(value => !!value);
+const alreadyComputed = new WeakMap();
+
+export default function getLinks(trace, spanIndex, items, itemIndex) {
+ if (linkPatterns.length === 0) {
+ return [];
+ }
+ const item = items[itemIndex];
+ let result = alreadyComputed.get(item);
+ if (!result) {
+ result = computeLinks(linkPatterns, trace, spanIndex, items, itemIndex);
+ alreadyComputed.set(item, result);
+ }
+ return result;
+}