mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
TraceView: Allow span links defined on dataFrame (#40563)
* Align range to seconds in log queries * Use default display processor if there is none in FieldDisplayProxy * Allow links defined in dataframe * Remove debug log * Fix typings for span links * Lint go * Fix tests * Update tests * Add test for the display proxy * Streamline the fallback for diplayProcessor
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
|
||||
import { applyFieldOverrides } from './fieldOverrides';
|
||||
import { toDataFrame } from '../dataframe';
|
||||
import { MutableDataFrame, toDataFrame } from '../dataframe';
|
||||
import { createTheme } from '../themes';
|
||||
|
||||
describe('getFieldDisplayValuesProxy', () => {
|
||||
@@ -81,4 +81,14 @@ describe('getFieldDisplayValuesProxy', () => {
|
||||
expect(p.xyz).toBeUndefined();
|
||||
expect(p[100]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should use default display processor if display is not defined', () => {
|
||||
const p = getFieldDisplayValuesProxy({
|
||||
frame: new MutableDataFrame({ fields: [{ name: 'test', values: [1, 2] }] }),
|
||||
rowIndex: 0,
|
||||
});
|
||||
expect(p.test.text).toBe('1');
|
||||
expect(p.test.numeric).toBe(1);
|
||||
expect(p.test.toString()).toBe('1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
import { toNumber } from 'lodash';
|
||||
import { DataFrame, DisplayValue, TimeZone } from '../types';
|
||||
import { formattedValueToString } from '../valueFormats';
|
||||
import { getDisplayProcessor } from './displayProcessor';
|
||||
|
||||
/**
|
||||
* Creates a proxy object that allows accessing fields on dataFrame through various means and then returns it's
|
||||
* display value. This is mainly useful for example in data links interpolation where you can easily create a scoped
|
||||
* variable that will allow you to access dataFrame data with ${__data.fields.fieldName}.
|
||||
* Allows accessing fields by name, index, displayName or 'name' label
|
||||
*
|
||||
* @param frame
|
||||
* @param rowIndex
|
||||
* @param options
|
||||
* @internal
|
||||
*/
|
||||
@@ -15,7 +18,7 @@ export function getFieldDisplayValuesProxy(options: {
|
||||
timeZone?: TimeZone;
|
||||
}): Record<string, DisplayValue> {
|
||||
return new Proxy({} as Record<string, DisplayValue>, {
|
||||
get: (obj: any, key: string) => {
|
||||
get: (obj: any, key: string): DisplayValue | undefined => {
|
||||
// 1. Match the name
|
||||
let field = options.frame.fields.find((f) => key === f.name);
|
||||
if (!field) {
|
||||
@@ -39,11 +42,11 @@ export function getFieldDisplayValuesProxy(options: {
|
||||
if (!field) {
|
||||
return undefined;
|
||||
}
|
||||
if (!field.display) {
|
||||
throw new Error('Field missing display processor ' + field.name);
|
||||
}
|
||||
// TODO: we could supply the field here for the getDisplayProcessor fallback but we would also need theme which
|
||||
// we do not have access to here
|
||||
const displayProcessor = field.display ?? getDisplayProcessor();
|
||||
const raw = field.values.get(options.rowIndex);
|
||||
const disp = field.display(raw);
|
||||
const disp = displayProcessor(raw);
|
||||
disp.toString = () => formattedValueToString(disp);
|
||||
return disp;
|
||||
},
|
||||
|
||||
@@ -27,7 +27,7 @@ import SpanTreeOffset from './SpanTreeOffset';
|
||||
import SpanBar from './SpanBar';
|
||||
import Ticks from './Ticks';
|
||||
|
||||
import { TNil } from '../types';
|
||||
import { SpanLinkFunc, TNil } from '../types';
|
||||
import { TraceSpan } from '../types/trace';
|
||||
import { autoColor, createStyle, Theme, withTheme } from '../Theme';
|
||||
|
||||
@@ -294,9 +294,7 @@ type SpanBarRowProps = {
|
||||
removeHoverIndentGuideId: (spanID: string) => void;
|
||||
clippingLeft?: boolean;
|
||||
clippingRight?: boolean;
|
||||
createSpanLink?: (
|
||||
span: TraceSpan
|
||||
) => { href: string; onClick?: (e: React.MouseEvent) => void; content: React.ReactNode };
|
||||
createSpanLink?: SpanLinkFunc;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -443,27 +441,31 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
|
||||
{createSpanLink &&
|
||||
(() => {
|
||||
const link = createSpanLink(span);
|
||||
return (
|
||||
<a
|
||||
href={link.href}
|
||||
// Needs to have target otherwise preventDefault would not work due to angularRouter.
|
||||
target={'_blank'}
|
||||
style={{ marginRight: '5px' }}
|
||||
rel="noopener noreferrer"
|
||||
onClick={
|
||||
link.onClick
|
||||
? (event) => {
|
||||
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link.onClick) {
|
||||
event.preventDefault();
|
||||
link.onClick(event);
|
||||
if (link) {
|
||||
return (
|
||||
<a
|
||||
href={link.href}
|
||||
// Needs to have target otherwise preventDefault would not work due to angularRouter.
|
||||
target={'_blank'}
|
||||
style={{ marginRight: '5px' }}
|
||||
rel="noopener noreferrer"
|
||||
onClick={
|
||||
link.onClick
|
||||
? (event) => {
|
||||
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link.onClick) {
|
||||
event.preventDefault();
|
||||
link.onClick(event);
|
||||
}
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{link.content}
|
||||
</a>
|
||||
);
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{link.content}
|
||||
</a>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})()}
|
||||
|
||||
{span.references && span.references.length > 1 && (
|
||||
|
||||
@@ -24,14 +24,13 @@ import { formatDuration } from '../utils';
|
||||
import CopyIcon from '../../common/CopyIcon';
|
||||
import LabeledList from '../../common/LabeledList';
|
||||
|
||||
import { TNil } from '../../types';
|
||||
import { SpanLinkFunc, TNil } from '../../types';
|
||||
import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan } from '../../types/trace';
|
||||
import AccordianReferences from './AccordianReferences';
|
||||
import { autoColor, createStyle, Theme, useTheme } from '../../Theme';
|
||||
import { UIDivider } from '../../uiElementsContext';
|
||||
import { ubFlex, ubFlexAuto, ubItemsCenter, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles';
|
||||
import { DataLinkButton, TextArea } from '@grafana/ui';
|
||||
import { CreateSpanLink } from '../types';
|
||||
|
||||
const getStyles = createStyle((theme: Theme) => {
|
||||
return {
|
||||
@@ -116,7 +115,7 @@ type SpanDetailProps = {
|
||||
stackTracesToggle: (spanID: string) => void;
|
||||
referencesToggle: (spanID: string) => void;
|
||||
focusSpan: (uiFind: string) => void;
|
||||
createSpanLink?: CreateSpanLink;
|
||||
createSpanLink?: SpanLinkFunc;
|
||||
};
|
||||
|
||||
export default function SpanDetail(props: SpanDetailProps) {
|
||||
|
||||
@@ -22,7 +22,7 @@ import TimelineRow from './TimelineRow';
|
||||
import { autoColor, createStyle, Theme, withTheme } from '../Theme';
|
||||
|
||||
import { TraceLog, TraceSpan, TraceKeyValuePair, TraceLink } from '../types/trace';
|
||||
import { CreateSpanLink } from './types';
|
||||
import { SpanLinkFunc } from '../types';
|
||||
|
||||
const getStyles = createStyle((theme: Theme) => {
|
||||
return {
|
||||
@@ -86,7 +86,7 @@ type SpanDetailRowProps = {
|
||||
addHoverIndentGuideId: (spanID: string) => void;
|
||||
removeHoverIndentGuideId: (spanID: string) => void;
|
||||
theme: Theme;
|
||||
createSpanLink?: CreateSpanLink;
|
||||
createSpanLink?: SpanLinkFunc;
|
||||
};
|
||||
|
||||
export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProps> {
|
||||
|
||||
@@ -31,13 +31,12 @@ import {
|
||||
} from './utils';
|
||||
import { Accessors } from '../ScrollManager';
|
||||
import { getColorByKey } from '../utils/color-generator';
|
||||
import { TNil } from '../types';
|
||||
import { SpanLinkFunc, 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';
|
||||
|
||||
type TExtractUiFindFromStateReturn = {
|
||||
uiFind: string | undefined;
|
||||
@@ -84,7 +83,7 @@ type TVirtualizedTraceViewOwnProps = {
|
||||
addHoverIndentGuideId: (spanID: string) => void;
|
||||
removeHoverIndentGuideId: (spanID: string) => void;
|
||||
theme: Theme;
|
||||
createSpanLink?: CreateSpanLink;
|
||||
createSpanLink?: SpanLinkFunc;
|
||||
scrollElement?: Element;
|
||||
};
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ import VirtualizedTraceView from './VirtualizedTraceView';
|
||||
import { merge as mergeShortcuts } from '../keyboard-shortcuts';
|
||||
import { Accessors } from '../ScrollManager';
|
||||
import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from './types';
|
||||
import { TNil } from '../types';
|
||||
import { SpanLinkFunc, TNil } from '../types';
|
||||
import { TraceSpan, Trace, TraceLog, TraceKeyValuePair, TraceLink } from '../types/trace';
|
||||
import TTraceTimeline from '../types/TTraceTimeline';
|
||||
import { autoColor, createStyle, Theme, withTheme } from '../Theme';
|
||||
@@ -99,9 +99,7 @@ type TProps = TExtractUiFindFromStateReturn & {
|
||||
removeHoverIndentGuideId: (spanID: string) => void;
|
||||
linksGetter: (span: TraceSpan, items: TraceKeyValuePair[], itemIndex: number) => TraceLink[];
|
||||
theme: Theme;
|
||||
createSpanLink?: (
|
||||
span: TraceSpan
|
||||
) => { href: string; onClick?: (e: React.MouseEvent) => void; content: React.ReactNode };
|
||||
createSpanLink?: SpanLinkFunc;
|
||||
scrollElement?: Element;
|
||||
};
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
import { TraceSpan } from '../types/trace';
|
||||
import { TNil } from '../types';
|
||||
|
||||
interface TimeCursorUpdate {
|
||||
@@ -52,11 +51,3 @@ export interface ViewRangeTime {
|
||||
export interface ViewRange {
|
||||
time: ViewRangeTime;
|
||||
}
|
||||
|
||||
export type CreateSpanLink = (
|
||||
span: TraceSpan
|
||||
) => {
|
||||
href: string;
|
||||
onClick?: (e: React.MouseEvent) => void;
|
||||
content: React.ReactNode;
|
||||
};
|
||||
|
||||
@@ -16,7 +16,7 @@ import { ApiError } from './api-error';
|
||||
import { Trace } from './trace';
|
||||
|
||||
export { TraceSpan, TraceResponse, Trace, TraceProcess, TraceKeyValuePair, TraceLink } from './trace';
|
||||
|
||||
export { SpanLinkFunc, SpanLinkDef } from './links';
|
||||
export { default as TTraceTimeline } from './TTraceTimeline';
|
||||
export { default as TNil } from './TNil';
|
||||
|
||||
|
||||
10
packages/jaeger-ui-components/src/types/links.ts
Normal file
10
packages/jaeger-ui-components/src/types/links.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { TraceSpan } from './trace';
|
||||
import React from 'react';
|
||||
|
||||
export type SpanLinkDef = {
|
||||
href: string;
|
||||
onClick?: (event: any) => void;
|
||||
content: React.ReactNode;
|
||||
};
|
||||
|
||||
export type SpanLinkFunc = (span: TraceSpan) => SpanLinkDef | undefined;
|
||||
@@ -61,6 +61,7 @@ export type TraceSpanData = {
|
||||
stackTraces?: string[];
|
||||
flags: number;
|
||||
errorIconColor?: string;
|
||||
dataFrameRowIndex?: number;
|
||||
};
|
||||
|
||||
export type TraceSpan = TraceSpanData & {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"sort"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
@@ -201,8 +202,13 @@ func (e *cloudWatchExecutor) executeStartQuery(ctx context.Context, logsClient c
|
||||
logStreamIdentifierInternal + "|" + parameters.Get("queryString").MustString("")
|
||||
|
||||
startQueryInput := &cloudwatchlogs.StartQueryInput{
|
||||
StartTime: aws.Int64(startTime.Unix()),
|
||||
EndTime: aws.Int64(endTime.Unix()),
|
||||
StartTime: aws.Int64(startTime.Unix()),
|
||||
// Usually grafana time range allows only second precision, but you can create ranges with milliseconds
|
||||
// for example when going from trace to logs for that trace and trace length is sub second. In that case
|
||||
// StartTime is effectively floored while here EndTime is ceiled and so we should get the logs user wants
|
||||
// and also a little bit more but as CW logs accept only seconds as integers there is not much to do about
|
||||
// that.
|
||||
EndTime: aws.Int64(int64(math.Ceil(float64(endTime.UnixNano()) / 1e9))),
|
||||
LogGroupNames: aws.StringSlice(parameters.Get("logGroupNames").MustStringArray()),
|
||||
QueryString: aws.String(modifiedQueryString),
|
||||
}
|
||||
|
||||
@@ -1,34 +1,31 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { render, prettyDOM } from '@testing-library/react';
|
||||
import { render, prettyDOM, screen } from '@testing-library/react';
|
||||
import { TraceView } from './TraceView';
|
||||
import { TracePageHeader, TraceTimelineViewer } from '@jaegertracing/jaeger-ui-components';
|
||||
import { setDataSourceSrv } from '@grafana/runtime';
|
||||
import { ExploreId } from 'app/types';
|
||||
import { TraceData, TraceSpanData } from '@jaegertracing/jaeger-ui-components/src/types/trace';
|
||||
import { MutableDataFrame } from '@grafana/data';
|
||||
import { configureStore } from '../../../store/configureStore';
|
||||
import { Provider } from 'react-redux';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
useSelector: jest.fn(() => undefined),
|
||||
connect: jest.fn((v) => v),
|
||||
}));
|
||||
|
||||
function renderTraceView() {
|
||||
const wrapper = shallow(<TraceView exploreId={ExploreId.left} dataFrames={[frameOld]} splitOpenFn={() => {}} />);
|
||||
function renderTraceView(frames = [frameOld]) {
|
||||
const store = configureStore();
|
||||
const { container, baseElement } = render(
|
||||
<Provider store={store}>
|
||||
<TraceView exploreId={ExploreId.left} dataFrames={frames} splitOpenFn={() => {}} />
|
||||
</Provider>
|
||||
);
|
||||
return {
|
||||
timeline: wrapper.find(TraceTimelineViewer),
|
||||
header: wrapper.find(TracePageHeader),
|
||||
wrapper,
|
||||
header: container.children[0],
|
||||
timeline: container.children[1],
|
||||
container,
|
||||
baseElement,
|
||||
};
|
||||
}
|
||||
|
||||
function renderTraceViewNew() {
|
||||
const wrapper = shallow(<TraceView exploreId={ExploreId.left} dataFrames={[frameNew]} splitOpenFn={() => {}} />);
|
||||
return {
|
||||
timeline: wrapper.find(TraceTimelineViewer),
|
||||
header: wrapper.find(TracePageHeader),
|
||||
wrapper,
|
||||
};
|
||||
return renderTraceView([frameNew]);
|
||||
}
|
||||
|
||||
describe('TraceView', () => {
|
||||
@@ -42,132 +39,83 @@ describe('TraceView', () => {
|
||||
|
||||
it('renders TraceTimelineViewer', () => {
|
||||
const { timeline, header } = renderTraceView();
|
||||
expect(timeline).toHaveLength(1);
|
||||
expect(header).toHaveLength(1);
|
||||
expect(timeline).toBeDefined();
|
||||
expect(header).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders TraceTimelineViewer with new format', () => {
|
||||
const { timeline, header } = renderTraceViewNew();
|
||||
expect(timeline).toHaveLength(1);
|
||||
expect(header).toHaveLength(1);
|
||||
expect(timeline).toBeDefined();
|
||||
expect(header).toBeDefined();
|
||||
});
|
||||
|
||||
it('renders renders the same for old and new format', () => {
|
||||
const { baseElement } = render(
|
||||
<TraceView exploreId={ExploreId.left} dataFrames={[frameNew]} splitOpenFn={() => {}} />
|
||||
);
|
||||
const { baseElement: baseElementOld } = render(
|
||||
<TraceView exploreId={ExploreId.left} dataFrames={[frameOld]} splitOpenFn={() => {}} />
|
||||
);
|
||||
const { baseElement } = renderTraceViewNew();
|
||||
const { baseElement: baseElementOld } = renderTraceView();
|
||||
expect(prettyDOM(baseElement)).toEqual(prettyDOM(baseElementOld));
|
||||
});
|
||||
|
||||
it('does not render anything on missing trace', () => {
|
||||
// Simulating Explore's access to empty response data
|
||||
const { container } = render(<TraceView exploreId={ExploreId.left} dataFrames={[]} splitOpenFn={() => {}} />);
|
||||
const { container } = renderTraceView([]);
|
||||
expect(container.hasChildNodes()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('toggles detailState', () => {
|
||||
let { timeline, wrapper } = renderTraceViewNew();
|
||||
expect(timeline.props().traceTimeline.detailStates.size).toBe(0);
|
||||
it('toggles detailState', async () => {
|
||||
renderTraceViewNew();
|
||||
expect(screen.queryByText(/Tags/)).toBeFalsy();
|
||||
const spanView = screen.getAllByText('', { selector: 'div[data-test-id="span-view"]' })[0];
|
||||
userEvent.click(spanView);
|
||||
expect(screen.queryByText(/Tags/)).toBeTruthy();
|
||||
|
||||
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);
|
||||
userEvent.click(spanView);
|
||||
screen.debug(screen.queryAllByText(/Tags/));
|
||||
expect(screen.queryByText(/Tags/)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('toggles children visibility', () => {
|
||||
let { timeline, wrapper } = renderTraceViewNew();
|
||||
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0);
|
||||
renderTraceViewNew();
|
||||
expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
|
||||
userEvent.click(screen.getAllByText('', { selector: 'span[data-test-id="SpanTreeOffset--indentGuide"]' })[0]);
|
||||
expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(1);
|
||||
|
||||
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 } = renderTraceViewNew();
|
||||
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);
|
||||
userEvent.click(screen.getAllByText('', { selector: 'span[data-test-id="SpanTreeOffset--indentGuide"]' })[0]);
|
||||
expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
|
||||
});
|
||||
|
||||
it('toggles collapses and expands one level of spans', () => {
|
||||
let { timeline, wrapper } = renderTraceViewNew();
|
||||
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);
|
||||
renderTraceViewNew();
|
||||
expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
|
||||
userEvent.click(screen.getByLabelText('Collapse +1'));
|
||||
expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(2);
|
||||
userEvent.click(screen.getByLabelText('Expand +1'));
|
||||
expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
|
||||
});
|
||||
|
||||
it('toggles collapses and expands all levels', () => {
|
||||
let { timeline, wrapper } = renderTraceViewNew();
|
||||
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);
|
||||
renderTraceViewNew();
|
||||
expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
|
||||
userEvent.click(screen.getByLabelText('Collapse All'));
|
||||
expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(1);
|
||||
userEvent.click(screen.getByLabelText('Expand All'));
|
||||
expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
|
||||
});
|
||||
|
||||
it('searches for spans', () => {
|
||||
let { wrapper, header } = renderTraceViewNew();
|
||||
header.props().onSearchValueChange('HTTP POST - api_prom_push');
|
||||
|
||||
const timeline = wrapper.find(TraceTimelineViewer);
|
||||
expect(timeline.props().findMatchesIDs?.has('1ed38015486087ca')).toBeTruthy();
|
||||
renderTraceViewNew();
|
||||
userEvent.type(screen.getByPlaceholderText('Find...'), '1ed38015486087ca');
|
||||
expect(
|
||||
(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' })[0].parentNode! as HTMLElement).className
|
||||
).toContain('rowMatchingFilter');
|
||||
});
|
||||
|
||||
it('change viewRange', () => {
|
||||
let { header, timeline, wrapper } = renderTraceViewNew();
|
||||
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);
|
||||
it('shows timeline ticks', () => {
|
||||
renderTraceViewNew();
|
||||
function ticks() {
|
||||
return screen.getByText('', { selector: 'div[data-test-id="TimelineHeaderRow"]' }).children[1].children[1]
|
||||
.textContent;
|
||||
}
|
||||
expect(ticks()).toBe('0μs274.5μs549μs823.5μs1.1ms');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -63,7 +63,9 @@ export function TraceView(props: Props) {
|
||||
*/
|
||||
const [slim, setSlim] = useState(false);
|
||||
|
||||
const traceProp = useMemo(() => transformDataFrames(props.dataFrames), [props.dataFrames]);
|
||||
// At this point we only show single trace.
|
||||
const frame = props.dataFrames[0];
|
||||
const traceProp = useMemo(() => transformDataFrames(frame), [frame]);
|
||||
const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
|
||||
const dataSourceName = useSelector((state: StoreState) => state.explore[props.exploreId]?.datasourceInstance?.name);
|
||||
const traceToLogsOptions = (getDatasourceSrv().getInstanceSettings(dataSourceName)?.jsonData as TraceToLogsData)
|
||||
@@ -97,10 +99,10 @@ export function TraceView(props: Props) {
|
||||
[childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, traceProp?.traceID]
|
||||
);
|
||||
|
||||
const createSpanLink = useMemo(() => createSpanLinkFactory(props.splitOpenFn, traceToLogsOptions), [
|
||||
props.splitOpenFn,
|
||||
traceToLogsOptions,
|
||||
]);
|
||||
const createSpanLink = useMemo(
|
||||
() => createSpanLinkFactory({ splitOpenFn: props.splitOpenFn, traceToLogsOptions, dataFrame: frame }),
|
||||
[props.splitOpenFn, traceToLogsOptions, frame]
|
||||
);
|
||||
const scrollElement = document.getElementsByClassName('scrollbar-view')[0];
|
||||
const onSlimViewClicked = useCallback(() => setSlim(!slim), [slim]);
|
||||
|
||||
@@ -173,9 +175,7 @@ export function TraceView(props: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
function transformDataFrames(frames: DataFrame[]): Trace | null {
|
||||
// At this point we only show single trace.
|
||||
const frame = frames[0];
|
||||
function transformDataFrames(frame?: DataFrame): Trace | null {
|
||||
if (!frame) {
|
||||
return null;
|
||||
}
|
||||
@@ -203,7 +203,7 @@ function transformTraceDataFrame(frame: DataFrame): TraceResponse {
|
||||
return {
|
||||
traceID: view.get(0).traceID,
|
||||
processes,
|
||||
spans: view.toArray().map((s) => {
|
||||
spans: view.toArray().map((s, index) => {
|
||||
return {
|
||||
...s,
|
||||
duration: s.duration * 1000,
|
||||
@@ -212,6 +212,7 @@ function transformTraceDataFrame(frame: DataFrame): TraceResponse {
|
||||
flags: 0,
|
||||
references: s.parentSpanID ? [{ refType: 'CHILD_OF', spanID: s.parentSpanID, traceID: s.traceID }] : undefined,
|
||||
logs: s.logs?.map((l) => ({ ...l, timestamp: l.timestamp * 1000 })) || [],
|
||||
dataFrameRowIndex: index,
|
||||
};
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
import { DataSourceInstanceSettings, ScopedVars } from '@grafana/data';
|
||||
import { DataSourceInstanceSettings, MutableDataFrame } from '@grafana/data';
|
||||
import { setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
|
||||
import { createSpanLinkFactory } from './createSpanLink';
|
||||
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceToLogsOptions } from '../../../core/components/TraceToLogsSettings';
|
||||
import { LinkSrv, setLinkSrv } from '../../../angular/panel/panellinks/link_srv';
|
||||
import { TemplateSrv } from '../../templating/template_srv';
|
||||
|
||||
describe('createSpanLinkFactory', () => {
|
||||
it('returns undefined if there is no data source uid', () => {
|
||||
const splitOpenFn = jest.fn();
|
||||
const createLink = createSpanLinkFactory(splitOpenFn);
|
||||
const createLink = createSpanLinkFactory({ splitOpenFn: splitOpenFn });
|
||||
expect(createLink).not.toBeDefined();
|
||||
});
|
||||
|
||||
@@ -13,52 +17,19 @@ describe('createSpanLinkFactory', () => {
|
||||
beforeAll(() => {
|
||||
setDataSourceSrv({
|
||||
getInstanceSettings(uid: string): DataSourceInstanceSettings | undefined {
|
||||
return {
|
||||
uid: 'loki1',
|
||||
name: 'loki1',
|
||||
} as any;
|
||||
return { uid: 'loki1', name: 'loki1' } as any;
|
||||
},
|
||||
} as any);
|
||||
|
||||
setTemplateSrv({
|
||||
replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string {
|
||||
return target!;
|
||||
},
|
||||
} as any);
|
||||
setLinkSrv(new LinkSrv());
|
||||
setTemplateSrv(new TemplateSrv());
|
||||
});
|
||||
|
||||
it('with default keys when tags not configured', () => {
|
||||
const splitOpenFn = jest.fn();
|
||||
const createLink = createSpanLinkFactory(splitOpenFn, { datasourceUid: 'lokiUid' });
|
||||
const createLink = setupSpanLinkFactory();
|
||||
expect(createLink).toBeDefined();
|
||||
const linkDef = createLink!({
|
||||
startTime: new Date('2020-10-14T01:00:00Z').valueOf() * 1000,
|
||||
duration: 1000 * 1000,
|
||||
tags: [
|
||||
{
|
||||
key: 'host',
|
||||
value: 'host',
|
||||
},
|
||||
],
|
||||
process: {
|
||||
tags: [
|
||||
{
|
||||
key: 'cluster',
|
||||
value: 'cluster1',
|
||||
},
|
||||
{
|
||||
key: 'hostname',
|
||||
value: 'hostname1',
|
||||
},
|
||||
{
|
||||
key: 'label2',
|
||||
value: 'val2',
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
} as any);
|
||||
|
||||
expect(linkDef.href).toBe(
|
||||
const linkDef = createLink!(createTraceSpan());
|
||||
expect(linkDef!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"}","refId":""}]}'
|
||||
)}`
|
||||
@@ -66,33 +37,22 @@ describe('createSpanLinkFactory', () => {
|
||||
});
|
||||
|
||||
it('with tags that passed in and without tags that are not in the span', () => {
|
||||
const splitOpenFn = jest.fn();
|
||||
const createLink = createSpanLinkFactory(splitOpenFn, { datasourceUid: 'lokiUid', tags: ['ip', 'newTag'] });
|
||||
const createLink = setupSpanLinkFactory({
|
||||
tags: ['ip', 'newTag'],
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const linkDef = createLink!({
|
||||
startTime: new Date('2020-10-14T01:00:00Z').valueOf() * 1000,
|
||||
duration: 1000 * 1000,
|
||||
tags: [
|
||||
{
|
||||
key: 'host',
|
||||
value: 'host',
|
||||
const linkDef = createLink!(
|
||||
createTraceSpan({
|
||||
process: {
|
||||
serviceName: 'service',
|
||||
tags: [
|
||||
{ key: 'hostname', value: 'hostname1' },
|
||||
{ key: 'ip', value: '192.168.0.1' },
|
||||
],
|
||||
},
|
||||
],
|
||||
process: {
|
||||
tags: [
|
||||
{
|
||||
key: 'hostname',
|
||||
value: 'hostname1',
|
||||
},
|
||||
{
|
||||
key: 'ip',
|
||||
value: '192.168.0.1',
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
} as any);
|
||||
|
||||
expect(linkDef.href).toBe(
|
||||
})
|
||||
);
|
||||
expect(linkDef!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{ip=\\"192.168.0.1\\"}","refId":""}]}'
|
||||
)}`
|
||||
@@ -100,36 +60,22 @@ describe('createSpanLinkFactory', () => {
|
||||
});
|
||||
|
||||
it('from tags and process tags as well', () => {
|
||||
const splitOpenFn = jest.fn();
|
||||
const createLink = createSpanLinkFactory(splitOpenFn, {
|
||||
datasourceUid: 'lokiUid',
|
||||
const createLink = setupSpanLinkFactory({
|
||||
tags: ['ip', 'host'],
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const linkDef = createLink!({
|
||||
startTime: new Date('2020-10-14T01:00:00Z').valueOf() * 1000,
|
||||
duration: 1000 * 1000,
|
||||
tags: [
|
||||
{
|
||||
key: 'host',
|
||||
value: 'host',
|
||||
const linkDef = createLink!(
|
||||
createTraceSpan({
|
||||
process: {
|
||||
serviceName: 'service',
|
||||
tags: [
|
||||
{ key: 'hostname', value: 'hostname1' },
|
||||
{ key: 'ip', value: '192.168.0.1' },
|
||||
],
|
||||
},
|
||||
],
|
||||
process: {
|
||||
tags: [
|
||||
{
|
||||
key: 'hostname',
|
||||
value: 'hostname1',
|
||||
},
|
||||
{
|
||||
key: 'ip',
|
||||
value: '192.168.0.1',
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
} as any);
|
||||
|
||||
expect(linkDef.href).toBe(
|
||||
})
|
||||
);
|
||||
expect(linkDef!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{ip=\\"192.168.0.1\\", host=\\"host\\"}","refId":""}]}'
|
||||
)}`
|
||||
@@ -137,38 +83,23 @@ describe('createSpanLinkFactory', () => {
|
||||
});
|
||||
|
||||
it('with adjusted start and end time', () => {
|
||||
const splitOpenFn = jest.fn();
|
||||
const createLink = createSpanLinkFactory(splitOpenFn, {
|
||||
datasourceUid: 'lokiUid',
|
||||
const createLink = setupSpanLinkFactory({
|
||||
spanStartTimeShift: '1m',
|
||||
spanEndTimeShift: '1m',
|
||||
});
|
||||
|
||||
expect(createLink).toBeDefined();
|
||||
const linkDef = createLink!({
|
||||
startTime: new Date('2020-10-14T01:00:00Z').valueOf() * 1000,
|
||||
duration: 1000 * 1000,
|
||||
tags: [
|
||||
{
|
||||
key: 'host',
|
||||
value: 'host',
|
||||
const linkDef = createLink!(
|
||||
createTraceSpan({
|
||||
process: {
|
||||
serviceName: 'service',
|
||||
tags: [
|
||||
{ key: 'hostname', value: 'hostname1' },
|
||||
{ key: 'ip', value: '192.168.0.1' },
|
||||
],
|
||||
},
|
||||
],
|
||||
process: {
|
||||
tags: [
|
||||
{
|
||||
key: 'hostname',
|
||||
value: 'hostname1',
|
||||
},
|
||||
{
|
||||
key: 'ip',
|
||||
value: '192.168.0.1',
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
} as any);
|
||||
|
||||
expect(linkDef.href).toBe(
|
||||
})
|
||||
);
|
||||
expect(linkDef!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:01:00.000Z","to":"2020-10-14T01:01:01.000Z"},"datasource":"loki1","queries":[{"expr":"{hostname=\\"hostname1\\"}","refId":""}]}'
|
||||
)}`
|
||||
@@ -176,47 +107,89 @@ describe('createSpanLinkFactory', () => {
|
||||
});
|
||||
|
||||
it('filters by trace and span ID', () => {
|
||||
const splitOpenFn = jest.fn();
|
||||
const createLink = createSpanLinkFactory(splitOpenFn, {
|
||||
datasourceUid: 'lokiUid',
|
||||
const createLink = setupSpanLinkFactory({
|
||||
filterBySpanID: true,
|
||||
filterByTraceID: true,
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const linkDef = createLink!({
|
||||
spanID: '6605c7b08e715d6c',
|
||||
traceID: '7946b05c2e2e4e5a',
|
||||
startTime: new Date('2020-10-14T01:00:00Z').valueOf() * 1000,
|
||||
duration: 1000 * 1000,
|
||||
tags: [
|
||||
{
|
||||
key: 'host',
|
||||
value: 'host',
|
||||
},
|
||||
],
|
||||
process: {
|
||||
tags: [
|
||||
{
|
||||
key: 'cluster',
|
||||
value: 'cluster1',
|
||||
},
|
||||
{
|
||||
key: 'hostname',
|
||||
value: 'hostname1',
|
||||
},
|
||||
{
|
||||
key: 'label2',
|
||||
value: 'val2',
|
||||
},
|
||||
],
|
||||
} as any,
|
||||
} as any);
|
||||
const linkDef = createLink!(createTraceSpan());
|
||||
|
||||
expect(linkDef.href).toBe(
|
||||
expect(linkDef!.href).toBe(
|
||||
`/explore?left=${encodeURIComponent(
|
||||
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"loki1","queries":[{"expr":"{cluster=\\"cluster1\\", hostname=\\"hostname1\\"} |=\\"7946b05c2e2e4e5a\\" |=\\"6605c7b08e715d6c\\"","refId":""}]}'
|
||||
)}`
|
||||
);
|
||||
});
|
||||
|
||||
it('creates link from dataFrame', () => {
|
||||
const splitOpenFn = jest.fn();
|
||||
const createLink = createSpanLinkFactory({
|
||||
splitOpenFn,
|
||||
dataFrame: new MutableDataFrame({
|
||||
fields: [
|
||||
{ name: 'traceID', values: ['testTraceId'] },
|
||||
{
|
||||
name: 'spanID',
|
||||
config: { links: [{ title: 'link', url: '${__data.fields.spanID}' }] },
|
||||
values: ['testSpanId'],
|
||||
},
|
||||
],
|
||||
}),
|
||||
});
|
||||
expect(createLink).toBeDefined();
|
||||
const linkDef = createLink!(createTraceSpan());
|
||||
|
||||
expect(linkDef!.href).toBe('testSpanId');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function setupSpanLinkFactory(options: Partial<TraceToLogsOptions> = {}) {
|
||||
const splitOpenFn = jest.fn();
|
||||
return createSpanLinkFactory({
|
||||
splitOpenFn,
|
||||
traceToLogsOptions: {
|
||||
datasourceUid: 'lokiUid',
|
||||
...options,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function createTraceSpan(overrides: Partial<TraceSpan> = {}): TraceSpan {
|
||||
return {
|
||||
spanID: '6605c7b08e715d6c',
|
||||
traceID: '7946b05c2e2e4e5a',
|
||||
processID: 'processId',
|
||||
operationName: 'operation',
|
||||
logs: [],
|
||||
startTime: new Date('2020-10-14T01:00:00Z').valueOf() * 1000,
|
||||
duration: 1000 * 1000,
|
||||
flags: 0,
|
||||
hasChildren: false,
|
||||
dataFrameRowIndex: 0,
|
||||
tags: [
|
||||
{
|
||||
key: 'host',
|
||||
value: 'host',
|
||||
},
|
||||
],
|
||||
process: {
|
||||
serviceName: 'test service',
|
||||
tags: [
|
||||
{
|
||||
key: 'cluster',
|
||||
value: 'cluster1',
|
||||
},
|
||||
{
|
||||
key: 'hostname',
|
||||
value: 'hostname1',
|
||||
},
|
||||
{
|
||||
key: 'label2',
|
||||
value: 'val2',
|
||||
},
|
||||
],
|
||||
},
|
||||
...overrides,
|
||||
} as any;
|
||||
}
|
||||
|
||||
@@ -1,18 +1,68 @@
|
||||
import { DataLink, dateTime, Field, mapInternalLinkToExplore, rangeUtil, SplitOpen, TimeRange } from '@grafana/data';
|
||||
import {
|
||||
DataFrame,
|
||||
DataLink,
|
||||
dateTime,
|
||||
Field,
|
||||
mapInternalLinkToExplore,
|
||||
rangeUtil,
|
||||
SplitOpen,
|
||||
TimeRange,
|
||||
} from '@grafana/data';
|
||||
import { getTemplateSrv } from '@grafana/runtime';
|
||||
import { Icon } from '@grafana/ui';
|
||||
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
import { SpanLinkDef, SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings';
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import React from 'react';
|
||||
import { LokiQuery } from '../../../plugins/datasource/loki/types';
|
||||
import { getFieldLinksForExplore } from '../utils/links';
|
||||
|
||||
/**
|
||||
* This is a factory for the link creator. It returns the function mainly so it can return undefined in which case
|
||||
* the trace view won't create any links and to capture the datasource and split function making it easier to memoize
|
||||
* with useMemo.
|
||||
*/
|
||||
export function createSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions?: TraceToLogsOptions) {
|
||||
export function createSpanLinkFactory({
|
||||
splitOpenFn,
|
||||
traceToLogsOptions,
|
||||
dataFrame,
|
||||
}: {
|
||||
splitOpenFn: SplitOpen;
|
||||
traceToLogsOptions?: TraceToLogsOptions;
|
||||
dataFrame?: DataFrame;
|
||||
}): SpanLinkFunc | undefined {
|
||||
if (!dataFrame || dataFrame.fields.length === 1 || !dataFrame.fields.some((f) => Boolean(f.config.links?.length))) {
|
||||
// if the dataframe contains just a single blob of data (legacy format) or does not have any links configured,
|
||||
// let's try to use the old legacy path.
|
||||
return legacyCreateSpanLinkFactory(splitOpenFn, traceToLogsOptions);
|
||||
} else {
|
||||
return function (span: TraceSpan): SpanLinkDef | undefined {
|
||||
// We should be here only if there are some links in the dataframe
|
||||
const field = dataFrame.fields.find((f) => Boolean(f.config.links?.length))!;
|
||||
try {
|
||||
const links = getFieldLinksForExplore({
|
||||
field,
|
||||
rowIndex: span.dataFrameRowIndex!,
|
||||
splitOpenFn,
|
||||
range: getTimeRangeFromSpan(span),
|
||||
dataFrame,
|
||||
});
|
||||
|
||||
return {
|
||||
href: links[0].href,
|
||||
onClick: links[0].onClick,
|
||||
content: <Icon name="gf-logs" title="Explore the logs for this in split view" />,
|
||||
};
|
||||
} catch (error) {
|
||||
// It's fairly easy to crash here for example if data source defines wrong interpolation in the data link
|
||||
console.error(error);
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
function legacyCreateSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions?: TraceToLogsOptions) {
|
||||
// We should return if dataSourceUid is undefined otherwise getInstanceSettings would return testDataSource.
|
||||
if (!traceToLogsOptions?.datasourceUid) {
|
||||
return undefined;
|
||||
@@ -24,7 +74,7 @@ export function createSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return function (span: TraceSpan): { href: string; onClick?: (event: any) => void; content: React.ReactNode } {
|
||||
return function (span: TraceSpan): SpanLinkDef {
|
||||
// This is reusing existing code from derived fields which may not be ideal match so some data is a bit faked at
|
||||
// the moment. Issue is that the trace itself isn't clearly mapped to dataFrame (right now it's just a json blob
|
||||
// inside a single field) so the dataLinks as config of that dataFrame abstraction breaks down a bit and we do
|
||||
@@ -47,7 +97,12 @@ export function createSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions
|
||||
link: dataLink,
|
||||
internalLink: dataLink.internal!,
|
||||
scopedVars: {},
|
||||
range: getTimeRangeFromSpan(span, traceToLogsOptions),
|
||||
range: getTimeRangeFromSpan(span, {
|
||||
startMs: traceToLogsOptions.spanStartTimeShift
|
||||
? rangeUtil.intervalToMs(traceToLogsOptions.spanStartTimeShift)
|
||||
: 0,
|
||||
endMs: traceToLogsOptions.spanEndTimeShift ? rangeUtil.intervalToMs(traceToLogsOptions.spanEndTimeShift) : 0,
|
||||
}),
|
||||
field: {} as Field,
|
||||
onClickFn: splitOpenFn,
|
||||
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
|
||||
@@ -91,15 +146,14 @@ function getLokiQueryFromSpan(span: TraceSpan, options: TraceToLogsOptions): str
|
||||
/**
|
||||
* Gets a time range from the span.
|
||||
*/
|
||||
function getTimeRangeFromSpan(span: TraceSpan, traceToLogsOptions?: TraceToLogsOptions): TimeRange {
|
||||
const adjustedStartTime = traceToLogsOptions?.spanStartTimeShift
|
||||
? Math.floor(span.startTime / 1000 + rangeUtil.intervalToMs(traceToLogsOptions.spanStartTimeShift))
|
||||
: Math.floor(span.startTime / 1000);
|
||||
function getTimeRangeFromSpan(
|
||||
span: TraceSpan,
|
||||
timeShift: { startMs: number; endMs: number } = { startMs: 0, endMs: 0 }
|
||||
): TimeRange {
|
||||
const adjustedStartTime = Math.floor(span.startTime / 1000 + timeShift.startMs);
|
||||
const from = dateTime(adjustedStartTime);
|
||||
const spanEndMs = (span.startTime + span.duration) / 1000;
|
||||
let adjustedEndTime = traceToLogsOptions?.spanEndTimeShift
|
||||
? Math.floor(spanEndMs + rangeUtil.intervalToMs(traceToLogsOptions.spanEndTimeShift))
|
||||
: Math.floor(spanEndMs);
|
||||
let adjustedEndTime = Math.floor(spanEndMs + timeShift.endMs);
|
||||
|
||||
// Because we can only pass milliseconds in the url we need to check if they equal.
|
||||
// We need end time to be later than start time
|
||||
|
||||
Reference in New Issue
Block a user