mirror of
https://github.com/grafana/grafana.git
synced 2025-01-24 15:27:01 -06:00
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:
parent
e7d4b175b7
commit
e4f0d269f4
@ -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>
|
||||
|
@ -52,7 +52,7 @@ export const getStyles = createStyle((theme: Theme) => {
|
||||
background: ${autoColor(theme, '#f5f5f5')};
|
||||
}
|
||||
&:not(:hover) .${copyIconClassName} {
|
||||
display: none;
|
||||
visibility: hidden;
|
||||
}
|
||||
`,
|
||||
keyColumn: css`
|
||||
|
@ -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}
|
||||
|
@ -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', () => {
|
||||
|
@ -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}
|
||||
|
@ -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';
|
||||
|
17
packages/jaeger-ui-components/src/constants/tag-keys.tsx
Normal file
17
packages/jaeger-ui-components/src/constants/tag-keys.tsx
Normal 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';
|
61
packages/jaeger-ui-components/src/utils/date.test.js
Normal file
61
packages/jaeger-ui-components/src/utils/date.test.js
Normal 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');
|
||||
});
|
||||
});
|
@ -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) {
|
||||
|
@ -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([]));
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user