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 { 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 & {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user