diff --git a/packages/grafana-data/src/field/getFieldDisplayValuesProxy.test.tsx b/packages/grafana-data/src/field/getFieldDisplayValuesProxy.test.tsx index 685824158c5..0efbb4e1ffd 100644 --- a/packages/grafana-data/src/field/getFieldDisplayValuesProxy.test.tsx +++ b/packages/grafana-data/src/field/getFieldDisplayValuesProxy.test.tsx @@ -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'); + }); }); diff --git a/packages/grafana-data/src/field/getFieldDisplayValuesProxy.ts b/packages/grafana-data/src/field/getFieldDisplayValuesProxy.ts index 255cc7b5714..4faec716e4a 100644 --- a/packages/grafana-data/src/field/getFieldDisplayValuesProxy.ts +++ b/packages/grafana-data/src/field/getFieldDisplayValuesProxy.ts @@ -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 { return new Proxy({} as Record, { - 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; }, diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx index 86fc8c4bf94..41f63928012 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx @@ -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 { {createSpanLink && (() => { const link = createSpanLink(span); - return ( - { - if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link.onClick) { - event.preventDefault(); - link.onClick(event); + if (link) { + return ( + { + if (!(event.ctrlKey || event.metaKey || event.shiftKey) && link.onClick) { + event.preventDefault(); + link.onClick(event); + } } - } - : undefined - } - > - {link.content} - - ); + : undefined + } + > + {link.content} + + ); + } else { + return null; + } })()} {span.references && span.references.length > 1 && ( diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx index e80830bf16c..5fd019ab0e2 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx @@ -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) { diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx index 661e1fc988d..827292384b9 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx @@ -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 { diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx index a04916574bf..bb6cf600360 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -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; }; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx index 144a0247030..5e4fd7518b7 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx @@ -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; }; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/types.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/types.tsx index 65785ba1252..c427b0ecd3d 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/types.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/types.tsx @@ -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; -}; diff --git a/packages/jaeger-ui-components/src/types/index.tsx b/packages/jaeger-ui-components/src/types/index.tsx index ff5809c278e..5db322fdceb 100644 --- a/packages/jaeger-ui-components/src/types/index.tsx +++ b/packages/jaeger-ui-components/src/types/index.tsx @@ -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'; diff --git a/packages/jaeger-ui-components/src/types/links.ts b/packages/jaeger-ui-components/src/types/links.ts new file mode 100644 index 00000000000..58448dcb302 --- /dev/null +++ b/packages/jaeger-ui-components/src/types/links.ts @@ -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; diff --git a/packages/jaeger-ui-components/src/types/trace.ts b/packages/jaeger-ui-components/src/types/trace.ts index a0fb2b3db83..0b54d267cff 100644 --- a/packages/jaeger-ui-components/src/types/trace.ts +++ b/packages/jaeger-ui-components/src/types/trace.ts @@ -61,6 +61,7 @@ export type TraceSpanData = { stackTraces?: string[]; flags: number; errorIconColor?: string; + dataFrameRowIndex?: number; }; export type TraceSpan = TraceSpanData & { diff --git a/pkg/tsdb/cloudwatch/log_actions.go b/pkg/tsdb/cloudwatch/log_actions.go index b2b0ee536ba..8d9c1f93285 100644 --- a/pkg/tsdb/cloudwatch/log_actions.go +++ b/pkg/tsdb/cloudwatch/log_actions.go @@ -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), } diff --git a/public/app/features/explore/TraceView/TraceView.test.tsx b/public/app/features/explore/TraceView/TraceView.test.tsx index 46bcd174946..a48e68e68de 100644 --- a/public/app/features/explore/TraceView/TraceView.test.tsx +++ b/public/app/features/explore/TraceView/TraceView.test.tsx @@ -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( {}} />); +function renderTraceView(frames = [frameOld]) { + const store = configureStore(); + const { container, baseElement } = render( + + {}} /> + + ); 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( {}} />); - 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( - {}} /> - ); - const { baseElement: baseElementOld } = render( - {}} /> - ); + 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( {}} />); + 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'); }); }); diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index 91483dffddd..b7b58492757 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -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, }; }), }; diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index 715d8e615d6..efd1a760bea 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -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 = {}) { + const splitOpenFn = jest.fn(); + return createSpanLinkFactory({ + splitOpenFn, + traceToLogsOptions: { + datasourceUid: 'lokiUid', + ...options, + }, + }); +} + +function createTraceSpan(overrides: Partial = {}): 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; +} diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 96a0099b5f3..1b74869f1f1 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -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: , + }; + } 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