mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore: Add trace UI to show traces from tracing datasources (#23047)
* 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 Co-authored-by: David Kaltschmidt <david.kaltschmidt@gmail.com>
This commit is contained in:
@@ -7,54 +7,108 @@ import {
|
||||
DataQueryError,
|
||||
DataQueryRequest,
|
||||
CoreApp,
|
||||
MutableDataFrame,
|
||||
} from '@grafana/data';
|
||||
import { getFirstNonQueryRowSpecificError } from 'app/core/utils/explore';
|
||||
import { ExploreId } from 'app/types/explore';
|
||||
import { shallow } from 'enzyme';
|
||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||
import { Explore, ExploreProps } from './Explore';
|
||||
import { scanStopAction } from './state/actionTypes';
|
||||
import { toggleGraph } from './state/actions';
|
||||
import { Provider } from 'react-redux';
|
||||
import { configureStore } from 'app/store/configureStore';
|
||||
import { SecondaryActions } from './SecondaryActions';
|
||||
import { TraceView } from './TraceView';
|
||||
|
||||
const setup = (renderMethod: any, propOverrides?: object) => {
|
||||
const props: ExploreProps = {
|
||||
changeSize: jest.fn(),
|
||||
datasourceInstance: {
|
||||
meta: {
|
||||
metrics: true,
|
||||
logs: true,
|
||||
},
|
||||
components: {
|
||||
ExploreStartPage: {},
|
||||
},
|
||||
} as DataSourceApi,
|
||||
datasourceMissing: false,
|
||||
exploreId: ExploreId.left,
|
||||
initializeExplore: jest.fn(),
|
||||
initialized: true,
|
||||
modifyQueries: jest.fn(),
|
||||
update: {
|
||||
datasource: false,
|
||||
queries: false,
|
||||
range: false,
|
||||
mode: false,
|
||||
ui: false,
|
||||
const dummyProps: ExploreProps = {
|
||||
changeSize: jest.fn(),
|
||||
datasourceInstance: {
|
||||
meta: {
|
||||
metrics: true,
|
||||
logs: true,
|
||||
},
|
||||
refreshExplore: jest.fn(),
|
||||
scanning: false,
|
||||
scanRange: {
|
||||
from: '0',
|
||||
to: '0',
|
||||
components: {
|
||||
ExploreStartPage: {},
|
||||
},
|
||||
scanStart: jest.fn(),
|
||||
scanStopAction: scanStopAction,
|
||||
setQueries: jest.fn(),
|
||||
split: false,
|
||||
queryKeys: [],
|
||||
initialDatasource: 'test',
|
||||
initialQueries: [],
|
||||
initialRange: {
|
||||
} as DataSourceApi,
|
||||
datasourceMissing: false,
|
||||
exploreId: ExploreId.left,
|
||||
initializeExplore: jest.fn(),
|
||||
initialized: true,
|
||||
modifyQueries: jest.fn(),
|
||||
update: {
|
||||
datasource: false,
|
||||
queries: false,
|
||||
range: false,
|
||||
mode: false,
|
||||
ui: false,
|
||||
},
|
||||
refreshExplore: jest.fn(),
|
||||
scanning: false,
|
||||
scanRange: {
|
||||
from: '0',
|
||||
to: '0',
|
||||
},
|
||||
scanStart: jest.fn(),
|
||||
scanStopAction: scanStopAction,
|
||||
setQueries: jest.fn(),
|
||||
split: false,
|
||||
queryKeys: [],
|
||||
initialDatasource: 'test',
|
||||
initialQueries: [],
|
||||
initialRange: {
|
||||
from: toUtc('2019-01-01 10:00:00'),
|
||||
to: toUtc('2019-01-01 16:00:00'),
|
||||
raw: {
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
mode: ExploreMode.Metrics,
|
||||
initialUI: {
|
||||
showingTable: false,
|
||||
showingGraph: false,
|
||||
showingLogs: false,
|
||||
},
|
||||
isLive: false,
|
||||
syncedTimes: false,
|
||||
updateTimeRange: jest.fn(),
|
||||
graphResult: [],
|
||||
loading: false,
|
||||
absoluteRange: {
|
||||
from: 0,
|
||||
to: 0,
|
||||
},
|
||||
showingGraph: false,
|
||||
showingTable: false,
|
||||
timeZone: 'UTC',
|
||||
onHiddenSeriesChanged: jest.fn(),
|
||||
toggleGraph: toggleGraph,
|
||||
queryResponse: {
|
||||
state: LoadingState.NotStarted,
|
||||
series: [],
|
||||
request: ({
|
||||
requestId: '1',
|
||||
dashboardId: 0,
|
||||
interval: '1s',
|
||||
panelId: 1,
|
||||
scopedVars: {
|
||||
apps: {
|
||||
value: 'value',
|
||||
},
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
},
|
||||
],
|
||||
timezone: 'UTC',
|
||||
app: CoreApp.Explore,
|
||||
startTime: 0,
|
||||
} as unknown) as DataQueryRequest,
|
||||
error: {} as DataQueryError,
|
||||
timeRange: {
|
||||
from: toUtc('2019-01-01 10:00:00'),
|
||||
to: toUtc('2019-01-01 16:00:00'),
|
||||
raw: {
|
||||
@@ -62,68 +116,21 @@ const setup = (renderMethod: any, propOverrides?: object) => {
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
mode: ExploreMode.Metrics,
|
||||
initialUI: {
|
||||
showingTable: false,
|
||||
showingGraph: false,
|
||||
showingLogs: false,
|
||||
},
|
||||
isLive: false,
|
||||
syncedTimes: false,
|
||||
updateTimeRange: jest.fn(),
|
||||
graphResult: [],
|
||||
loading: false,
|
||||
absoluteRange: {
|
||||
from: 0,
|
||||
to: 0,
|
||||
},
|
||||
showingGraph: false,
|
||||
showingTable: false,
|
||||
timeZone: 'UTC',
|
||||
onHiddenSeriesChanged: jest.fn(),
|
||||
toggleGraph: toggleGraph,
|
||||
queryResponse: {
|
||||
state: LoadingState.NotStarted,
|
||||
series: [],
|
||||
request: ({
|
||||
requestId: '1',
|
||||
dashboardId: 0,
|
||||
interval: '1s',
|
||||
panelId: 1,
|
||||
scopedVars: {
|
||||
apps: {
|
||||
value: 'value',
|
||||
},
|
||||
},
|
||||
targets: [
|
||||
{
|
||||
refId: 'A',
|
||||
},
|
||||
],
|
||||
timezone: 'UTC',
|
||||
app: CoreApp.Explore,
|
||||
startTime: 0,
|
||||
} as unknown) as DataQueryRequest,
|
||||
error: {} as DataQueryError,
|
||||
timeRange: {
|
||||
from: toUtc('2019-01-01 10:00:00'),
|
||||
to: toUtc('2019-01-01 16:00:00'),
|
||||
raw: {
|
||||
from: 'now-6h',
|
||||
to: 'now',
|
||||
},
|
||||
},
|
||||
},
|
||||
originPanelId: 1,
|
||||
addQueryRow: jest.fn(),
|
||||
};
|
||||
},
|
||||
originPanelId: 1,
|
||||
addQueryRow: jest.fn(),
|
||||
};
|
||||
|
||||
const setup = (renderMethod: any, propOverrides?: Partial<ExploreProps>) => {
|
||||
const store = configureStore();
|
||||
|
||||
Object.assign(props, propOverrides);
|
||||
return renderMethod(
|
||||
<Provider store={store}>
|
||||
<Explore {...props} />
|
||||
<Explore
|
||||
{...{
|
||||
...dummyProps,
|
||||
...propOverrides,
|
||||
}}
|
||||
/>
|
||||
</Provider>
|
||||
);
|
||||
};
|
||||
@@ -145,6 +152,40 @@ describe('Explore', () => {
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders SecondaryActions and add row button', () => {
|
||||
const wrapper = shallow(<Explore {...dummyProps} />);
|
||||
expect(wrapper.find(SecondaryActions)).toHaveLength(1);
|
||||
expect(wrapper.find(SecondaryActions).props().addQueryRowButtonHidden).toBe(false);
|
||||
});
|
||||
|
||||
it('does not show add row button if mode is tracing', () => {
|
||||
const wrapper = shallow(<Explore {...{ ...dummyProps, mode: ExploreMode.Tracing }} />);
|
||||
expect(wrapper.find(SecondaryActions).props().addQueryRowButtonHidden).toBe(true);
|
||||
});
|
||||
|
||||
it('renders TraceView if tracing mode', () => {
|
||||
const wrapper = shallow(
|
||||
<Explore
|
||||
{...{
|
||||
...dummyProps,
|
||||
mode: ExploreMode.Tracing,
|
||||
queryResponse: {
|
||||
...dummyProps.queryResponse,
|
||||
state: LoadingState.Done,
|
||||
series: [new MutableDataFrame({ fields: [{ name: 'trace', values: [{}] }] })],
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
const autoSizer = shallow(
|
||||
wrapper
|
||||
.find(AutoSizer)
|
||||
.props()
|
||||
.children({ width: 100, height: 100 }) as React.ReactElement
|
||||
);
|
||||
expect(autoSizer.find(TraceView).length).toBe(1);
|
||||
});
|
||||
|
||||
it('should filter out a query-row-specific error when looking for non-query-row-specific errors', async () => {
|
||||
const queryErrors = setupErrors(true);
|
||||
const queryError = getFirstNonQueryRowSpecificError(queryErrors);
|
||||
|
||||
@@ -59,6 +59,8 @@ import { getTimeZone } from '../profile/state/selectors';
|
||||
import { ErrorContainer } from './ErrorContainer';
|
||||
import { scanStopAction } from './state/actionTypes';
|
||||
import { ExploreGraphPanel } from './ExploreGraphPanel';
|
||||
import { TraceView } from './TraceView';
|
||||
import { SecondaryActions } from './SecondaryActions';
|
||||
|
||||
const getStyles = stylesFactory(() => {
|
||||
return {
|
||||
@@ -319,27 +321,14 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
{datasourceInstance && (
|
||||
<div className="explore-container">
|
||||
<QueryRows exploreEvents={this.exploreEvents} exploreId={exploreId} queryKeys={queryKeys} />
|
||||
<div className="gf-form">
|
||||
<button
|
||||
aria-label="Add row button"
|
||||
className={`gf-form-label gf-form-label--btn ${styles.button}`}
|
||||
onClick={this.onClickAddQueryRowButton}
|
||||
disabled={isLive}
|
||||
>
|
||||
<i className={'fa fa-fw fa-plus icon-margin-right'} />
|
||||
<span className="btn-title">{'\xA0' + 'Add query'}</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Rich history button"
|
||||
className={cx(`gf-form-label gf-form-label--btn ${styles.button}`, {
|
||||
['explore-active-button']: showRichHistory,
|
||||
})}
|
||||
onClick={this.toggleShowRichHistory}
|
||||
>
|
||||
<i className={'fa fa-fw fa-history icon-margin-right '} />
|
||||
<span className="btn-title">{'\xA0' + 'Query history'}</span>
|
||||
</button>
|
||||
</div>
|
||||
<SecondaryActions
|
||||
addQueryRowButtonDisabled={isLive}
|
||||
// We cannot show multiple traces at the same time right now so we do not show add query button.
|
||||
addQueryRowButtonHidden={mode === ExploreMode.Tracing}
|
||||
richHistoryButtonActive={showRichHistory}
|
||||
onClickAddQueryRowButton={this.onClickAddQueryRowButton}
|
||||
onClickRichHistoryButton={this.toggleShowRichHistory}
|
||||
/>
|
||||
<ErrorContainer queryError={queryError} />
|
||||
<AutoSizer className={styles.fullHeight} onResize={this.onResize} disableHeight>
|
||||
{({ width }) => {
|
||||
@@ -400,18 +389,12 @@ export class Explore extends React.PureComponent<ExploreProps, ExploreState> {
|
||||
onStopScanning={this.onStopScanning}
|
||||
/>
|
||||
)}
|
||||
{mode === ExploreMode.Tracing && (
|
||||
<div className={styles.fullHeight}>
|
||||
{queryResponse &&
|
||||
!!queryResponse.series.length &&
|
||||
queryResponse.series[0].fields[0].values.get(0) && (
|
||||
<iframe
|
||||
className={styles.iframe}
|
||||
src={queryResponse.series[0].fields[0].values.get(0)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{mode === ExploreMode.Tracing &&
|
||||
// We expect only one trace at the moment to be in the dataframe
|
||||
// If there is not data (like 404) we show a separate error so no need to show anything here
|
||||
queryResponse.series[0] && (
|
||||
<TraceView trace={queryResponse.series[0].fields[0].values.get(0) as any} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{showRichHistory && (
|
||||
|
||||
51
public/app/features/explore/SecondaryActions.test.tsx
Normal file
51
public/app/features/explore/SecondaryActions.test.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
import { noop } from 'lodash';
|
||||
import { shallow } from 'enzyme';
|
||||
import { SecondaryActions } from './SecondaryActions';
|
||||
|
||||
const addQueryRowButtonSelector = '[aria-label="Add row button"]';
|
||||
const richHistoryButtonSelector = '[aria-label="Rich history button"]';
|
||||
|
||||
describe('SecondaryActions', () => {
|
||||
it('should render component two buttons', () => {
|
||||
const wrapper = shallow(<SecondaryActions onClickAddQueryRowButton={noop} onClickRichHistoryButton={noop} />);
|
||||
expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(1);
|
||||
expect(wrapper.find(richHistoryButtonSelector)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should not render add row button if addQueryRowButtonHidden=true', () => {
|
||||
const wrapper = shallow(
|
||||
<SecondaryActions
|
||||
addQueryRowButtonHidden={true}
|
||||
onClickAddQueryRowButton={noop}
|
||||
onClickRichHistoryButton={noop}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find(addQueryRowButtonSelector)).toHaveLength(0);
|
||||
expect(wrapper.find(richHistoryButtonSelector)).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should disable add row button if addQueryRowButtonDisabled=true', () => {
|
||||
const wrapper = shallow(
|
||||
<SecondaryActions
|
||||
addQueryRowButtonDisabled={true}
|
||||
onClickAddQueryRowButton={noop}
|
||||
onClickRichHistoryButton={noop}
|
||||
/>
|
||||
);
|
||||
expect(wrapper.find(addQueryRowButtonSelector).props().disabled).toBe(true);
|
||||
});
|
||||
|
||||
it('should map click handlers correctly', () => {
|
||||
const onClickAddRow = jest.fn();
|
||||
const onClickHistory = jest.fn();
|
||||
const wrapper = shallow(
|
||||
<SecondaryActions onClickAddQueryRowButton={onClickAddRow} onClickRichHistoryButton={onClickHistory} />
|
||||
);
|
||||
wrapper.find(addQueryRowButtonSelector).simulate('click');
|
||||
expect(onClickAddRow).toBeCalled();
|
||||
|
||||
wrapper.find(richHistoryButtonSelector).simulate('click');
|
||||
expect(onClickHistory).toBeCalled();
|
||||
});
|
||||
});
|
||||
47
public/app/features/explore/SecondaryActions.tsx
Normal file
47
public/app/features/explore/SecondaryActions.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import { css, cx } from 'emotion';
|
||||
import { stylesFactory } from '@grafana/ui';
|
||||
|
||||
type Props = {
|
||||
addQueryRowButtonDisabled?: boolean;
|
||||
richHistoryButtonActive?: boolean;
|
||||
addQueryRowButtonHidden?: boolean;
|
||||
onClickAddQueryRowButton: () => void;
|
||||
onClickRichHistoryButton: () => void;
|
||||
};
|
||||
|
||||
const getStyles = stylesFactory(() => {
|
||||
return {
|
||||
button: css`
|
||||
margin: 1em 4px 0 0;
|
||||
`,
|
||||
};
|
||||
});
|
||||
export function SecondaryActions(props: Props) {
|
||||
const styles = getStyles();
|
||||
return (
|
||||
<div className="gf-form">
|
||||
{!props.addQueryRowButtonHidden && (
|
||||
<button
|
||||
aria-label="Add row button"
|
||||
className={`gf-form-label gf-form-label--btn ${styles.button}`}
|
||||
onClick={props.onClickAddQueryRowButton}
|
||||
disabled={props.addQueryRowButtonDisabled}
|
||||
>
|
||||
<i className={'fa fa-fw fa-plus icon-margin-right'} />
|
||||
<span className="btn-title">{'\xA0' + 'Add query'}</span>
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
aria-label="Rich history button"
|
||||
className={cx(`gf-form-label gf-form-label--btn ${styles.button}`, {
|
||||
['explore-active-button']: props.richHistoryButtonActive,
|
||||
})}
|
||||
onClick={props.onClickRichHistoryButton}
|
||||
>
|
||||
<i className={'fa fa-fw fa-history icon-margin-right '} />
|
||||
<span className="btn-title">{'\xA0' + 'Query history'}</span>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
182
public/app/features/explore/TraceView.test.tsx
Normal file
182
public/app/features/explore/TraceView.test.tsx
Normal file
@@ -0,0 +1,182 @@
|
||||
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,
|
||||
};
|
||||
247
public/app/features/explore/TraceView.tsx
Normal file
247
public/app/features/explore/TraceView.tsx
Normal file
@@ -0,0 +1,247 @@
|
||||
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);
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user