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:
@@ -278,6 +278,12 @@ type SpanBarRowProps = {
|
|||||||
serviceName: string;
|
serviceName: string;
|
||||||
}
|
}
|
||||||
| TNil;
|
| TNil;
|
||||||
|
noInstrumentedServer?:
|
||||||
|
| {
|
||||||
|
color: string;
|
||||||
|
serviceName: string;
|
||||||
|
}
|
||||||
|
| TNil;
|
||||||
showErrorIcon: boolean;
|
showErrorIcon: boolean;
|
||||||
getViewedBounds: ViewedBoundsFunctionType;
|
getViewedBounds: ViewedBoundsFunctionType;
|
||||||
traceStartTime: number;
|
traceStartTime: number;
|
||||||
@@ -326,6 +332,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
|
|||||||
isMatchingFilter,
|
isMatchingFilter,
|
||||||
numTicks,
|
numTicks,
|
||||||
rpc,
|
rpc,
|
||||||
|
noInstrumentedServer,
|
||||||
showErrorIcon,
|
showErrorIcon,
|
||||||
getViewedBounds,
|
getViewedBounds,
|
||||||
traceStartTime,
|
traceStartTime,
|
||||||
@@ -422,6 +429,13 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
|
|||||||
{rpc.serviceName}
|
{rpc.serviceName}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
{noInstrumentedServer && (
|
||||||
|
<span>
|
||||||
|
<IoArrowRightA />{' '}
|
||||||
|
<i className={styles.rpcColorMarker} style={{ background: noInstrumentedServer.color }} />
|
||||||
|
{noInstrumentedServer.serviceName}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
<small className={styles.endpointName}>{rpc ? rpc.operationName : operationName}</small>
|
<small className={styles.endpointName}>{rpc ? rpc.operationName : operationName}</small>
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const getStyles = createStyle((theme: Theme) => {
|
|||||||
background: ${autoColor(theme, '#f5f5f5')};
|
background: ${autoColor(theme, '#f5f5f5')};
|
||||||
}
|
}
|
||||||
&:not(:hover) .${copyIconClassName} {
|
&:not(:hover) .${copyIconClassName} {
|
||||||
display: none;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
keyColumn: css`
|
keyColumn: css`
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ export default function SpanDetail(props: SpanDetailProps) {
|
|||||||
onToggle={() => stackTracesToggle(spanID)}
|
onToggle={() => stackTracesToggle(spanID)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{references && references.length > 1 && (
|
{references && references.length > 0 && (references.length > 1 || references[0].refType !== 'CHILD_OF') && (
|
||||||
<AccordianReferences
|
<AccordianReferences
|
||||||
data={references}
|
data={references}
|
||||||
isOpen={isReferencesOpen}
|
isOpen={isReferencesOpen}
|
||||||
|
|||||||
@@ -350,6 +350,23 @@ describe('<VirtualizedTraceViewImpl>', () => {
|
|||||||
)
|
)
|
||||||
).toBe(true);
|
).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', () => {
|
describe('shouldScrollToFirstUiFindMatch', () => {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
createViewedBoundsFunc,
|
createViewedBoundsFunc,
|
||||||
findServerChildSpan,
|
findServerChildSpan,
|
||||||
isErrorSpan,
|
isErrorSpan,
|
||||||
|
isKindClient,
|
||||||
spanContainsErredSpan,
|
spanContainsErredSpan,
|
||||||
ViewedBoundsFunctionType,
|
ViewedBoundsFunctionType,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
@@ -31,6 +32,7 @@ import { getColorByKey } from '../utils/color-generator';
|
|||||||
import { TNil } from '../types';
|
import { TNil } from '../types';
|
||||||
import { TraceLog, TraceSpan, Trace, TraceKeyValuePair, TraceLink } from '../types/trace';
|
import { TraceLog, TraceSpan, Trace, TraceKeyValuePair, TraceLink } from '../types/trace';
|
||||||
import TTraceTimeline from '../types/TTraceTimeline';
|
import TTraceTimeline from '../types/TTraceTimeline';
|
||||||
|
import { PEER_SERVICE } from '../constants/tag-keys';
|
||||||
|
|
||||||
import { createStyle, Theme, withTheme } from '../Theme';
|
import { createStyle, Theme, withTheme } from '../Theme';
|
||||||
import { CreateSpanLink } from './types';
|
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();
|
const styles = getStyles();
|
||||||
return (
|
return (
|
||||||
<div className={styles.row} key={key} style={style} {...attrs}>
|
<div className={styles.row} key={key} style={style} {...attrs}>
|
||||||
@@ -375,6 +389,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
|
|||||||
onDetailToggled={detailToggle}
|
onDetailToggled={detailToggle}
|
||||||
onChildrenToggled={childrenToggle}
|
onChildrenToggled={childrenToggle}
|
||||||
rpc={rpc}
|
rpc={rpc}
|
||||||
|
noInstrumentedServer={noInstrumentedServer}
|
||||||
showErrorIcon={showErrorIcon}
|
showErrorIcon={showErrorIcon}
|
||||||
getViewedBounds={this.getViewedBounds}
|
getViewedBounds={this.getViewedBounds}
|
||||||
traceStartTime={trace.startTime}
|
traceStartTime={trace.startTime}
|
||||||
|
|||||||
@@ -112,4 +112,7 @@ export function findServerChildSpan(spans: TraceSpan[]) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const isKindClient = (span: TraceSpan): Boolean =>
|
||||||
|
span.tags.some(({ key, value }) => key === 'span.kind' && value === 'client');
|
||||||
|
|
||||||
export { formatDuration } from '../utils/date';
|
export { formatDuration } from '../utils/date';
|
||||||
|
|||||||
@@ -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';
|
||||||
@@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { round as _round } from 'lodash';
|
import { round as _round, dropWhile as _dropWhile } from 'lodash';
|
||||||
|
|
||||||
import { toFloatPrecision } from './number';
|
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 STANDARD_DATETIME_FORMAT = 'MMMM D YYYY, HH:mm:ss.SSS';
|
||||||
export const ONE_MILLISECOND = 1000;
|
export const ONE_MILLISECOND = 1000;
|
||||||
export const ONE_SECOND = 1000 * ONE_MILLISECOND;
|
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);
|
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} timestamp
|
||||||
* @param {number} initialTimestamp
|
* @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:
|
* Example:
|
||||||
* 5000ms => 5s
|
* 5000ms => 5s
|
||||||
* 1000μs => 1ms
|
* 1000μs => 1ms
|
||||||
|
* 183840s => 2d 3h
|
||||||
|
*
|
||||||
|
* @param {number} duration (in microseconds)
|
||||||
|
* @return {string} formatted duration
|
||||||
*/
|
*/
|
||||||
export function formatDuration(duration: number, inputUnit = 'microseconds'): string {
|
export function formatDuration(duration: number): string {
|
||||||
let d = duration;
|
// Drop all units that are too large except the last one
|
||||||
if (inputUnit === 'microseconds') {
|
const [primaryUnit, secondaryUnit] = _dropWhile(
|
||||||
d = duration / 1000;
|
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) {
|
const primaryValue = Math.floor(duration / primaryUnit.microseconds);
|
||||||
units = 's';
|
const primaryUnitString = `${primaryValue}${primaryUnit.unit}`;
|
||||||
d /= 1000;
|
const secondaryValue = Math.round((duration / secondaryUnit.microseconds) % primaryUnit.ofPrevious);
|
||||||
}
|
const secondaryUnitString = `${secondaryValue}${secondaryUnit.unit}`;
|
||||||
return _round(d, 2) + units;
|
return secondaryValue === 0 ? primaryUnitString : `${primaryUnitString} ${secondaryUnitString}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function formatRelativeDate(value: any, fullMonthName = false) {
|
export function formatRelativeDate(value: any, fullMonthName = false) {
|
||||||
|
|||||||
@@ -181,4 +181,9 @@ describe('filterSpans', () => {
|
|||||||
it('should return an empty set if no spans match the filter', () => {
|
it('should return an empty set if no spans match the filter', () => {
|
||||||
expect(filterSpans('-processTagKey1', spans)).toEqual(new Set());
|
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([]));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ export default function filterSpans(textFilter: string, spans: TraceSpan[] | TNi
|
|||||||
isTextInFilters(includeFilters, span.operationName) ||
|
isTextInFilters(includeFilters, span.operationName) ||
|
||||||
isTextInFilters(includeFilters, span.process.serviceName) ||
|
isTextInFilters(includeFilters, span.process.serviceName) ||
|
||||||
isTextInKeyValues(span.tags) ||
|
isTextInKeyValues(span.tags) ||
|
||||||
span.logs.some((log) => isTextInKeyValues(log.fields)) ||
|
(span.logs !== null && span.logs.some((log) => isTextInKeyValues(log.fields))) ||
|
||||||
isTextInKeyValues(span.process.tags) ||
|
isTextInKeyValues(span.process.tags) ||
|
||||||
includeFilters.some((filter) => filter === span.spanID);
|
includeFilters.some((filter) => filter === span.spanID);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user