Tracing: Adds header and minimap (#23315)

* Add integration with Jeager
Add Jaeger datasource and modify derived fields in loki to allow for opening a trace in Jager in separate split.
Modifies build so that this branch docker images are pushed to docker hub
Add a traceui dir with docker-compose and provision files for demoing.:wq

* Enable docker logger plugin to send logs to loki

* Add placeholder zipkin datasource

* Fixed rebase issues, added enhanceDataFrame to non-legacy code path

* Trace selector for jaeger query field

* Fix logs default mode for Loki

* Fix loading jaeger query field services on split

* Updated grafana image in traceui/compose file

* Fix prettier error

* Hide behind feature flag, clean up unused code.

* Fix tests

* Fix tests

* Cleanup code and review feedback

* Remove traceui directory

* Remove circle build changes

* Fix feature toggles object

* Fix merge issues

* Add trace ui in Explore

* WIP

* WIP

* WIP

* Make jaeger datasource return trace data instead of link

* Allow js in jest tests

* Return data from Jaeger datasource

* Take yarn.lock from master

* Fix missing component

* Update yarn lock

* Fix some ts and lint errors

* Fix merge

* Fix type errors

* Make tests pass again

* Add tests

* Fix es5 compatibility

* Add header with minimap

* Fix sizing issue due to column resizer handle

* Fix issues with sizing, search functionality, duplicate react, tests

* Refactor TraceView component, fix tests

* Fix type errors

* Add tests for hooks

Co-authored-by: David Kaltschmidt <david.kaltschmidt@gmail.com>
This commit is contained in:
Andrej Ocenas
2020-04-08 17:16:22 +02:00
committed by GitHub
parent d04dce6a37
commit 008bee8f27
61 changed files with 4608 additions and 529 deletions

View File

@@ -19,7 +19,7 @@ import { toggleGraph } from './state/actions';
import { Provider } from 'react-redux';
import { configureStore } from 'app/store/configureStore';
import { SecondaryActions } from './SecondaryActions';
import { TraceView } from './TraceView';
import { TraceView } from './TraceView/TraceView';
const dummyProps: ExploreProps = {
changeSize: jest.fn(),

View File

@@ -59,7 +59,7 @@ import { getTimeZone } from '../profile/state/selectors';
import { ErrorContainer } from './ErrorContainer';
import { scanStopAction } from './state/actionTypes';
import { ExploreGraphPanel } from './ExploreGraphPanel';
import { TraceView } from './TraceView';
import { TraceView } from './TraceView/TraceView';
import { SecondaryActions } from './SecondaryActions';
const getStyles = stylesFactory(() => {
@@ -73,18 +73,6 @@ const getStyles = stylesFactory(() => {
button: css`
margin: 1em 4px 0 0;
`,
// Utility class for iframe parents so that we can show iframe content with reasonable height instead of squished
// or some random explicit height.
fullHeight: css`
label: fullHeight;
height: 100%;
`,
iframe: css`
label: iframe;
border: none;
width: 100%;
height: 100%;
`,
};
});
@@ -330,22 +318,14 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
onClickRichHistoryButton={this.toggleShowRichHistory}
/>
<ErrorContainer queryError={queryError} />
<AutoSizer className={styles.fullHeight} onResize={this.onResize} disableHeight>
<AutoSizer onResize={this.onResize} disableHeight>
{({ width }) => {
if (width === 0) {
return null;
}
return (
<main
className={cx(
styles.logsMain,
// We need height to be 100% for tracing iframe to look good but in case of metrics mode
// it makes graph and table also full page high when they do not need to be.
mode === ExploreMode.Tracing && styles.fullHeight
)}
style={{ width }}
>
<main className={cx(styles.logsMain)} style={{ width }}>
<ErrorBoundaryAlert>
{showStartPage && StartPage && (
<div className={'grafana-info-box grafana-info-box--max-lg'}>

View File

@@ -1,182 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import { TraceView } from './TraceView';
import { SpanData, TraceData, TraceTimelineViewer } from '@jaegertracing/jaeger-ui-components';
describe('TraceView', () => {
it('renders TraceTimelineViewer', () => {
const wrapper = shallow(<TraceView trace={response} />);
expect(wrapper.find(TraceTimelineViewer)).toHaveLength(1);
});
it('toggles detailState', () => {
const wrapper = shallow(<TraceView trace={response} />);
let viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.detailStates.size).toBe(0);
viewer.props().detailToggle('1');
viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.detailStates.size).toBe(1);
expect(viewer.props().traceTimeline.detailStates.get('1')).not.toBeUndefined();
viewer.props().detailToggle('1');
viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.detailStates.size).toBe(0);
});
it('toggles children visibility', () => {
const wrapper = shallow(<TraceView trace={response} />);
let viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
viewer.props().childrenToggle('1');
viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(1);
expect(viewer.props().traceTimeline.childrenHiddenIDs.has('1')).toBeTruthy();
viewer.props().childrenToggle('1');
viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
});
it('toggles adds and removes hover indent guides', () => {
const wrapper = shallow(<TraceView trace={response} />);
let viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.hoverIndentGuideIds.size).toBe(0);
viewer.props().addHoverIndentGuideId('1');
viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.hoverIndentGuideIds.size).toBe(1);
expect(viewer.props().traceTimeline.hoverIndentGuideIds.has('1')).toBeTruthy();
viewer.props().removeHoverIndentGuideId('1');
viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.hoverIndentGuideIds.size).toBe(0);
});
it('toggles collapses and expands one level of spans', () => {
const wrapper = shallow(<TraceView trace={response} />);
let viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
const spans = viewer.props().trace.spans;
viewer.props().collapseOne(spans);
viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(1);
expect(viewer.props().traceTimeline.childrenHiddenIDs.has('3fb050342773d333')).toBeTruthy();
viewer.props().expandOne(spans);
viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
});
it('toggles collapses and expands all levels', () => {
const wrapper = shallow(<TraceView trace={response} />);
let viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
const spans = viewer.props().trace.spans;
viewer.props().collapseAll(spans);
viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(2);
expect(viewer.props().traceTimeline.childrenHiddenIDs.has('3fb050342773d333')).toBeTruthy();
expect(viewer.props().traceTimeline.childrenHiddenIDs.has('1ed38015486087ca')).toBeTruthy();
viewer.props().expandAll();
viewer = wrapper.find(TraceTimelineViewer);
expect(viewer.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
});
});
const response: TraceData & { spans: SpanData[] } = {
traceID: '1ed38015486087ca',
spans: [
{
traceID: '1ed38015486087ca',
spanID: '1ed38015486087ca',
flags: 1,
operationName: 'HTTP POST - api_prom_push',
references: [] as any,
startTime: 1585244579835187,
duration: 1098,
tags: [
{ key: 'sampler.type', type: 'string', value: 'const' },
{ key: 'sampler.param', type: 'bool', value: true },
{ key: 'span.kind', type: 'string', value: 'server' },
{ key: 'http.method', type: 'string', value: 'POST' },
{ key: 'http.url', type: 'string', value: '/api/prom/push' },
{ key: 'component', type: 'string', value: 'net/http' },
{ key: 'http.status_code', type: 'int64', value: 204 },
{ key: 'internal.span.format', type: 'string', value: 'proto' },
],
logs: [
{
timestamp: 1585244579835229,
fields: [{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[start reading]' }],
},
{
timestamp: 1585244579835241,
fields: [
{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[decompress]' },
{ key: 'size', type: 'int64', value: 315 },
],
},
{
timestamp: 1585244579835245,
fields: [
{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[unmarshal]' },
{ key: 'size', type: 'int64', value: 446 },
],
},
],
processID: 'p1',
warnings: null as any,
},
{
traceID: '1ed38015486087ca',
spanID: '3fb050342773d333',
flags: 1,
operationName: '/logproto.Pusher/Push',
references: [{ refType: 'CHILD_OF', traceID: '1ed38015486087ca', spanID: '1ed38015486087ca' }],
startTime: 1585244579835341,
duration: 921,
tags: [
{ key: 'span.kind', type: 'string', value: 'client' },
{ key: 'component', type: 'string', value: 'gRPC' },
{ key: 'internal.span.format', type: 'string', value: 'proto' },
],
logs: [],
processID: 'p1',
warnings: null,
},
{
traceID: '1ed38015486087ca',
spanID: '35118c298fc91f68',
flags: 1,
operationName: '/logproto.Pusher/Push',
references: [{ refType: 'CHILD_OF', traceID: '1ed38015486087ca', spanID: '3fb050342773d333' }],
startTime: 1585244579836040,
duration: 36,
tags: [
{ key: 'span.kind', type: 'string', value: 'server' },
{ key: 'component', type: 'string', value: 'gRPC' },
{ key: 'internal.span.format', type: 'string', value: 'proto' },
],
logs: [] as any,
processID: 'p1',
warnings: null as any,
},
],
processes: {
p1: {
serviceName: 'loki-all',
tags: [
{ key: 'client-uuid', type: 'string', value: '2a59d08899ef6a8a' },
{ key: 'hostname', type: 'string', value: '0080b530fae3' },
{ key: 'ip', type: 'string', value: '172.18.0.6' },
{ key: 'jaeger.version', type: 'string', value: 'Go-2.20.1' },
],
},
},
warnings: null as any,
};

View File

@@ -1,247 +0,0 @@
import {
DetailState,
KeyValuePair,
Link,
Log,
Span,
// SpanData,
// SpanReference,
Trace,
TraceTimelineViewer,
TTraceTimeline,
UIElementsContext,
ViewRangeTimeUpdate,
transformTraceData,
SpanData,
TraceData,
} from '@jaegertracing/jaeger-ui-components';
import React, { useState } from 'react';
type Props = {
trace: TraceData & { spans: SpanData[] };
};
export function TraceView(props: Props) {
/**
* Track whether details are open per span.
*/
const [detailStates, setDetailStates] = useState(new Map<string, DetailState>());
/**
* Track whether span is collapsed, meaning its children spans are hidden.
*/
const [childrenHiddenIDs, setChildrenHiddenIDs] = useState(new Set<string>());
/**
* For some reason this is used internally to handle hover state of indent guide. As indent guides are separate
* components per each row/span and you need to highlight all in multiple rows to make the effect of single line
* they need this kind of common imperative state changes.
*
* Ideally would be changed to trace view internal state.
*/
const [hoverIndentGuideIds, setHoverIndentGuideIds] = useState(new Set<string>());
/**
* Keeps state of resizable name column
*/
const [spanNameColumnWidth, setSpanNameColumnWidth] = useState(0.25);
function toggleDetail(spanID: string) {
const newDetailStates = new Map(detailStates);
if (newDetailStates.has(spanID)) {
newDetailStates.delete(spanID);
} else {
newDetailStates.set(spanID, new DetailState());
}
setDetailStates(newDetailStates);
}
function expandOne(spans: Span[]) {
if (childrenHiddenIDs.size === 0) {
return;
}
let prevExpandedDepth = -1;
let expandNextHiddenSpan = true;
const newChildrenHiddenIDs = spans.reduce((res, s) => {
if (s.depth <= prevExpandedDepth) {
expandNextHiddenSpan = true;
}
if (expandNextHiddenSpan && res.has(s.spanID)) {
res.delete(s.spanID);
expandNextHiddenSpan = false;
prevExpandedDepth = s.depth;
}
return res;
}, new Set(childrenHiddenIDs));
setChildrenHiddenIDs(newChildrenHiddenIDs);
}
function collapseOne(spans: Span[]) {
if (shouldDisableCollapse(spans, childrenHiddenIDs)) {
return;
}
let nearestCollapsedAncestor: Span | undefined;
const newChildrenHiddenIDs = spans.reduce((res, curSpan) => {
if (nearestCollapsedAncestor && curSpan.depth <= nearestCollapsedAncestor.depth) {
res.add(nearestCollapsedAncestor.spanID);
if (curSpan.hasChildren) {
nearestCollapsedAncestor = curSpan;
}
} else if (curSpan.hasChildren && !res.has(curSpan.spanID)) {
nearestCollapsedAncestor = curSpan;
}
return res;
}, new Set(childrenHiddenIDs));
// The last one
if (nearestCollapsedAncestor) {
newChildrenHiddenIDs.add(nearestCollapsedAncestor.spanID);
}
setChildrenHiddenIDs(newChildrenHiddenIDs);
}
function expandAll() {
setChildrenHiddenIDs(new Set<string>());
}
function collapseAll(spans: Span[]) {
if (shouldDisableCollapse(spans, childrenHiddenIDs)) {
return;
}
const newChildrenHiddenIDs = spans.reduce((res, s) => {
if (s.hasChildren) {
res.add(s.spanID);
}
return res;
}, new Set<string>());
setChildrenHiddenIDs(newChildrenHiddenIDs);
}
function childrenToggle(spanID: string) {
const newChildrenHiddenIDs = new Set(childrenHiddenIDs);
if (childrenHiddenIDs.has(spanID)) {
newChildrenHiddenIDs.delete(spanID);
} else {
newChildrenHiddenIDs.add(spanID);
}
setChildrenHiddenIDs(newChildrenHiddenIDs);
}
function detailLogItemToggle(spanID: string, log: Log) {
const old = detailStates.get(spanID);
if (!old) {
return;
}
const detailState = old.toggleLogItem(log);
const newDetailStates = new Map(detailStates);
newDetailStates.set(spanID, detailState);
return setDetailStates(newDetailStates);
}
function addHoverIndentGuideId(spanID: string) {
setHoverIndentGuideIds(prevState => {
const newHoverIndentGuideIds = new Set(prevState);
newHoverIndentGuideIds.add(spanID);
return newHoverIndentGuideIds;
});
}
function removeHoverIndentGuideId(spanID: string) {
setHoverIndentGuideIds(prevState => {
const newHoverIndentGuideIds = new Set(prevState);
newHoverIndentGuideIds.delete(spanID);
return newHoverIndentGuideIds;
});
}
const traceProp = transformTraceData(props.trace);
return (
<UIElementsContext.Provider
value={{
Popover: (() => null as any) as any,
Tooltip: (() => null as any) as any,
Icon: (() => null as any) as any,
Dropdown: (() => null as any) as any,
Menu: (() => null as any) as any,
MenuItem: (() => null as any) as any,
Button: (() => null as any) as any,
Divider: (() => null as any) as any,
}}
>
<TraceTimelineViewer
registerAccessors={() => {}}
scrollToFirstVisibleSpan={() => {}}
findMatchesIDs={null}
trace={traceProp}
traceTimeline={
{
childrenHiddenIDs,
detailStates,
hoverIndentGuideIds,
shouldScrollToFirstUiFindMatch: false,
spanNameColumnWidth,
traceID: '50b96206cf81dd64',
} as TTraceTimeline
}
updateNextViewRangeTime={(update: ViewRangeTimeUpdate) => {}}
updateViewRangeTime={() => {}}
viewRange={{ time: { current: [0, 1], cursor: null } }}
focusSpan={() => {}}
createLinkToExternalSpan={() => ''}
setSpanNameColumnWidth={setSpanNameColumnWidth}
collapseAll={collapseAll}
collapseOne={collapseOne}
expandAll={expandAll}
expandOne={expandOne}
childrenToggle={childrenToggle}
clearShouldScrollToFirstUiFindMatch={() => {}}
detailLogItemToggle={detailLogItemToggle}
detailLogsToggle={makeDetailSubsectionToggle('logs', detailStates, setDetailStates)}
detailWarningsToggle={makeDetailSubsectionToggle('warnings', detailStates, setDetailStates)}
detailReferencesToggle={makeDetailSubsectionToggle('references', detailStates, setDetailStates)}
detailProcessToggle={makeDetailSubsectionToggle('process', detailStates, setDetailStates)}
detailTagsToggle={makeDetailSubsectionToggle('tags', detailStates, setDetailStates)}
detailToggle={toggleDetail}
setTrace={(trace: Trace | null, uiFind: string | null) => {}}
addHoverIndentGuideId={addHoverIndentGuideId}
removeHoverIndentGuideId={removeHoverIndentGuideId}
linksGetter={(span: Span, items: KeyValuePair[], itemIndex: number) => [] as Link[]}
uiFind={undefined}
/>
</UIElementsContext.Provider>
);
}
function shouldDisableCollapse(allSpans: Span[], hiddenSpansIds: Set<string>) {
const allParentSpans = allSpans.filter(s => s.hasChildren);
return allParentSpans.length === hiddenSpansIds.size;
}
function makeDetailSubsectionToggle(
subSection: 'tags' | 'process' | 'logs' | 'warnings' | 'references',
detailStates: Map<string, DetailState>,
setDetailStates: (detailStates: Map<string, DetailState>) => void
) {
return (spanID: string) => {
const old = detailStates.get(spanID);
if (!old) {
return;
}
let detailState;
if (subSection === 'tags') {
detailState = old.toggleTags();
} else if (subSection === 'process') {
detailState = old.toggleProcess();
} else if (subSection === 'warnings') {
detailState = old.toggleWarnings();
} else if (subSection === 'references') {
detailState = old.toggleReferences();
} else {
detailState = old.toggleLogs();
}
const newDetailStates = new Map(detailStates);
newDetailStates.set(spanID, detailState);
setDetailStates(newDetailStates);
};
}

View File

@@ -0,0 +1,216 @@
import React from 'react';
import { shallow } from 'enzyme';
import { TraceView } from './TraceView';
import { SpanData, TraceData, TracePageHeader, TraceTimelineViewer } from '@jaegertracing/jaeger-ui-components';
function renderTraceView() {
const wrapper = shallow(<TraceView trace={response} />);
return {
timeline: wrapper.find(TraceTimelineViewer),
header: wrapper.find(TracePageHeader),
wrapper,
};
}
describe('TraceView', () => {
it('renders TraceTimelineViewer', () => {
const { timeline, header } = renderTraceView();
expect(timeline).toHaveLength(1);
expect(header).toHaveLength(1);
});
it('toggles detailState', () => {
let { timeline, wrapper } = renderTraceView();
expect(timeline.props().traceTimeline.detailStates.size).toBe(0);
timeline.props().detailToggle('1');
timeline = wrapper.find(TraceTimelineViewer);
expect(timeline.props().traceTimeline.detailStates.size).toBe(1);
expect(timeline.props().traceTimeline.detailStates.get('1')).not.toBeUndefined();
timeline.props().detailToggle('1');
timeline = wrapper.find(TraceTimelineViewer);
expect(timeline.props().traceTimeline.detailStates.size).toBe(0);
});
it('toggles children visibility', () => {
let { timeline, wrapper } = renderTraceView();
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
timeline.props().childrenToggle('1');
timeline = wrapper.find(TraceTimelineViewer);
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(1);
expect(timeline.props().traceTimeline.childrenHiddenIDs.has('1')).toBeTruthy();
timeline.props().childrenToggle('1');
timeline = wrapper.find(TraceTimelineViewer);
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
});
it('toggles adds and removes hover indent guides', () => {
let { timeline, wrapper } = renderTraceView();
expect(timeline.props().traceTimeline.hoverIndentGuideIds.size).toBe(0);
timeline.props().addHoverIndentGuideId('1');
timeline = wrapper.find(TraceTimelineViewer);
expect(timeline.props().traceTimeline.hoverIndentGuideIds.size).toBe(1);
expect(timeline.props().traceTimeline.hoverIndentGuideIds.has('1')).toBeTruthy();
timeline.props().removeHoverIndentGuideId('1');
timeline = wrapper.find(TraceTimelineViewer);
expect(timeline.props().traceTimeline.hoverIndentGuideIds.size).toBe(0);
});
it('toggles collapses and expands one level of spans', () => {
let { timeline, wrapper } = renderTraceView();
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
const spans = timeline.props().trace.spans;
timeline.props().collapseOne(spans);
timeline = wrapper.find(TraceTimelineViewer);
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(1);
expect(timeline.props().traceTimeline.childrenHiddenIDs.has('3fb050342773d333')).toBeTruthy();
timeline.props().expandOne(spans);
timeline = wrapper.find(TraceTimelineViewer);
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
});
it('toggles collapses and expands all levels', () => {
let { timeline, wrapper } = renderTraceView();
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
const spans = timeline.props().trace.spans;
timeline.props().collapseAll(spans);
timeline = wrapper.find(TraceTimelineViewer);
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(2);
expect(timeline.props().traceTimeline.childrenHiddenIDs.has('3fb050342773d333')).toBeTruthy();
expect(timeline.props().traceTimeline.childrenHiddenIDs.has('1ed38015486087ca')).toBeTruthy();
timeline.props().expandAll();
timeline = wrapper.find(TraceTimelineViewer);
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
});
it('searches for spans', () => {
let { wrapper, header } = renderTraceView();
header.props().onSearchValueChange('HTTP POST - api_prom_push');
const timeline = wrapper.find(TraceTimelineViewer);
expect(timeline.props().findMatchesIDs.has('1ed38015486087ca')).toBeTruthy();
});
it('change viewRange', () => {
let { header, timeline, wrapper } = renderTraceView();
const defaultRange = { time: { current: [0, 1] } };
expect(timeline.props().viewRange).toEqual(defaultRange);
expect(header.props().viewRange).toEqual(defaultRange);
header.props().updateViewRangeTime(0.2, 0.8);
let newRange = { time: { current: [0.2, 0.8] } };
timeline = wrapper.find(TraceTimelineViewer);
header = wrapper.find(TracePageHeader);
expect(timeline.props().viewRange).toEqual(newRange);
expect(header.props().viewRange).toEqual(newRange);
newRange = { time: { current: [0.3, 0.7] } };
timeline.props().updateViewRangeTime(0.3, 0.7);
timeline = wrapper.find(TraceTimelineViewer);
header = wrapper.find(TracePageHeader);
expect(timeline.props().viewRange).toEqual(newRange);
expect(header.props().viewRange).toEqual(newRange);
});
});
const response: TraceData & { spans: SpanData[] } = {
traceID: '1ed38015486087ca',
spans: [
{
traceID: '1ed38015486087ca',
spanID: '1ed38015486087ca',
flags: 1,
operationName: 'HTTP POST - api_prom_push',
references: [] as any,
startTime: 1585244579835187,
duration: 1098,
tags: [
{ key: 'sampler.type', type: 'string', value: 'const' },
{ key: 'sampler.param', type: 'bool', value: true },
{ key: 'span.kind', type: 'string', value: 'server' },
{ key: 'http.method', type: 'string', value: 'POST' },
{ key: 'http.url', type: 'string', value: '/api/prom/push' },
{ key: 'component', type: 'string', value: 'net/http' },
{ key: 'http.status_code', type: 'int64', value: 204 },
{ key: 'internal.span.format', type: 'string', value: 'proto' },
],
logs: [
{
timestamp: 1585244579835229,
fields: [{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[start reading]' }],
},
{
timestamp: 1585244579835241,
fields: [
{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[decompress]' },
{ key: 'size', type: 'int64', value: 315 },
],
},
{
timestamp: 1585244579835245,
fields: [
{ key: 'event', type: 'string', value: 'util.ParseProtoRequest[unmarshal]' },
{ key: 'size', type: 'int64', value: 446 },
],
},
],
processID: 'p1',
warnings: null as any,
},
{
traceID: '1ed38015486087ca',
spanID: '3fb050342773d333',
flags: 1,
operationName: '/logproto.Pusher/Push',
references: [{ refType: 'CHILD_OF', traceID: '1ed38015486087ca', spanID: '1ed38015486087ca' }],
startTime: 1585244579835341,
duration: 921,
tags: [
{ key: 'span.kind', type: 'string', value: 'client' },
{ key: 'component', type: 'string', value: 'gRPC' },
{ key: 'internal.span.format', type: 'string', value: 'proto' },
],
logs: [],
processID: 'p1',
warnings: null,
},
{
traceID: '1ed38015486087ca',
spanID: '35118c298fc91f68',
flags: 1,
operationName: '/logproto.Pusher/Push',
references: [{ refType: 'CHILD_OF', traceID: '1ed38015486087ca', spanID: '3fb050342773d333' }],
startTime: 1585244579836040,
duration: 36,
tags: [
{ key: 'span.kind', type: 'string', value: 'server' },
{ key: 'component', type: 'string', value: 'gRPC' },
{ key: 'internal.span.format', type: 'string', value: 'proto' },
],
logs: [] as any,
processID: 'p1',
warnings: null as any,
},
],
processes: {
p1: {
serviceName: 'loki-all',
tags: [
{ key: 'client-uuid', type: 'string', value: '2a59d08899ef6a8a' },
{ key: 'hostname', type: 'string', value: '0080b530fae3' },
{ key: 'ip', type: 'string', value: '172.18.0.6' },
{ key: 'jaeger.version', type: 'string', value: 'Go-2.20.1' },
],
},
},
warnings: null as any,
};

View File

@@ -0,0 +1,119 @@
import React, { useState } from 'react';
import {
KeyValuePair,
Link,
Span,
Trace,
TraceTimelineViewer,
TTraceTimeline,
UIElementsContext,
transformTraceData,
SpanData,
TraceData,
TracePageHeader,
} from '@jaegertracing/jaeger-ui-components';
import { UIElements } from './uiElements';
import { useViewRange } from './useViewRange';
import { useSearch } from './useSearch';
import { useChildrenState } from './useChildrenState';
import { useDetailState } from './useDetailState';
import { useHoverIndentGuide } from './useHoverIndentGuide';
type Props = {
trace: TraceData & { spans: SpanData[] };
};
export function TraceView(props: Props) {
const { expandOne, collapseOne, childrenToggle, collapseAll, childrenHiddenIDs, expandAll } = useChildrenState();
const {
detailStates,
toggleDetail,
detailLogItemToggle,
detailLogsToggle,
detailProcessToggle,
detailReferencesToggle,
detailTagsToggle,
detailWarningsToggle,
} = useDetailState();
const { removeHoverIndentGuideId, addHoverIndentGuideId, hoverIndentGuideIds } = useHoverIndentGuide();
const { viewRange, updateViewRangeTime, updateNextViewRangeTime } = useViewRange();
/**
* Keeps state of resizable name column width
*/
const [spanNameColumnWidth, setSpanNameColumnWidth] = useState(0.25);
/**
* State of the top minimap, slim means it is collapsed.
*/
const [slim, setSlim] = useState(false);
const traceProp = transformTraceData(props.trace);
const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
return (
<UIElementsContext.Provider value={UIElements}>
<TracePageHeader
canCollapse={true}
clearSearch={() => {}}
focusUiFindMatches={() => {}}
hideMap={false}
hideSummary={false}
nextResult={() => {}}
onSlimViewClicked={() => setSlim(!slim)}
onTraceGraphViewClicked={() => {}}
prevResult={() => {}}
resultCount={0}
slimView={slim}
textFilter={null}
trace={traceProp}
traceGraphView={false}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
viewRange={viewRange}
searchValue={search}
onSearchValueChange={setSearch}
hideSearchButtons={true}
/>
<TraceTimelineViewer
registerAccessors={() => {}}
scrollToFirstVisibleSpan={() => {}}
findMatchesIDs={spanFindMatches}
trace={traceProp}
traceTimeline={
{
childrenHiddenIDs,
detailStates,
hoverIndentGuideIds,
shouldScrollToFirstUiFindMatch: false,
spanNameColumnWidth,
traceID: '50b96206cf81dd64',
} as TTraceTimeline
}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
viewRange={viewRange}
focusSpan={() => {}}
createLinkToExternalSpan={() => ''}
setSpanNameColumnWidth={setSpanNameColumnWidth}
collapseAll={collapseAll}
collapseOne={collapseOne}
expandAll={expandAll}
expandOne={expandOne}
childrenToggle={childrenToggle}
clearShouldScrollToFirstUiFindMatch={() => {}}
detailLogItemToggle={detailLogItemToggle}
detailLogsToggle={detailLogsToggle}
detailWarningsToggle={detailWarningsToggle}
detailReferencesToggle={detailReferencesToggle}
detailProcessToggle={detailProcessToggle}
detailTagsToggle={detailTagsToggle}
detailToggle={toggleDetail}
setTrace={(trace: Trace | null, uiFind: string | null) => {}}
addHoverIndentGuideId={addHoverIndentGuideId}
removeHoverIndentGuideId={removeHoverIndentGuideId}
linksGetter={(span: Span, items: KeyValuePair[], itemIndex: number) => [] as Link[]}
uiFind={search}
/>
</UIElementsContext.Provider>
);
}

View File

@@ -0,0 +1,45 @@
import React from 'react';
import { ButtonProps, Elements } from '@jaegertracing/jaeger-ui-components';
import { Button, Input } from '@grafana/ui';
/**
* Right now Jaeger components need some UI elements to be injected. This is to get rid of AntD UI library that was
* used by default.
*/
// This needs to be static to prevent remounting on every render.
export const UIElements: Elements = {
Popover: (() => null as any) as any,
Tooltip: (() => null as any) as any,
Icon: (() => null as any) as any,
Dropdown: (() => null as any) as any,
Menu: (() => null as any) as any,
MenuItem: (() => null as any) as any,
Button: ({ onClick, children, className }: ButtonProps) => (
<Button variant={'secondary'} onClick={onClick} className={className}>
{children}
</Button>
),
Divider,
Input: props => <Input {...props} />,
InputGroup: ({ children, className, style }) => (
<span className={className} style={style}>
{children}
</span>
),
};
function Divider({ className }: { className?: string }) {
return (
<div
style={{
display: 'inline-block',
background: '#e8e8e8',
width: '1px',
height: '0.9em',
margin: '0 8px',
}}
className={className}
/>
);
}

View File

@@ -0,0 +1,65 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useChildrenState } from './useChildrenState';
import { Span } from '@jaegertracing/jaeger-ui-components';
describe('useChildrenState', () => {
describe('childrenToggle', () => {
it('toggles children state', async () => {
const { result } = renderHook(() => useChildrenState());
expect(result.current.childrenHiddenIDs.size).toBe(0);
act(() => result.current.childrenToggle('testId'));
expect(result.current.childrenHiddenIDs.size).toBe(1);
expect(result.current.childrenHiddenIDs.has('testId')).toBe(true);
act(() => result.current.childrenToggle('testId'));
expect(result.current.childrenHiddenIDs.size).toBe(0);
});
});
describe('expandAll', () => {
it('expands all', async () => {
const { result } = renderHook(() => useChildrenState());
act(() => result.current.childrenToggle('testId1'));
act(() => result.current.childrenToggle('testId2'));
expect(result.current.childrenHiddenIDs.size).toBe(2);
act(() => result.current.expandAll());
expect(result.current.childrenHiddenIDs.size).toBe(0);
});
});
describe('collapseAll', () => {
it('hides spans that have children', async () => {
const { result } = renderHook(() => useChildrenState());
act(() =>
result.current.collapseAll([
{ spanID: 'span1', hasChildren: true } as Span,
{ spanID: 'span2', hasChildren: false } as Span,
])
);
expect(result.current.childrenHiddenIDs.size).toBe(1);
expect(result.current.childrenHiddenIDs.has('span1')).toBe(true);
});
it('does nothing if already collapsed', async () => {
const { result } = renderHook(() => useChildrenState());
act(() => result.current.childrenToggle('span1'));
act(() =>
result.current.collapseAll([
{ spanID: 'span1', hasChildren: true } as Span,
{ spanID: 'span2', hasChildren: false } as Span,
])
);
expect(result.current.childrenHiddenIDs.size).toBe(1);
expect(result.current.childrenHiddenIDs.has('span1')).toBe(true);
});
});
// Other function are not yet used.
});

View File

@@ -0,0 +1,94 @@
import { useState } from 'react';
import { Span } from '@jaegertracing/jaeger-ui-components';
/**
* Children state means whether spans are collapsed or not. Also provides some functions to manipulate that state.
*/
export function useChildrenState() {
const [childrenHiddenIDs, setChildrenHiddenIDs] = useState(new Set<string>());
function expandOne(spans: Span[]) {
if (childrenHiddenIDs.size === 0) {
return;
}
let prevExpandedDepth = -1;
let expandNextHiddenSpan = true;
const newChildrenHiddenIDs = spans.reduce((res, s) => {
if (s.depth <= prevExpandedDepth) {
expandNextHiddenSpan = true;
}
if (expandNextHiddenSpan && res.has(s.spanID)) {
res.delete(s.spanID);
expandNextHiddenSpan = false;
prevExpandedDepth = s.depth;
}
return res;
}, new Set(childrenHiddenIDs));
setChildrenHiddenIDs(newChildrenHiddenIDs);
}
function collapseOne(spans: Span[]) {
if (shouldDisableCollapse(spans, childrenHiddenIDs)) {
return;
}
let nearestCollapsedAncestor: Span | undefined;
const newChildrenHiddenIDs = spans.reduce((res, curSpan) => {
if (nearestCollapsedAncestor && curSpan.depth <= nearestCollapsedAncestor.depth) {
res.add(nearestCollapsedAncestor.spanID);
if (curSpan.hasChildren) {
nearestCollapsedAncestor = curSpan;
}
} else if (curSpan.hasChildren && !res.has(curSpan.spanID)) {
nearestCollapsedAncestor = curSpan;
}
return res;
}, new Set(childrenHiddenIDs));
// The last one
if (nearestCollapsedAncestor) {
newChildrenHiddenIDs.add(nearestCollapsedAncestor.spanID);
}
setChildrenHiddenIDs(newChildrenHiddenIDs);
}
function expandAll() {
setChildrenHiddenIDs(new Set<string>());
}
function collapseAll(spans: Span[]) {
if (shouldDisableCollapse(spans, childrenHiddenIDs)) {
return;
}
const newChildrenHiddenIDs = spans.reduce((res, s) => {
if (s.hasChildren) {
res.add(s.spanID);
}
return res;
}, new Set<string>());
setChildrenHiddenIDs(newChildrenHiddenIDs);
}
function childrenToggle(spanID: string) {
const newChildrenHiddenIDs = new Set(childrenHiddenIDs);
if (childrenHiddenIDs.has(spanID)) {
newChildrenHiddenIDs.delete(spanID);
} else {
newChildrenHiddenIDs.add(spanID);
}
setChildrenHiddenIDs(newChildrenHiddenIDs);
}
return {
childrenHiddenIDs,
expandOne,
collapseOne,
expandAll,
collapseAll,
childrenToggle,
};
}
function shouldDisableCollapse(allSpans: Span[], hiddenSpansIds: Set<string>) {
const allParentSpans = allSpans.filter(s => s.hasChildren);
return allParentSpans.length === hiddenSpansIds.size;
}

View File

@@ -0,0 +1,56 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { Log } from '@jaegertracing/jaeger-ui-components';
import { useDetailState } from './useDetailState';
describe('useDetailState', () => {
it('toggles detail', async () => {
const { result } = renderHook(() => useDetailState());
expect(result.current.detailStates.size).toBe(0);
act(() => result.current.toggleDetail('span1'));
expect(result.current.detailStates.size).toBe(1);
expect(result.current.detailStates.has('span1')).toBe(true);
act(() => result.current.toggleDetail('span1'));
expect(result.current.detailStates.size).toBe(0);
});
it('toggles logs and logs items', async () => {
const { result } = renderHook(() => useDetailState());
act(() => result.current.toggleDetail('span1'));
act(() => result.current.detailLogsToggle('span1'));
expect(result.current.detailStates.get('span1').logs.isOpen).toBe(true);
const log = { timestamp: 1 } as Log;
act(() => result.current.detailLogItemToggle('span1', log));
expect(result.current.detailStates.get('span1').logs.openedItems.has(log)).toBe(true);
});
it('toggles warnings', async () => {
const { result } = renderHook(() => useDetailState());
act(() => result.current.toggleDetail('span1'));
act(() => result.current.detailWarningsToggle('span1'));
expect(result.current.detailStates.get('span1').isWarningsOpen).toBe(true);
});
it('toggles references', async () => {
const { result } = renderHook(() => useDetailState());
act(() => result.current.toggleDetail('span1'));
act(() => result.current.detailReferencesToggle('span1'));
expect(result.current.detailStates.get('span1').isReferencesOpen).toBe(true);
});
it('toggles processes', async () => {
const { result } = renderHook(() => useDetailState());
act(() => result.current.toggleDetail('span1'));
act(() => result.current.detailProcessToggle('span1'));
expect(result.current.detailStates.get('span1').isProcessOpen).toBe(true);
});
it('toggles tags', async () => {
const { result } = renderHook(() => useDetailState());
act(() => result.current.toggleDetail('span1'));
act(() => result.current.detailTagsToggle('span1'));
expect(result.current.detailStates.get('span1').isTagsOpen).toBe(true);
});
});

View File

@@ -0,0 +1,70 @@
import { useState } from 'react';
import { DetailState, Log } from '@jaegertracing/jaeger-ui-components';
/**
* Keeps state of the span detail. This means whether span details are open but also state of each detail subitem
* like logs or tags.
*/
export function useDetailState() {
const [detailStates, setDetailStates] = useState(new Map<string, DetailState>());
function toggleDetail(spanID: string) {
const newDetailStates = new Map(detailStates);
if (newDetailStates.has(spanID)) {
newDetailStates.delete(spanID);
} else {
newDetailStates.set(spanID, new DetailState());
}
setDetailStates(newDetailStates);
}
function detailLogItemToggle(spanID: string, log: Log) {
const old = detailStates.get(spanID);
if (!old) {
return;
}
const detailState = old.toggleLogItem(log);
const newDetailStates = new Map(detailStates);
newDetailStates.set(spanID, detailState);
return setDetailStates(newDetailStates);
}
return {
detailStates,
toggleDetail,
detailLogItemToggle,
detailLogsToggle: makeDetailSubsectionToggle('logs', detailStates, setDetailStates),
detailWarningsToggle: makeDetailSubsectionToggle('warnings', detailStates, setDetailStates),
detailReferencesToggle: makeDetailSubsectionToggle('references', detailStates, setDetailStates),
detailProcessToggle: makeDetailSubsectionToggle('process', detailStates, setDetailStates),
detailTagsToggle: makeDetailSubsectionToggle('tags', detailStates, setDetailStates),
};
}
function makeDetailSubsectionToggle(
subSection: 'tags' | 'process' | 'logs' | 'warnings' | 'references',
detailStates: Map<string, DetailState>,
setDetailStates: (detailStates: Map<string, DetailState>) => void
) {
return (spanID: string) => {
const old = detailStates.get(spanID);
if (!old) {
return;
}
let detailState;
if (subSection === 'tags') {
detailState = old.toggleTags();
} else if (subSection === 'process') {
detailState = old.toggleProcess();
} else if (subSection === 'warnings') {
detailState = old.toggleWarnings();
} else if (subSection === 'references') {
detailState = old.toggleReferences();
} else {
detailState = old.toggleLogs();
}
const newDetailStates = new Map(detailStates);
newDetailStates.set(spanID, detailState);
setDetailStates(newDetailStates);
};
}

View File

@@ -0,0 +1,16 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useHoverIndentGuide } from './useHoverIndentGuide';
describe('useHoverIndentGuide', () => {
it('adds and removes indent guide ids', async () => {
const { result } = renderHook(() => useHoverIndentGuide());
expect(result.current.hoverIndentGuideIds.size).toBe(0);
act(() => result.current.addHoverIndentGuideId('span1'));
expect(result.current.hoverIndentGuideIds.size).toBe(1);
expect(result.current.hoverIndentGuideIds.has('span1')).toBe(true);
act(() => result.current.removeHoverIndentGuideId('span1'));
expect(result.current.hoverIndentGuideIds.size).toBe(0);
});
});

View File

@@ -0,0 +1,30 @@
import { useState } from 'react';
/**
* This is used internally to handle hover state of indent guide. As indent guides are separate
* components per each row/span and you need to highlight all in multiple rows to make the effect of single line
* they need this kind of common imperative state changes.
*
* Ideally would be changed to trace view internal state.
*/
export function useHoverIndentGuide() {
const [hoverIndentGuideIds, setHoverIndentGuideIds] = useState(new Set<string>());
function addHoverIndentGuideId(spanID: string) {
setHoverIndentGuideIds(prevState => {
const newHoverIndentGuideIds = new Set(prevState);
newHoverIndentGuideIds.add(spanID);
return newHoverIndentGuideIds;
});
}
function removeHoverIndentGuideId(spanID: string) {
setHoverIndentGuideIds(prevState => {
const newHoverIndentGuideIds = new Set(prevState);
newHoverIndentGuideIds.delete(spanID);
return newHoverIndentGuideIds;
});
}
return { hoverIndentGuideIds, addHoverIndentGuideId, removeHoverIndentGuideId };
}

View File

@@ -0,0 +1,44 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useSearch } from './useSearch';
import { Span } from '@jaegertracing/jaeger-ui-components';
describe('useSearch', () => {
it('returns matching span IDs', async () => {
const { result } = renderHook(() =>
useSearch([
{
spanID: 'span1',
operationName: 'operation1',
process: {
serviceName: 'service1',
tags: [],
},
tags: [],
logs: [],
} as Span,
{
spanID: 'span2',
operationName: 'operation2',
process: {
serviceName: 'service2',
tags: [],
},
tags: [],
logs: [],
} as Span,
])
);
act(() => result.current.setSearch('service1'));
expect(result.current.spanFindMatches.size).toBe(1);
expect(result.current.spanFindMatches.has('span1')).toBe(true);
});
it('works without spans', async () => {
const { result } = renderHook(() => useSearch());
act(() => result.current.setSearch('service1'));
expect(result.current.spanFindMatches).toBe(undefined);
});
});

View File

@@ -0,0 +1,15 @@
import { useState } from 'react';
import { Span, filterSpans } from '@jaegertracing/jaeger-ui-components';
/**
* Controls the state of search input that highlights spans if they match the search string.
* @param spans
*/
export function useSearch(spans?: Span[]) {
const [search, setSearch] = useState('');
let spanFindMatches: Set<string> | undefined;
if (search && spans) {
spanFindMatches = filterSpans(search, spans);
}
return { search, setSearch, spanFindMatches };
}

View File

@@ -0,0 +1,25 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useViewRange } from './useViewRange';
describe('useViewRange', () => {
it('defaults to full range', async () => {
const { result } = renderHook(() => useViewRange());
expect(result.current.viewRange).toEqual({ time: { current: [0, 1] } });
});
describe('updateNextViewRangeTime', () => {
it('updates time', async () => {
const { result } = renderHook(() => useViewRange());
act(() => result.current.updateNextViewRangeTime({ cursor: 0.5 }));
expect(result.current.viewRange).toEqual({ time: { current: [0, 1], cursor: 0.5 } });
});
});
describe('updateViewRangeTime', () => {
it('updates time', async () => {
const { result } = renderHook(() => useViewRange());
act(() => result.current.updateViewRangeTime(0.1, 0.2));
expect(result.current.viewRange).toEqual({ time: { current: [0.1, 0.2] } });
});
});
});

View File

@@ -0,0 +1,35 @@
import { useState } from 'react';
import { ViewRangeTimeUpdate, ViewRange } from '@jaegertracing/jaeger-ui-components';
/**
* Controls state of the zoom function that can be used through minimap in header or on the timeline. ViewRange contains
* state not only for current range that is showing but range that is currently being selected by the user.
*/
export function useViewRange() {
const [viewRange, setViewRange] = useState<ViewRange>({
time: {
current: [0, 1],
},
});
function updateNextViewRangeTime(update: ViewRangeTimeUpdate) {
setViewRange(
(prevRange): ViewRange => {
const time = { ...prevRange.time, ...update };
return { ...prevRange, time };
}
);
}
function updateViewRangeTime(start: number, end: number) {
const current: [number, number] = [start, end];
const time = { current };
setViewRange(
(prevRange): ViewRange => {
return { ...prevRange, time };
}
);
}
return { viewRange, updateViewRangeTime, updateNextViewRangeTime };
}

View File

@@ -25,7 +25,7 @@ export class Wrapper extends Component<WrapperProps> {
return (
<div className="page-scrollbar-wrapper">
<CustomScrollbar autoHeightMin={'100%'} autoHeightMax={''} className="custom-scrollbar--page">
<div style={{ height: '100%' }} className="explore-wrapper">
<div className="explore-wrapper">
<ErrorBoundaryAlert style="page">
<Explore exploreId={ExploreId.left} />
</ErrorBoundaryAlert>