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 = ( + + {links.map((link, index) => { + const { text, url } = link; + return ( + // `index` is necessary in the key because url can repeat + // eslint-disable-next-line react/no-array-index-key + + + {text} + + + ); + })} + + ); + valueMarkup = ( + + + + {jsonTable} + + + ); + } return ( // `i` is necessary in the key because row.key can repeat // eslint-disable-next-line react/no-array-index-key
- + ); })} 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; +}
{row.key}{jsonTable}{valueMarkup}