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
This commit is contained in:
Connor Lindsey 2021-08-02 06:28:20 -06:00 committed by GitHub
parent e7d4b175b7
commit e4f0d269f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 169 additions and 15 deletions

View File

@ -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<SpanBarRowProps> {
isMatchingFilter,
numTicks,
rpc,
noInstrumentedServer,
showErrorIcon,
getViewedBounds,
traceStartTime,
@ -422,6 +429,13 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
{rpc.serviceName}
</span>
)}
{noInstrumentedServer && (
<span>
<IoArrowRightA />{' '}
<i className={styles.rpcColorMarker} style={{ background: noInstrumentedServer.color }} />
{noInstrumentedServer.serviceName}
</span>
)}
</span>
<small className={styles.endpointName}>{rpc ? rpc.operationName : operationName}</small>
</a>

View File

@ -52,7 +52,7 @@ export const getStyles = createStyle((theme: Theme) => {
background: ${autoColor(theme, '#f5f5f5')};
}
&:not(:hover) .${copyIconClassName} {
display: none;
visibility: hidden;
}
`,
keyColumn: css`

View File

@ -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') && (
<AccordianReferences
data={references}
isOpen={isReferencesOpen}

View File

@ -350,6 +350,23 @@ describe('<VirtualizedTraceViewImpl>', () => {
)
).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', () => {

View File

@ -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<VirtualizedTra
};
}
}
const peerServiceKV = span.tags.find((kv) => 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 (
<div className={styles.row} key={key} style={style} {...attrs}>
@ -375,6 +389,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
onDetailToggled={detailToggle}
onChildrenToggled={childrenToggle}
rpc={rpc}
noInstrumentedServer={noInstrumentedServer}
showErrorIcon={showErrorIcon}
getViewedBounds={this.getViewedBounds}
traceStartTime={trace.startTime}

View File

@ -112,4 +112,7 @@ export function findServerChildSpan(spans: TraceSpan[]) {
return null;
}
export const isKindClient = (span: TraceSpan): Boolean =>
span.tags.some(({ key, value }) => key === 'span.kind' && value === 'client');
export { formatDuration } from '../utils/date';

View File

@ -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';

View File

@ -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');
});
});

View File

@ -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) {

View File

@ -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([]));
});
});

View File

@ -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);