From ac17f3f8b22dd1251c27433b563344d15b411fc8 Mon Sep 17 00:00:00 2001 From: Ruben Vargas Date: Fri, 3 May 2019 01:08:21 -0500 Subject: [PATCH] Deduplicate tags for spans Signed-off-by: Ruben Vargas --- .../SpanDetail/AccordianLogs.css | 1 + .../SpanDetail/AccordianLogs.tsx | 2 +- .../SpanDetail/AccordianText.css | 40 ++++++++++ .../SpanDetail/AccordianText.test.js | 63 ++++++++++++++++ .../SpanDetail/AccordianText.tsx | 75 +++++++++++++++++++ .../SpanDetail/DetailState.tsx | 11 ++- .../SpanDetail/TextList.css | 40 ++++++++++ .../SpanDetail/TextList.test.js | 37 +++++++++ .../SpanDetail/TextList.tsx | 42 +++++++++++ .../TraceTimelineViewer/SpanDetail/index.css | 7 ++ .../SpanDetail/index.test.js | 19 +++++ .../TraceTimelineViewer/SpanDetail/index.tsx | 21 +++++- .../TraceTimelineViewer/SpanDetailRow.tsx | 3 + .../VirtualizedTraceView.tsx | 3 + .../TraceTimelineViewer/duck.test.js | 7 ++ .../TracePage/TraceTimelineViewer/duck.tsx | 8 +- .../src/model/transform-trace-data.tsx | 25 ++++++- packages/jaeger-ui/src/types/trace.tsx | 1 + 18 files changed, 398 insertions(+), 7 deletions(-) create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.css create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.test.js create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.tsx create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.css create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.test.js create mode 100644 packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.tsx diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.css index 4af98f1b45..937d42c735 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.css @@ -24,6 +24,7 @@ limitations under the License. color: inherit; display: block; padding: 0.25rem 0.5rem; + padding-left: 0; } .AccordianLogs--header:hover { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx index 58a5af9a5f..4560eb6e9e 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianLogs.tsx @@ -56,7 +56,7 @@ export default function AccordianLogs(props: AccordianLogsProps) { } return ( -
+
{arrow} Logs ({logs.length}) diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.css new file mode 100644 index 0000000000..87c3563dbc --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.css @@ -0,0 +1,40 @@ +/* +Copyright (c) 2017 Uber Technologies, Inc. + +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. +*/ + +.AccordianText--header { + cursor: pointer; + overflow: hidden; + padding: 0.25em 0.1em; + text-overflow: ellipsis; + white-space: nowrap; +} + +.AccordianText--header:hover { + background: #e8e8e8; +} + +.AccordianText--header.is-empty { + background: none; + cursor: initial; +} + +.AccordianText--header.is-high-contrast:not(.is-empty):hover { + background: #ddd; +} + +.AccordianText--emptyIcon { + color: #aaa; +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.test.js new file mode 100644 index 0000000000..a506125997 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.test.js @@ -0,0 +1,63 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// 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 React from 'react'; +import { shallow } from 'enzyme'; +import IoIosWarning from 'react-icons/lib/io/android-warning'; + +import AccordianText from './AccordianText'; +import TextList from './TextList'; + +const warnings = ['Duplicated tag', 'Duplicated spanId']; + +describe('', () => { + let wrapper; + + const props = { + compact: false, + data: warnings, + highContrast: false, + isOpen: false, + label: 'le-label', + onToggle: jest.fn(), + }; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders without exploding', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.exists()).toBe(true); + }); + + it('renders the label', () => { + const header = wrapper.find(`.AccordianText--header > strong`); + expect(header.length).toBe(1); + expect(header.text()).toBe(`${props.label}:`); + }); + + it('renders the table instead of the summarywhen it is expanded', () => { + wrapper.setProps({ isOpen: true }); + const table = wrapper.find(TextList); + expect(table.length).toBe(1); + expect(table.prop('data')).toBe(warnings); + }); + + it('renders prefix icon', () => { + const prefixIcon = ; + wrapper.setProps({ isOpen: true, prefixIcon }); + expect(wrapper.find(IoIosWarning).length).toBe(1); + }); +}); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.tsx new file mode 100644 index 0000000000..d195e4eee8 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/AccordianText.tsx @@ -0,0 +1,75 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// 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 * as React from 'react'; +import cx from 'classnames'; +import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down'; +import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right'; +import TextList from './TextList'; +import { TNil } from '../../../../types'; + +import './AccordianText.css'; + +type AccordianTextProps = { + className?: string | TNil; + data: string[]; + highContrast?: boolean; + interactive?: boolean; + isOpen: boolean; + label: string; + onToggle?: null | (() => void); + prefixIcon?: null | React.ReactElement; +}; + +export default function AccordianText(props: AccordianTextProps) { + const { className, data, highContrast, interactive, isOpen, label, onToggle, prefixIcon } = props; + const isEmpty = !Array.isArray(data) || !data.length; + const iconCls = cx('u-align-icon', { 'AccordianKeyValues--emptyIcon': isEmpty }); + let arrow: React.ReactNode | null = null; + let headerProps: Object | null = null; + if (interactive) { + arrow = isOpen ? : ; + headerProps = { + 'aria-checked': isOpen, + onClick: isEmpty ? null : onToggle, + role: 'switch', + }; + } + return ( +
+
+ {arrow} + {React.isValidElement(prefixIcon) && prefixIcon} + + {label} + {isOpen || ':'} + +
+ {isOpen && } +
+ ); +} + +AccordianText.defaultProps = { + className: null, + highContrast: false, + interactive: true, + onToggle: null, +}; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.tsx index 22835c81a0..ed822aea1d 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/DetailState.tsx @@ -21,11 +21,14 @@ export default class DetailState { isTagsOpen: boolean; isProcessOpen: boolean; logs: { isOpen: boolean; openedItems: Set }; + isWarningsOpen: boolean; constructor(oldState?: DetailState) { - const { isTagsOpen, isProcessOpen, logs }: DetailState | Record = oldState || {}; + const { isTagsOpen, isProcessOpen, isWarningsOpen, logs }: DetailState | Record = + oldState || {}; this.isTagsOpen = Boolean(isTagsOpen); this.isProcessOpen = Boolean(isProcessOpen); + this.isWarningsOpen = Boolean(isWarningsOpen); this.logs = { isOpen: Boolean(logs && logs.isOpen), openedItems: logs && logs.openedItems ? new Set(logs.openedItems) : new Set(), @@ -44,6 +47,12 @@ export default class DetailState { return next; } + toggleWarnings() { + const next = new DetailState(this); + next.isWarningsOpen = !this.isWarningsOpen; + return next; + } + toggleLogs() { const next = new DetailState(this); next.logs.isOpen = !this.logs.isOpen; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.css new file mode 100644 index 0000000000..2796dfae50 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.css @@ -0,0 +1,40 @@ +/* +Copyright (c) 2017 Uber Technologies, Inc. + +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. +*/ + +.TextList { + background: #fff; + border: 1px solid #ddd; + margin-bottom: 0.7em; + max-height: 450px; + overflow: auto; +} + +.TextList-body { + vertical-align: baseline; +} + +.TextList--row > td { + padding: 0.25rem 0.5rem; +} + +.TextList-row:nth-child(2n) > td { + background: #f5f5f5; +} + +.TextList--row > td { + padding: 0.25rem 0.5rem; + vertical-align: top; +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.test.js new file mode 100644 index 0000000000..f7578b6b2e --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.test.js @@ -0,0 +1,37 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// 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 React from 'react'; +import { shallow } from 'enzyme'; +import TextList from './TextList'; + +describe('', () => { + let wrapper; + + const data = [{ key: 'span.kind', value: 'client' }, { key: 'omg', value: 'mos-def' }]; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('renders without exploding', () => { + expect(wrapper).toBeDefined(); + expect(wrapper.find('.TextList').length).toBe(1); + }); + + it('renders a table row for each data element', () => { + const trs = wrapper.find('tr'); + expect(trs.length).toBe(data.length); + }); +}); diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.tsx new file mode 100644 index 0000000000..c5addca423 --- /dev/null +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/TextList.tsx @@ -0,0 +1,42 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// 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 * as React from 'react'; + +import './TextList.css'; + +type TextListProps = { + data: string[]; +}; + +export default function TextList(props: TextListProps) { + const { data } = props; + return ( +
+ + + {data.map((row, i) => { + return ( + // `i` is necessary in the key because row.key can repeat + // eslint-disable-next-line react/no-array-index-key + + + + ); + })} + +
{row}
+
+ ); +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.css index f7c0b19e57..e557fd1505 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.css @@ -40,3 +40,10 @@ limitations under the License. .SpanDetail--debugValue:hover { color: #333; } + +.SpanDetail--warnIcon { + background: transparent; + color: #ffa500; + font-size: 1em; + margin-right: 0.2rem; +} diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js index 3af5e7907a..898f29ee51 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.test.js @@ -13,6 +13,7 @@ // limitations under the License. /* eslint-disable import/first */ + jest.mock('../utils'); import React from 'react'; @@ -20,6 +21,8 @@ import { shallow } from 'enzyme'; import AccordianKeyValues from './AccordianKeyValues'; import AccordianLogs from './AccordianLogs'; +import AccordianText from './AccordianText'; + import DetailState from './DetailState'; import SpanDetail from './index'; import { formatDuration } from '../utils'; @@ -46,6 +49,7 @@ describe('', () => { logsToggle: jest.fn(), processToggle: jest.fn(), tagsToggle: jest.fn(), + warningsToggle: jest.fn(), }; span.logs = [ { @@ -58,6 +62,8 @@ describe('', () => { }, ]; + span.warnings = ['Warning 1', 'Warning 2']; + beforeEach(() => { formatDuration.mockReset(); props.tagsToggle.mockReset(); @@ -120,6 +126,19 @@ describe('', () => { expect(props.logItemToggle).toHaveBeenLastCalledWith(span.spanID, somethingUniq); }); + it('renders the warnings', () => { + const target = ( + + ); + expect(wrapper.containsMatchingElement(target)).toBe(true); + wrapper.find({ data: span.warnings }).simulate('toggle'); + expect(props.warningsToggle).toHaveBeenLastCalledWith(span.spanID); + }); + it('renders CopyIcon with deep link URL', () => { expect( wrapper diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx index 3766b62d52..86075a2799 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/SpanDetail/index.tsx @@ -14,16 +14,18 @@ import React from 'react'; import { Divider } from 'antd'; +import IoIosWarning from 'react-icons/lib/io/android-warning'; import AccordianKeyValues from './AccordianKeyValues'; import AccordianLogs from './AccordianLogs'; +import AccordianText from './AccordianText'; import DetailState from './DetailState'; import { formatDuration } from '../utils'; import CopyIcon from '../../../common/CopyIcon'; import LabeledList from '../../../common/LabeledList'; import { TNil } from '../../../../types'; -import { Log, Span, KeyValuePair, Link } from '../../../../types/trace'; +import { KeyValuePair, Link, Log, Span } from '../../../../types/trace'; import './index.css'; @@ -36,6 +38,7 @@ type SpanDetailProps = { span: Span; tagsToggle: (spanID: string) => void; traceStartTime: number; + warningsToggle: (spanID: string) => void; }; export default function SpanDetail(props: SpanDetailProps) { @@ -48,9 +51,10 @@ export default function SpanDetail(props: SpanDetailProps) { span, tagsToggle, traceStartTime, + warningsToggle, } = props; - const { isTagsOpen, isProcessOpen, logs: logsState } = detailState; - const { operationName, process, duration, relativeStartTime, spanID, logs, tags } = span; + const { isTagsOpen, isProcessOpen, logs: logsState, isWarningsOpen } = detailState; + const { operationName, process, duration, relativeStartTime, spanID, logs, tags, warnings } = span; const overviewItems = [ { key: 'svc', @@ -113,6 +117,17 @@ export default function SpanDetail(props: SpanDetailProps) { timestamp={traceStartTime} /> )} + {warnings && + warnings.length > 0 && ( + warningsToggle(spanID)} + prefixIcon={} + /> + )} {spanID} void; logsToggle: (spanID: string) => void; processToggle: (spanID: string) => void; + warningsToggle: (spanID: string) => void; span: Span; tagsToggle: (spanID: string) => void; traceStartTime: number; @@ -55,6 +56,7 @@ export default class SpanDetailRow extends React.PureComponent void; detailLogItemToggle: (spanID: string, log: Log) => void; detailLogsToggle: (spanID: string) => void; + detailWarningsToggle: (spanID: string) => void; detailProcessToggle: (spanID: string) => void; detailTagsToggle: (spanID: string) => void; detailToggle: (spanID: string) => void; @@ -371,6 +372,7 @@ export class VirtualizedTraceViewImpl extends React.Component { unchecked: new DetailState(), checked: baseDetail.toggleLogs(), }, + { + msg: 'toggles warnings', + action: actions.detailWarningsToggle(id), + get: state => state.detailStates.get(id), + unchecked: new DetailState(), + checked: baseDetail.toggleWarnings(), + }, ]; beforeEach(() => { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.tsx index b5741442f2..9d641ab93c 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/duck.tsx @@ -74,6 +74,7 @@ export const actionTypes = generateActionTypes('@jaeger-ui/trace-timeline-viewer 'REMOVE_HOVER_INDENT_GUIDE_ID', 'SET_SPAN_NAME_COLUMN_WIDTH', 'SET_TRACE', + 'DETAIL_WARNINGS_TOGGLE', ]); const fullActions = createActions({ @@ -87,6 +88,7 @@ const fullActions = createActions({ [actionTypes.EXPAND_ALL]: () => ({}), [actionTypes.EXPAND_ONE]: (spans: Span[]) => ({ spans }), [actionTypes.DETAIL_PROCESS_TOGGLE]: (spanID: string) => ({ spanID }), + [actionTypes.DETAIL_WARNINGS_TOGGLE]: (spanID: string) => ({ spanID }), [actionTypes.DETAIL_TAGS_TOGGLE]: (spanID: string) => ({ spanID }), [actionTypes.DETAIL_TOGGLE]: (spanID: string) => ({ spanID }), [actionTypes.FOCUS_UI_FIND_MATCHES]: (trace: Trace, uiFind: string | TNil) => ({ trace, uiFind }), @@ -237,7 +239,7 @@ function detailToggle(state: TTraceTimeline, { spanID }: TSpanIdValue) { } function detailSubsectionToggle( - subSection: 'tags' | 'process' | 'logs', + subSection: 'tags' | 'process' | 'logs' | 'warnings', state: TTraceTimeline, { spanID }: TSpanIdValue ) { @@ -250,6 +252,8 @@ function detailSubsectionToggle( detailState = old.toggleTags(); } else if (subSection === 'process') { detailState = old.toggleProcess(); + } else if (subSection === 'warnings') { + detailState = old.toggleWarnings(); } else { detailState = old.toggleLogs(); } @@ -261,6 +265,7 @@ function detailSubsectionToggle( const detailTagsToggle = detailSubsectionToggle.bind(null, 'tags'); const detailProcessToggle = detailSubsectionToggle.bind(null, 'process'); const detailLogsToggle = detailSubsectionToggle.bind(null, 'logs'); +const detailWarningsToggle = detailSubsectionToggle.bind(null, 'warnings'); function detailLogItemToggle(state: TTraceTimeline, { spanID, logItem }: TSpanIdLogValue) { const old = state.detailStates.get(spanID); @@ -299,6 +304,7 @@ export default handleActions( [actionTypes.DETAIL_LOGS_TOGGLE]: guardReducer(detailLogsToggle), [actionTypes.DETAIL_LOG_ITEM_TOGGLE]: guardReducer(detailLogItemToggle), [actionTypes.DETAIL_PROCESS_TOGGLE]: guardReducer(detailProcessToggle), + [actionTypes.DETAIL_WARNINGS_TOGGLE]: guardReducer(detailWarningsToggle), [actionTypes.DETAIL_TAGS_TOGGLE]: guardReducer(detailTagsToggle), [actionTypes.DETAIL_TOGGLE]: guardReducer(detailToggle), [actionTypes.EXPAND_ALL]: guardReducer(expandAll), diff --git a/packages/jaeger-ui/src/model/transform-trace-data.tsx b/packages/jaeger-ui/src/model/transform-trace-data.tsx index 78183e5904..fa8e67d6fa 100644 --- a/packages/jaeger-ui/src/model/transform-trace-data.tsx +++ b/packages/jaeger-ui/src/model/transform-trace-data.tsx @@ -15,10 +15,29 @@ import _isEqual from 'lodash/isEqual'; import { getTraceSpanIdsAsTree } from '../selectors/trace'; -import { Process, Span, SpanData, Trace, TraceData } from '../types/trace'; +import { KeyValuePair, Process, Span, SpanData, Trace, TraceData } from '../types/trace'; import TreeNode from '../utils/TreeNode'; type SpanWithProcess = SpanData & { process: Process }; +/* +{ + key: string; + value: any; +}; + */ +function deduplicateTags(spanTags: Array) { + const warningsHash: { [key: string]: string } = {}; + const tags: Array = spanTags.reduce>((uniqueTags, tag) => { + if (!uniqueTags.some(t => t.key === tag.key && t.value === tag.value)) { + uniqueTags.push(tag); + } else { + warningsHash[`${tag.key}:${tag.value}`] = `Duplicate tag "${tag.key}:${tag.value}"`; + } + return uniqueTags; + }, []); + const warnings = Object.values(warningsHash); + return { tags, warnings }; +} /** * NOTE: Mutates `data` - Transform the HTTP response data into the form the app @@ -93,6 +112,10 @@ export default function transformTraceData(data: TraceData & { spans: SpanWithPr span.relativeStartTime = span.startTime - traceStartTime; span.depth = depth - 1; span.hasChildren = node.children.length > 0; + const tagsInfo = deduplicateTags(span.tags); + span.tags = tagsInfo.tags; + span.warnings = span.warnings || []; + span.warnings = span.warnings.concat(tagsInfo.warnings); span.references.forEach(ref => { const refSpan = spanMap.get(ref.spanID) as Span; if (refSpan) { diff --git a/packages/jaeger-ui/src/types/trace.tsx b/packages/jaeger-ui/src/types/trace.tsx index 8fcc5db853..1f56c2b62e 100644 --- a/packages/jaeger-ui/src/types/trace.tsx +++ b/packages/jaeger-ui/src/types/trace.tsx @@ -54,6 +54,7 @@ export type SpanData = { logs: Array; tags: Array; references: Array; + warnings: Array; }; export type Span = SpanData & {