From e4f0d269f4fa070348fb264237ad89e4b80a6725 Mon Sep 17 00:00:00 2001 From: Connor Lindsey Date: Mon, 2 Aug 2021 06:28:20 -0600 Subject: [PATCH] Connor/jaeger misc sync (#37420) * Avoid resize on mouse hover (KeyValueTable) * Add null check for span.logs in filter-spans * Display references unless it's a single CHILD_OF * Identify uninstrumented services * Improve span duration formatting --- .../src/TraceTimelineViewer/SpanBarRow.tsx | 14 +++++ .../SpanDetail/KeyValuesTable.tsx | 2 +- .../TraceTimelineViewer/SpanDetail/index.tsx | 2 +- .../VirtualizedTraceView.test.js | 17 ++++++ .../VirtualizedTraceView.tsx | 15 +++++ .../src/TraceTimelineViewer/utils.tsx | 3 + .../src/constants/tag-keys.tsx | 17 ++++++ .../src/utils/date.test.js | 61 +++++++++++++++++++ .../jaeger-ui-components/src/utils/date.tsx | 46 ++++++++++---- .../src/utils/filter-spans.test.js | 5 ++ .../src/utils/filter-spans.tsx | 2 +- 11 files changed, 169 insertions(+), 15 deletions(-) create mode 100644 packages/jaeger-ui-components/src/constants/tag-keys.tsx create mode 100644 packages/jaeger-ui-components/src/utils/date.test.js diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx index ed18d978cb9..7c58421c741 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx @@ -278,6 +278,12 @@ type SpanBarRowProps = { serviceName: string; } | TNil; + noInstrumentedServer?: + | { + color: string; + serviceName: string; + } + | TNil; showErrorIcon: boolean; getViewedBounds: ViewedBoundsFunctionType; traceStartTime: number; @@ -326,6 +332,7 @@ export class UnthemedSpanBarRow extends React.PureComponent { isMatchingFilter, numTicks, rpc, + noInstrumentedServer, showErrorIcon, getViewedBounds, traceStartTime, @@ -422,6 +429,13 @@ export class UnthemedSpanBarRow extends React.PureComponent { {rpc.serviceName} )} + {noInstrumentedServer && ( + + {' '} + + {noInstrumentedServer.serviceName} + + )} {rpc ? rpc.operationName : operationName} diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx index c6ad98b0bb2..e9b4058d633 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/KeyValuesTable.tsx @@ -52,7 +52,7 @@ export const getStyles = createStyle((theme: Theme) => { background: ${autoColor(theme, '#f5f5f5')}; } &:not(:hover) .${copyIconClassName} { - display: none; + visibility: hidden; } `, keyColumn: css` diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx index 311823d8004..e80830bf16c 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx @@ -255,7 +255,7 @@ export default function SpanDetail(props: SpanDetailProps) { onToggle={() => stackTracesToggle(spanID)} /> )} - {references && references.length > 1 && ( + {references && references.length > 0 && (references.length > 1 || references[0].refType !== 'CHILD_OF') && ( ', () => { ) ).toBe(true); }); + + it('renders a SpanBarRow with a client span and no instrumented server span', () => { + const externServiceName = 'externalServiceTest'; + const leafSpan = trace.spans.find((span) => !span.hasChildren); + const leafSpanIndex = trace.spans.indexOf(leafSpan); + const clientTags = [ + { key: 'span.kind', value: 'client' }, + { key: 'peer.service', value: externServiceName }, + ...leafSpan.tags, + ]; + const altTrace = updateSpan(trace, leafSpanIndex, { tags: clientTags }); + wrapper.setProps({ trace: altTrace }); + const rowWrapper = mount(instance.renderRow('some-key', {}, leafSpanIndex, {})); + const spanBarRow = rowWrapper.find(SpanBarRow); + expect(spanBarRow.length).toBe(1); + expect(spanBarRow.prop('noInstrumentedServer')).not.toBeNull(); + }); }); describe('shouldScrollToFirstUiFindMatch', () => { diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx index 9c03faa695b..27cc406782a 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -23,6 +23,7 @@ import { createViewedBoundsFunc, findServerChildSpan, isErrorSpan, + isKindClient, spanContainsErredSpan, ViewedBoundsFunctionType, } from './utils'; @@ -31,6 +32,7 @@ import { getColorByKey } from '../utils/color-generator'; import { TNil } from '../types'; import { TraceLog, TraceSpan, Trace, TraceKeyValuePair, TraceLink } from '../types/trace'; import TTraceTimeline from '../types/TTraceTimeline'; +import { PEER_SERVICE } from '../constants/tag-keys'; import { createStyle, Theme, withTheme } from '../Theme'; import { CreateSpanLink } from './types'; @@ -360,6 +362,18 @@ export class UnthemedVirtualizedTraceView extends React.Component kv.key === PEER_SERVICE); + // Leaf, kind == client and has peer.service.tag, is likely a client span that does a request + // to an uninstrumented/external service + let noInstrumentedServer = null; + if (!span.hasChildren && peerServiceKV && isKindClient(span)) { + noInstrumentedServer = { + serviceName: peerServiceKV.value, + color: getColorByKey(peerServiceKV.value, theme), + }; + } + const styles = getStyles(); return (
@@ -375,6 +389,7 @@ export class UnthemedVirtualizedTraceView extends React.Component + span.tags.some(({ key, value }) => key === 'span.kind' && value === 'client'); + export { formatDuration } from '../utils/date'; diff --git a/packages/jaeger-ui-components/src/constants/tag-keys.tsx b/packages/jaeger-ui-components/src/constants/tag-keys.tsx new file mode 100644 index 00000000000..fb4587a547d --- /dev/null +++ b/packages/jaeger-ui-components/src/constants/tag-keys.tsx @@ -0,0 +1,17 @@ +// Copyright (c) 2018 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. + +export const HTTP_METHOD = 'http.method' as 'http.method'; +export const PEER_SERVICE = 'peer.service' as 'peer.service'; +export const SPAN_KIND = 'span.kind' as 'span.kind'; diff --git a/packages/jaeger-ui-components/src/utils/date.test.js b/packages/jaeger-ui-components/src/utils/date.test.js new file mode 100644 index 00000000000..64e54bb240f --- /dev/null +++ b/packages/jaeger-ui-components/src/utils/date.test.js @@ -0,0 +1,61 @@ +// Copyright (c) 2020 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 { formatDuration, ONE_MILLISECOND, ONE_SECOND, ONE_MINUTE, ONE_HOUR, ONE_DAY } from './date.tsx'; + +describe('formatDuration', () => { + it('keeps microseconds the same', () => { + expect(formatDuration(1)).toBe('1μs'); + }); + + it('displays a maximum of 2 units and rounds the last one', () => { + const input = 10 * ONE_DAY + 13 * ONE_HOUR + 30 * ONE_MINUTE; + expect(formatDuration(input)).toBe('10d 14h'); + }); + + it('skips units that are empty', () => { + const input = 2 * ONE_DAY + 5 * ONE_MINUTE; + expect(formatDuration(input)).toBe('2d'); + }); + + it('displays milliseconds in decimals', () => { + const input = 2 * ONE_MILLISECOND + 357; + expect(formatDuration(input)).toBe('2.36ms'); + }); + + it('displays seconds in decimals', () => { + const input = 2 * ONE_SECOND + 357 * ONE_MILLISECOND; + expect(formatDuration(input)).toBe('2.36s'); + }); + + it('displays minutes in split units', () => { + const input = 2 * ONE_MINUTE + 30 * ONE_SECOND + 555 * ONE_MILLISECOND; + expect(formatDuration(input)).toBe('2m 31s'); + }); + + it('displays hours in split units', () => { + const input = 2 * ONE_HOUR + 30 * ONE_MINUTE + 30 * ONE_SECOND; + expect(formatDuration(input)).toBe('2h 31m'); + }); + + it('displays times less than a μs', () => { + const input = 0.1; + expect(formatDuration(input)).toBe('0.1μs'); + }); + + it('displays times of 0', () => { + const input = 0; + expect(formatDuration(input)).toBe('0μs'); + }); +}); diff --git a/packages/jaeger-ui-components/src/utils/date.tsx b/packages/jaeger-ui-components/src/utils/date.tsx index b43fa354405..eb5aa6a795a 100644 --- a/packages/jaeger-ui-components/src/utils/date.tsx +++ b/packages/jaeger-ui-components/src/utils/date.tsx @@ -13,7 +13,7 @@ // limitations under the License. import moment from 'moment-timezone'; -import { round as _round } from 'lodash'; +import { round as _round, dropWhile as _dropWhile } from 'lodash'; import { toFloatPrecision } from './number'; @@ -25,8 +25,20 @@ export const STANDARD_TIME_FORMAT = 'HH:mm'; export const STANDARD_DATETIME_FORMAT = 'MMMM D YYYY, HH:mm:ss.SSS'; export const ONE_MILLISECOND = 1000; export const ONE_SECOND = 1000 * ONE_MILLISECOND; +export const ONE_MINUTE = 60 * ONE_SECOND; +export const ONE_HOUR = 60 * ONE_MINUTE; +export const ONE_DAY = 24 * ONE_HOUR; export const DEFAULT_MS_PRECISION = Math.log10(ONE_MILLISECOND); +const UNIT_STEPS: Array<{ unit: string; microseconds: number; ofPrevious: number }> = [ + { unit: 'd', microseconds: ONE_DAY, ofPrevious: 24 }, + { unit: 'h', microseconds: ONE_HOUR, ofPrevious: 60 }, + { unit: 'm', microseconds: ONE_MINUTE, ofPrevious: 60 }, + { unit: 's', microseconds: ONE_SECOND, ofPrevious: 1000 }, + { unit: 'ms', microseconds: ONE_MILLISECOND, ofPrevious: 1000 }, + { unit: 'μs', microseconds: 1, ofPrevious: 1000 }, +]; + /** * @param {number} timestamp * @param {number} initialTimestamp @@ -83,23 +95,33 @@ export function formatSecondTime(duration: number) { } /** - * Humanizes the duration based on the inputUnit + * Humanizes the duration for display. * * Example: * 5000ms => 5s * 1000μs => 1ms + * 183840s => 2d 3h + * + * @param {number} duration (in microseconds) + * @return {string} formatted duration */ -export function formatDuration(duration: number, inputUnit = 'microseconds'): string { - let d = duration; - if (inputUnit === 'microseconds') { - d = duration / 1000; +export function formatDuration(duration: number): string { + // Drop all units that are too large except the last one + const [primaryUnit, secondaryUnit] = _dropWhile( + UNIT_STEPS, + ({ microseconds }, index) => index < UNIT_STEPS.length - 1 && microseconds > duration + ); + + if (primaryUnit.ofPrevious === 1000) { + // If the unit is decimal based, display as a decimal + return `${_round(duration / primaryUnit.microseconds, 2)}${primaryUnit.unit}`; } - let units = 'ms'; - if (d >= 1000) { - units = 's'; - d /= 1000; - } - return _round(d, 2) + units; + + const primaryValue = Math.floor(duration / primaryUnit.microseconds); + const primaryUnitString = `${primaryValue}${primaryUnit.unit}`; + const secondaryValue = Math.round((duration / secondaryUnit.microseconds) % primaryUnit.ofPrevious); + const secondaryUnitString = `${secondaryValue}${secondaryUnit.unit}`; + return secondaryValue === 0 ? primaryUnitString : `${primaryUnitString} ${secondaryUnitString}`; } export function formatRelativeDate(value: any, fullMonthName = false) { diff --git a/packages/jaeger-ui-components/src/utils/filter-spans.test.js b/packages/jaeger-ui-components/src/utils/filter-spans.test.js index b8d61cc973b..275d8573cfa 100644 --- a/packages/jaeger-ui-components/src/utils/filter-spans.test.js +++ b/packages/jaeger-ui-components/src/utils/filter-spans.test.js @@ -181,4 +181,9 @@ describe('filterSpans', () => { it('should return an empty set if no spans match the filter', () => { expect(filterSpans('-processTagKey1', spans)).toEqual(new Set()); }); + + it('should return no spans when logs is null', () => { + const nullSpan = { ...span0, logs: null }; + expect(filterSpans('logFieldKey1', [nullSpan])).toEqual(new Set([])); + }); }); diff --git a/packages/jaeger-ui-components/src/utils/filter-spans.tsx b/packages/jaeger-ui-components/src/utils/filter-spans.tsx index 0eb1615d2fa..19c21254bcf 100644 --- a/packages/jaeger-ui-components/src/utils/filter-spans.tsx +++ b/packages/jaeger-ui-components/src/utils/filter-spans.tsx @@ -57,7 +57,7 @@ export default function filterSpans(textFilter: string, spans: TraceSpan[] | TNi isTextInFilters(includeFilters, span.operationName) || isTextInFilters(includeFilters, span.process.serviceName) || isTextInKeyValues(span.tags) || - span.logs.some((log) => isTextInKeyValues(log.fields)) || + (span.logs !== null && span.logs.some((log) => isTextInKeyValues(log.fields))) || isTextInKeyValues(span.process.tags) || includeFilters.some((filter) => filter === span.spanID);