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:
Andrej Ocenas
2021-10-27 18:40:40 +02:00
committed by GitHub
parent 641a18b92e
commit 00ffe1a4fd
16 changed files with 337 additions and 342 deletions
@@ -1,6 +1,6 @@
import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy'; import { getFieldDisplayValuesProxy } from './getFieldDisplayValuesProxy';
import { applyFieldOverrides } from './fieldOverrides'; import { applyFieldOverrides } from './fieldOverrides';
import { toDataFrame } from '../dataframe'; import { MutableDataFrame, toDataFrame } from '../dataframe';
import { createTheme } from '../themes'; import { createTheme } from '../themes';
describe('getFieldDisplayValuesProxy', () => { describe('getFieldDisplayValuesProxy', () => {
@@ -81,4 +81,14 @@ describe('getFieldDisplayValuesProxy', () => {
expect(p.xyz).toBeUndefined(); expect(p.xyz).toBeUndefined();
expect(p[100]).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 { toNumber } from 'lodash';
import { DataFrame, DisplayValue, TimeZone } from '../types'; import { DataFrame, DisplayValue, TimeZone } from '../types';
import { formattedValueToString } from '../valueFormats'; 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 * @param options
* @internal * @internal
*/ */
@@ -15,7 +18,7 @@ export function getFieldDisplayValuesProxy(options: {
timeZone?: TimeZone; timeZone?: TimeZone;
}): Record<string, DisplayValue> { }): Record<string, DisplayValue> {
return new Proxy({} as 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 // 1. Match the name
let field = options.frame.fields.find((f) => key === f.name); let field = options.frame.fields.find((f) => key === f.name);
if (!field) { if (!field) {
@@ -39,11 +42,11 @@ export function getFieldDisplayValuesProxy(options: {
if (!field) { if (!field) {
return undefined; return undefined;
} }
if (!field.display) { // TODO: we could supply the field here for the getDisplayProcessor fallback but we would also need theme which
throw new Error('Field missing display processor ' + field.name); // we do not have access to here
} const displayProcessor = field.display ?? getDisplayProcessor();
const raw = field.values.get(options.rowIndex); const raw = field.values.get(options.rowIndex);
const disp = field.display(raw); const disp = displayProcessor(raw);
disp.toString = () => formattedValueToString(disp); disp.toString = () => formattedValueToString(disp);
return disp; return disp;
}, },
@@ -27,7 +27,7 @@ import SpanTreeOffset from './SpanTreeOffset';
import SpanBar from './SpanBar'; import SpanBar from './SpanBar';
import Ticks from './Ticks'; import Ticks from './Ticks';
import { TNil } from '../types'; import { SpanLinkFunc, TNil } from '../types';
import { TraceSpan } from '../types/trace'; import { TraceSpan } from '../types/trace';
import { autoColor, createStyle, Theme, withTheme } from '../Theme'; import { autoColor, createStyle, Theme, withTheme } from '../Theme';
@@ -294,9 +294,7 @@ type SpanBarRowProps = {
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
clippingLeft?: boolean; clippingLeft?: boolean;
clippingRight?: boolean; clippingRight?: boolean;
createSpanLink?: ( createSpanLink?: SpanLinkFunc;
span: TraceSpan
) => { href: string; onClick?: (e: React.MouseEvent) => void; content: React.ReactNode };
}; };
/** /**
@@ -443,27 +441,31 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
{createSpanLink && {createSpanLink &&
(() => { (() => {
const link = createSpanLink(span); const link = createSpanLink(span);
return ( if (link) {
<a return (
href={link.href} <a
// Needs to have target otherwise preventDefault would not work due to angularRouter. href={link.href}
target={'_blank'} // Needs to have target otherwise preventDefault would not work due to angularRouter.
style={{ marginRight: '5px' }} target={'_blank'}
rel="noopener noreferrer" style={{ marginRight: '5px' }}
onClick={ rel="noopener noreferrer"
link.onClick onClick={
? (event) => { link.onClick
if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link.onClick) { ? (event) => {
event.preventDefault(); if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link.onClick) {
link.onClick(event); event.preventDefault();
link.onClick(event);
}
} }
} : undefined
: undefined }
} >
> {link.content}
{link.content} </a>
</a> );
); } else {
return null;
}
})()} })()}
{span.references && span.references.length > 1 && ( {span.references && span.references.length > 1 && (
@@ -24,14 +24,13 @@ import { formatDuration } from '../utils';
import CopyIcon from '../../common/CopyIcon'; import CopyIcon from '../../common/CopyIcon';
import LabeledList from '../../common/LabeledList'; import LabeledList from '../../common/LabeledList';
import { TNil } from '../../types'; import { SpanLinkFunc, TNil } from '../../types';
import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan } from '../../types/trace'; import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan } from '../../types/trace';
import AccordianReferences from './AccordianReferences'; import AccordianReferences from './AccordianReferences';
import { autoColor, createStyle, Theme, useTheme } from '../../Theme'; import { autoColor, createStyle, Theme, useTheme } from '../../Theme';
import { UIDivider } from '../../uiElementsContext'; import { UIDivider } from '../../uiElementsContext';
import { ubFlex, ubFlexAuto, ubItemsCenter, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles'; import { ubFlex, ubFlexAuto, ubItemsCenter, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles';
import { DataLinkButton, TextArea } from '@grafana/ui'; import { DataLinkButton, TextArea } from '@grafana/ui';
import { CreateSpanLink } from '../types';
const getStyles = createStyle((theme: Theme) => { const getStyles = createStyle((theme: Theme) => {
return { return {
@@ -116,7 +115,7 @@ type SpanDetailProps = {
stackTracesToggle: (spanID: string) => void; stackTracesToggle: (spanID: string) => void;
referencesToggle: (spanID: string) => void; referencesToggle: (spanID: string) => void;
focusSpan: (uiFind: string) => void; focusSpan: (uiFind: string) => void;
createSpanLink?: CreateSpanLink; createSpanLink?: SpanLinkFunc;
}; };
export default function SpanDetail(props: SpanDetailProps) { export default function SpanDetail(props: SpanDetailProps) {
@@ -22,7 +22,7 @@ import TimelineRow from './TimelineRow';
import { autoColor, createStyle, Theme, withTheme } from '../Theme'; import { autoColor, createStyle, Theme, withTheme } from '../Theme';
import { TraceLog, TraceSpan, TraceKeyValuePair, TraceLink } from '../types/trace'; import { TraceLog, TraceSpan, TraceKeyValuePair, TraceLink } from '../types/trace';
import { CreateSpanLink } from './types'; import { SpanLinkFunc } from '../types';
const getStyles = createStyle((theme: Theme) => { const getStyles = createStyle((theme: Theme) => {
return { return {
@@ -86,7 +86,7 @@ type SpanDetailRowProps = {
addHoverIndentGuideId: (spanID: string) => void; addHoverIndentGuideId: (spanID: string) => void;
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
theme: Theme; theme: Theme;
createSpanLink?: CreateSpanLink; createSpanLink?: SpanLinkFunc;
}; };
export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProps> { export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProps> {
@@ -31,13 +31,12 @@ import {
} from './utils'; } from './utils';
import { Accessors } from '../ScrollManager'; import { Accessors } from '../ScrollManager';
import { getColorByKey } from '../utils/color-generator'; 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 { TraceLog, TraceSpan, Trace, TraceKeyValuePair, TraceLink } from '../types/trace';
import TTraceTimeline from '../types/TTraceTimeline'; import TTraceTimeline from '../types/TTraceTimeline';
import { PEER_SERVICE } from '../constants/tag-keys'; import { PEER_SERVICE } from '../constants/tag-keys';
import { createStyle, Theme, withTheme } from '../Theme'; import { createStyle, Theme, withTheme } from '../Theme';
import { CreateSpanLink } from './types';
type TExtractUiFindFromStateReturn = { type TExtractUiFindFromStateReturn = {
uiFind: string | undefined; uiFind: string | undefined;
@@ -84,7 +83,7 @@ type TVirtualizedTraceViewOwnProps = {
addHoverIndentGuideId: (spanID: string) => void; addHoverIndentGuideId: (spanID: string) => void;
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
theme: Theme; theme: Theme;
createSpanLink?: CreateSpanLink; createSpanLink?: SpanLinkFunc;
scrollElement?: Element; scrollElement?: Element;
}; };
@@ -20,7 +20,7 @@ import VirtualizedTraceView from './VirtualizedTraceView';
import { merge as mergeShortcuts } from '../keyboard-shortcuts'; import { merge as mergeShortcuts } from '../keyboard-shortcuts';
import { Accessors } from '../ScrollManager'; import { Accessors } from '../ScrollManager';
import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from './types'; 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 { TraceSpan, Trace, TraceLog, TraceKeyValuePair, TraceLink } from '../types/trace';
import TTraceTimeline from '../types/TTraceTimeline'; import TTraceTimeline from '../types/TTraceTimeline';
import { autoColor, createStyle, Theme, withTheme } from '../Theme'; import { autoColor, createStyle, Theme, withTheme } from '../Theme';
@@ -99,9 +99,7 @@ type TProps = TExtractUiFindFromStateReturn & {
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
linksGetter: (span: TraceSpan, items: TraceKeyValuePair[], itemIndex: number) => TraceLink[]; linksGetter: (span: TraceSpan, items: TraceKeyValuePair[], itemIndex: number) => TraceLink[];
theme: Theme; theme: Theme;
createSpanLink?: ( createSpanLink?: SpanLinkFunc;
span: TraceSpan
) => { href: string; onClick?: (e: React.MouseEvent) => void; content: React.ReactNode };
scrollElement?: Element; scrollElement?: Element;
}; };
@@ -12,7 +12,6 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { TraceSpan } from '../types/trace';
import { TNil } from '../types'; import { TNil } from '../types';
interface TimeCursorUpdate { interface TimeCursorUpdate {
@@ -52,11 +51,3 @@ export interface ViewRangeTime {
export interface ViewRange { export interface ViewRange {
time: ViewRangeTime; 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'; import { Trace } from './trace';
export { TraceSpan, TraceResponse, Trace, TraceProcess, TraceKeyValuePair, TraceLink } 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 TTraceTimeline } from './TTraceTimeline';
export { default as TNil } from './TNil'; export { default as TNil } from './TNil';
@@ -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[]; stackTraces?: string[];
flags: number; flags: number;
errorIconColor?: string; errorIconColor?: string;
dataFrameRowIndex?: number;
}; };
export type TraceSpan = TraceSpanData & { export type TraceSpan = TraceSpanData & {
+8 -2
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"math"
"sort" "sort"
"github.com/aws/aws-sdk-go/aws" "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("") logStreamIdentifierInternal + "|" + parameters.Get("queryString").MustString("")
startQueryInput := &cloudwatchlogs.StartQueryInput{ startQueryInput := &cloudwatchlogs.StartQueryInput{
StartTime: aws.Int64(startTime.Unix()), StartTime: aws.Int64(startTime.Unix()),
EndTime: aws.Int64(endTime.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()), LogGroupNames: aws.StringSlice(parameters.Get("logGroupNames").MustStringArray()),
QueryString: aws.String(modifiedQueryString), QueryString: aws.String(modifiedQueryString),
} }
@@ -1,34 +1,31 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { render, prettyDOM, screen } from '@testing-library/react';
import { render, prettyDOM } from '@testing-library/react';
import { TraceView } from './TraceView'; import { TraceView } from './TraceView';
import { TracePageHeader, TraceTimelineViewer } from '@jaegertracing/jaeger-ui-components';
import { setDataSourceSrv } from '@grafana/runtime'; import { setDataSourceSrv } from '@grafana/runtime';
import { ExploreId } from 'app/types'; import { ExploreId } from 'app/types';
import { TraceData, TraceSpanData } from '@jaegertracing/jaeger-ui-components/src/types/trace'; import { TraceData, TraceSpanData } from '@jaegertracing/jaeger-ui-components/src/types/trace';
import { MutableDataFrame } from '@grafana/data'; 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', () => ({ function renderTraceView(frames = [frameOld]) {
useSelector: jest.fn(() => undefined), const store = configureStore();
connect: jest.fn((v) => v), const { container, baseElement } = render(
})); <Provider store={store}>
<TraceView exploreId={ExploreId.left} dataFrames={frames} splitOpenFn={() => {}} />
function renderTraceView() { </Provider>
const wrapper = shallow(<TraceView exploreId={ExploreId.left} dataFrames={[frameOld]} splitOpenFn={() => {}} />); );
return { return {
timeline: wrapper.find(TraceTimelineViewer), header: container.children[0],
header: wrapper.find(TracePageHeader), timeline: container.children[1],
wrapper, container,
baseElement,
}; };
} }
function renderTraceViewNew() { function renderTraceViewNew() {
const wrapper = shallow(<TraceView exploreId={ExploreId.left} dataFrames={[frameNew]} splitOpenFn={() => {}} />); return renderTraceView([frameNew]);
return {
timeline: wrapper.find(TraceTimelineViewer),
header: wrapper.find(TracePageHeader),
wrapper,
};
} }
describe('TraceView', () => { describe('TraceView', () => {
@@ -42,132 +39,83 @@ describe('TraceView', () => {
it('renders TraceTimelineViewer', () => { it('renders TraceTimelineViewer', () => {
const { timeline, header } = renderTraceView(); const { timeline, header } = renderTraceView();
expect(timeline).toHaveLength(1); expect(timeline).toBeDefined();
expect(header).toHaveLength(1); expect(header).toBeDefined();
}); });
it('renders TraceTimelineViewer with new format', () => { it('renders TraceTimelineViewer with new format', () => {
const { timeline, header } = renderTraceViewNew(); const { timeline, header } = renderTraceViewNew();
expect(timeline).toHaveLength(1); expect(timeline).toBeDefined();
expect(header).toHaveLength(1); expect(header).toBeDefined();
}); });
it('renders renders the same for old and new format', () => { it('renders renders the same for old and new format', () => {
const { baseElement } = render( const { baseElement } = renderTraceViewNew();
<TraceView exploreId={ExploreId.left} dataFrames={[frameNew]} splitOpenFn={() => {}} /> const { baseElement: baseElementOld } = renderTraceView();
);
const { baseElement: baseElementOld } = render(
<TraceView exploreId={ExploreId.left} dataFrames={[frameOld]} splitOpenFn={() => {}} />
);
expect(prettyDOM(baseElement)).toEqual(prettyDOM(baseElementOld)); expect(prettyDOM(baseElement)).toEqual(prettyDOM(baseElementOld));
}); });
it('does not render anything on missing trace', () => { it('does not render anything on missing trace', () => {
// Simulating Explore's access to empty response data // Simulating Explore's access to empty response data
const { container } = render(<TraceView exploreId={ExploreId.left} dataFrames={[]} splitOpenFn={() => {}} />); const { container } = renderTraceView([]);
expect(container.hasChildNodes()).toBeFalsy(); expect(container.hasChildNodes()).toBeFalsy();
}); });
it('toggles detailState', () => { it('toggles detailState', async () => {
let { timeline, wrapper } = renderTraceViewNew(); renderTraceViewNew();
expect(timeline.props().traceTimeline.detailStates.size).toBe(0); 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'); userEvent.click(spanView);
timeline = wrapper.find(TraceTimelineViewer); screen.debug(screen.queryAllByText(/Tags/));
expect(timeline.props().traceTimeline.detailStates.size).toBe(1); expect(screen.queryByText(/Tags/)).toBeFalsy();
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', () => { it('toggles children visibility', () => {
let { timeline, wrapper } = renderTraceViewNew(); renderTraceViewNew();
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0); 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'); userEvent.click(screen.getAllByText('', { selector: 'span[data-test-id="SpanTreeOffset--indentGuide"]' })[0]);
timeline = wrapper.find(TraceTimelineViewer); expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
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);
}); });
it('toggles collapses and expands one level of spans', () => { it('toggles collapses and expands one level of spans', () => {
let { timeline, wrapper } = renderTraceViewNew(); renderTraceViewNew();
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0); expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
const spans = timeline.props().trace.spans; userEvent.click(screen.getByLabelText('Collapse +1'));
expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(2);
timeline.props().collapseOne(spans); userEvent.click(screen.getByLabelText('Expand +1'));
timeline = wrapper.find(TraceTimelineViewer); expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
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', () => { it('toggles collapses and expands all levels', () => {
let { timeline, wrapper } = renderTraceViewNew(); renderTraceViewNew();
expect(timeline.props().traceTimeline.childrenHiddenIDs.size).toBe(0); expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
const spans = timeline.props().trace.spans; userEvent.click(screen.getByLabelText('Collapse All'));
expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(1);
timeline.props().collapseAll(spans); userEvent.click(screen.getByLabelText('Expand All'));
timeline = wrapper.find(TraceTimelineViewer); expect(screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' }).length).toBe(3);
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', () => { it('searches for spans', () => {
let { wrapper, header } = renderTraceViewNew(); renderTraceViewNew();
header.props().onSearchValueChange('HTTP POST - api_prom_push'); userEvent.type(screen.getByPlaceholderText('Find...'), '1ed38015486087ca');
expect(
const timeline = wrapper.find(TraceTimelineViewer); (screen.queryAllByText('', { selector: 'div[data-test-id="span-view"]' })[0].parentNode! as HTMLElement).className
expect(timeline.props().findMatchesIDs?.has('1ed38015486087ca')).toBeTruthy(); ).toContain('rowMatchingFilter');
}); });
it('change viewRange', () => { it('shows timeline ticks', () => {
let { header, timeline, wrapper } = renderTraceViewNew(); renderTraceViewNew();
const defaultRange = { time: { current: [0, 1] } }; function ticks() {
expect(timeline.props().viewRange).toEqual(defaultRange); return screen.getByText('', { selector: 'div[data-test-id="TimelineHeaderRow"]' }).children[1].children[1]
expect(header.props().viewRange).toEqual(defaultRange); .textContent;
header.props().updateViewRangeTime(0.2, 0.8); }
expect(ticks()).toBe('0μs274.5μs549μs823.5μs1.1ms');
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);
}); });
}); });
@@ -63,7 +63,9 @@ export function TraceView(props: Props) {
*/ */
const [slim, setSlim] = useState(false); 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 { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
const dataSourceName = useSelector((state: StoreState) => state.explore[props.exploreId]?.datasourceInstance?.name); const dataSourceName = useSelector((state: StoreState) => state.explore[props.exploreId]?.datasourceInstance?.name);
const traceToLogsOptions = (getDatasourceSrv().getInstanceSettings(dataSourceName)?.jsonData as TraceToLogsData) const traceToLogsOptions = (getDatasourceSrv().getInstanceSettings(dataSourceName)?.jsonData as TraceToLogsData)
@@ -97,10 +99,10 @@ export function TraceView(props: Props) {
[childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, traceProp?.traceID] [childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, traceProp?.traceID]
); );
const createSpanLink = useMemo(() => createSpanLinkFactory(props.splitOpenFn, traceToLogsOptions), [ const createSpanLink = useMemo(
props.splitOpenFn, () => createSpanLinkFactory({ splitOpenFn: props.splitOpenFn, traceToLogsOptions, dataFrame: frame }),
traceToLogsOptions, [props.splitOpenFn, traceToLogsOptions, frame]
]); );
const scrollElement = document.getElementsByClassName('scrollbar-view')[0]; const scrollElement = document.getElementsByClassName('scrollbar-view')[0];
const onSlimViewClicked = useCallback(() => setSlim(!slim), [slim]); const onSlimViewClicked = useCallback(() => setSlim(!slim), [slim]);
@@ -173,9 +175,7 @@ export function TraceView(props: Props) {
); );
} }
function transformDataFrames(frames: DataFrame[]): Trace | null { function transformDataFrames(frame?: DataFrame): Trace | null {
// At this point we only show single trace.
const frame = frames[0];
if (!frame) { if (!frame) {
return null; return null;
} }
@@ -203,7 +203,7 @@ function transformTraceDataFrame(frame: DataFrame): TraceResponse {
return { return {
traceID: view.get(0).traceID, traceID: view.get(0).traceID,
processes, processes,
spans: view.toArray().map((s) => { spans: view.toArray().map((s, index) => {
return { return {
...s, ...s,
duration: s.duration * 1000, duration: s.duration * 1000,
@@ -212,6 +212,7 @@ function transformTraceDataFrame(frame: DataFrame): TraceResponse {
flags: 0, flags: 0,
references: s.parentSpanID ? [{ refType: 'CHILD_OF', spanID: s.parentSpanID, traceID: s.traceID }] : undefined, references: s.parentSpanID ? [{ refType: 'CHILD_OF', spanID: s.parentSpanID, traceID: s.traceID }] : undefined,
logs: s.logs?.map((l) => ({ ...l, timestamp: l.timestamp * 1000 })) || [], 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 { setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
import { createSpanLinkFactory } from './createSpanLink'; 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', () => { describe('createSpanLinkFactory', () => {
it('returns undefined if there is no data source uid', () => { it('returns undefined if there is no data source uid', () => {
const splitOpenFn = jest.fn(); const splitOpenFn = jest.fn();
const createLink = createSpanLinkFactory(splitOpenFn); const createLink = createSpanLinkFactory({ splitOpenFn: splitOpenFn });
expect(createLink).not.toBeDefined(); expect(createLink).not.toBeDefined();
}); });
@@ -13,52 +17,19 @@ describe('createSpanLinkFactory', () => {
beforeAll(() => { beforeAll(() => {
setDataSourceSrv({ setDataSourceSrv({
getInstanceSettings(uid: string): DataSourceInstanceSettings | undefined { getInstanceSettings(uid: string): DataSourceInstanceSettings | undefined {
return { return { uid: 'loki1', name: 'loki1' } as any;
uid: 'loki1',
name: 'loki1',
} as any;
}, },
} as any); } as any);
setTemplateSrv({ setLinkSrv(new LinkSrv());
replace(target?: string, scopedVars?: ScopedVars, format?: string | Function): string { setTemplateSrv(new TemplateSrv());
return target!;
},
} as any);
}); });
it('with default keys when tags not configured', () => { it('with default keys when tags not configured', () => {
const splitOpenFn = jest.fn(); const createLink = setupSpanLinkFactory();
const createLink = createSpanLinkFactory(splitOpenFn, { datasourceUid: 'lokiUid' });
expect(createLink).toBeDefined(); expect(createLink).toBeDefined();
const linkDef = createLink!({ const linkDef = createLink!(createTraceSpan());
startTime: new Date('2020-10-14T01:00:00Z').valueOf() * 1000, expect(linkDef!.href).toBe(
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(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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', () => { it('with tags that passed in and without tags that are not in the span', () => {
const splitOpenFn = jest.fn(); const createLink = setupSpanLinkFactory({
const createLink = createSpanLinkFactory(splitOpenFn, { datasourceUid: 'lokiUid', tags: ['ip', 'newTag'] }); tags: ['ip', 'newTag'],
});
expect(createLink).toBeDefined(); expect(createLink).toBeDefined();
const linkDef = createLink!({ const linkDef = createLink!(
startTime: new Date('2020-10-14T01:00:00Z').valueOf() * 1000, createTraceSpan({
duration: 1000 * 1000, process: {
tags: [ serviceName: 'service',
{ tags: [
key: 'host', { key: 'hostname', value: 'hostname1' },
value: 'host', { key: 'ip', value: '192.168.0.1' },
],
}, },
], })
process: { );
tags: [ expect(linkDef!.href).toBe(
{
key: 'hostname',
value: 'hostname1',
},
{
key: 'ip',
value: '192.168.0.1',
},
],
} as any,
} as any);
expect(linkDef.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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', () => { it('from tags and process tags as well', () => {
const splitOpenFn = jest.fn(); const createLink = setupSpanLinkFactory({
const createLink = createSpanLinkFactory(splitOpenFn, {
datasourceUid: 'lokiUid',
tags: ['ip', 'host'], tags: ['ip', 'host'],
}); });
expect(createLink).toBeDefined(); expect(createLink).toBeDefined();
const linkDef = createLink!({ const linkDef = createLink!(
startTime: new Date('2020-10-14T01:00:00Z').valueOf() * 1000, createTraceSpan({
duration: 1000 * 1000, process: {
tags: [ serviceName: 'service',
{ tags: [
key: 'host', { key: 'hostname', value: 'hostname1' },
value: 'host', { key: 'ip', value: '192.168.0.1' },
],
}, },
], })
process: { );
tags: [ expect(linkDef!.href).toBe(
{
key: 'hostname',
value: 'hostname1',
},
{
key: 'ip',
value: '192.168.0.1',
},
],
} as any,
} as any);
expect(linkDef.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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', () => { it('with adjusted start and end time', () => {
const splitOpenFn = jest.fn(); const createLink = setupSpanLinkFactory({
const createLink = createSpanLinkFactory(splitOpenFn, {
datasourceUid: 'lokiUid',
spanStartTimeShift: '1m', spanStartTimeShift: '1m',
spanEndTimeShift: '1m', spanEndTimeShift: '1m',
}); });
expect(createLink).toBeDefined(); expect(createLink).toBeDefined();
const linkDef = createLink!({ const linkDef = createLink!(
startTime: new Date('2020-10-14T01:00:00Z').valueOf() * 1000, createTraceSpan({
duration: 1000 * 1000, process: {
tags: [ serviceName: 'service',
{ tags: [
key: 'host', { key: 'hostname', value: 'hostname1' },
value: 'host', { key: 'ip', value: '192.168.0.1' },
],
}, },
], })
process: { );
tags: [ expect(linkDef!.href).toBe(
{
key: 'hostname',
value: 'hostname1',
},
{
key: 'ip',
value: '192.168.0.1',
},
],
} as any,
} as any);
expect(linkDef.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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', () => { it('filters by trace and span ID', () => {
const splitOpenFn = jest.fn(); const createLink = setupSpanLinkFactory({
const createLink = createSpanLinkFactory(splitOpenFn, {
datasourceUid: 'lokiUid',
filterBySpanID: true, filterBySpanID: true,
filterByTraceID: true, filterByTraceID: true,
}); });
expect(createLink).toBeDefined(); expect(createLink).toBeDefined();
const linkDef = createLink!({ const linkDef = createLink!(createTraceSpan());
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);
expect(linkDef.href).toBe( expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent( `/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":""}]}' '{"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 { getTemplateSrv } from '@grafana/runtime';
import { Icon } from '@grafana/ui'; 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 { TraceToLogsOptions } from 'app/core/components/TraceToLogsSettings';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import React from 'react'; import React from 'react';
import { LokiQuery } from '../../../plugins/datasource/loki/types'; 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 * 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 * the trace view won't create any links and to capture the datasource and split function making it easier to memoize
* with useMemo. * 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. // We should return if dataSourceUid is undefined otherwise getInstanceSettings would return testDataSource.
if (!traceToLogsOptions?.datasourceUid) { if (!traceToLogsOptions?.datasourceUid) {
return undefined; return undefined;
@@ -24,7 +74,7 @@ export function createSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions
return undefined; 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 // 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 // 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 // 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, link: dataLink,
internalLink: dataLink.internal!, internalLink: dataLink.internal!,
scopedVars: {}, 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, field: {} as Field,
onClickFn: splitOpenFn, onClickFn: splitOpenFn,
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()), replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
@@ -91,15 +146,14 @@ function getLokiQueryFromSpan(span: TraceSpan, options: TraceToLogsOptions): str
/** /**
* Gets a time range from the span. * Gets a time range from the span.
*/ */
function getTimeRangeFromSpan(span: TraceSpan, traceToLogsOptions?: TraceToLogsOptions): TimeRange { function getTimeRangeFromSpan(
const adjustedStartTime = traceToLogsOptions?.spanStartTimeShift span: TraceSpan,
? Math.floor(span.startTime / 1000 + rangeUtil.intervalToMs(traceToLogsOptions.spanStartTimeShift)) timeShift: { startMs: number; endMs: number } = { startMs: 0, endMs: 0 }
: Math.floor(span.startTime / 1000); ): TimeRange {
const adjustedStartTime = Math.floor(span.startTime / 1000 + timeShift.startMs);
const from = dateTime(adjustedStartTime); const from = dateTime(adjustedStartTime);
const spanEndMs = (span.startTime + span.duration) / 1000; const spanEndMs = (span.startTime + span.duration) / 1000;
let adjustedEndTime = traceToLogsOptions?.spanEndTimeShift let adjustedEndTime = Math.floor(spanEndMs + timeShift.endMs);
? Math.floor(spanEndMs + rangeUtil.intervalToMs(traceToLogsOptions.spanEndTimeShift))
: Math.floor(spanEndMs);
// Because we can only pass milliseconds in the url we need to check if they equal. // 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 // We need end time to be later than start time