Tracing: Add trace to metrics config behind feature toggle (#46298)

* Add trace to metrics behind feature flag
This commit is contained in:
Connor Lindsey 2022-05-05 14:46:18 -06:00 committed by GitHub
parent 34fefa1d47
commit c1b5ea3e54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 635 additions and 273 deletions

View File

@ -86,13 +86,10 @@ exports[`no enzyme tests`] = {
"packages/jaeger-ui-components/src/TraceTimelineViewer/ListView/index.test.js:1734982398": [
[14, 26, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/ReferencesButton.test.js:3807792910": [
[14, 19, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.test.js:1478502145": [
[14, 17, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js:3826510429": [
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js:1451240090": [
[14, 26, 13, "RegExp match", "2409514259"]
],
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.js:2408389970": [

View File

@ -60,4 +60,5 @@ export interface FeatureToggles {
cloudWatchDynamicLabels?: boolean;
datasourceQueryMultiStatus?: boolean;
azureMonitorExperimentalUI?: boolean;
traceToMetrics?: boolean;
}

View File

@ -1,64 +0,0 @@
// Copyright (c) 2019 The Jaeger Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { shallow } from 'enzyme';
import React from 'react';
import { Tooltip } from '@grafana/ui';
import traceGenerator from '../demo/trace-generators';
import transformTraceData from '../model/transform-trace-data';
import ReferenceLink from '../url/ReferenceLink';
import ReferencesButton, { getStyles } from './ReferencesButton';
describe(ReferencesButton, () => {
const trace = transformTraceData(traceGenerator.trace({ numberOfSpans: 10 }));
const oneReference = trace.spans[1].references;
const moreReferences = oneReference.slice();
const externalSpanID = 'extSpan';
moreReferences.push(
{
refType: 'CHILD_OF',
traceID: trace.traceID,
spanID: trace.spans[2].spanID,
span: trace.spans[2],
},
{
refType: 'CHILD_OF',
traceID: 'otherTrace',
spanID: externalSpanID,
}
);
const baseProps = {
focusSpan: () => {},
};
it('renders single reference', () => {
const props = { ...baseProps, references: oneReference };
const wrapper = shallow(<ReferencesButton {...props} />);
const refLink = wrapper.find(ReferenceLink);
const tooltip = wrapper.find(Tooltip);
const styles = getStyles();
expect(refLink.length).toBe(1);
expect(refLink.prop('reference')).toBe(oneReference[0]);
expect(refLink.first().props().className).toBe(styles.MultiParent);
expect(tooltip.length).toBe(1);
expect(tooltip.prop('content')).toBe(props.tooltipText);
});
});

View File

@ -1,66 +0,0 @@
// Copyright (c) 2019 The Jaeger Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { css } from '@emotion/css';
import React from 'react';
import { Tooltip, useStyles2 } from '@grafana/ui';
import { TraceSpanReference } from '../types/trace';
import ReferenceLink from '../url/ReferenceLink';
export const getStyles = () => {
return {
MultiParent: css`
padding: 0 5px;
& ~ & {
margin-left: 5px;
}
`,
TraceRefLink: css`
display: flex;
justify-content: space-between;
`,
NewWindowIcon: css`
margin: 0.2em 0 0;
`,
tooltip: css`
max-width: none;
`,
};
};
type TReferencesButtonProps = {
references: TraceSpanReference[];
children: React.ReactNode;
tooltipText: string;
focusSpan: (spanID: string) => void;
};
const ReferencesButton = (props: TReferencesButtonProps) => {
const { references, children, tooltipText, focusSpan } = props;
const styles = useStyles2(getStyles);
// TODO: handle multiple items with some dropdown
const ref = references[0];
return (
<Tooltip content={tooltipText}>
<ReferenceLink reference={ref} focusSpan={focusSpan} className={styles.MultiParent}>
{children}
</ReferenceLink>
</Tooltip>
);
};
export default ReferencesButton;

View File

@ -15,8 +15,8 @@
import { mount, shallow } from 'enzyme';
import React from 'react';
import ReferencesButton from './ReferencesButton';
import SpanBarRow from './SpanBarRow';
import { SpanLinksMenu } from './SpanLinks';
import SpanTreeOffset from './SpanTreeOffset';
jest.mock('./SpanTreeOffset', () => {
@ -89,7 +89,7 @@ describe('<SpanBarRow>', () => {
const span = Object.assign(newSpan, {
references: [
{
refType: 'CHILD_OF',
refType: 'FOLLOWS_FROM',
traceID: 'trace1',
spanID: 'span0',
span: {
@ -97,7 +97,7 @@ describe('<SpanBarRow>', () => {
},
},
{
refType: 'CHILD_OF',
refType: 'FOLLOWS_FROM',
traceID: 'otherTrace',
spanID: 'span1',
span: {
@ -107,13 +107,20 @@ describe('<SpanBarRow>', () => {
],
});
const spanRow = shallow(<SpanBarRow {...props} span={span} />)
const spanRow = shallow(
<SpanBarRow
{...props}
span={span}
createSpanLink={() => ({
traceLinks: [{ href: 'href' }, { href: 'href' }],
})}
/>
)
.dive()
.dive()
.dive();
const refButton = spanRow.find(ReferencesButton);
expect(refButton.length).toEqual(1);
expect(refButton.at(0).props().tooltipText).toEqual('Contains multiple references');
const menu = spanRow.find(SpanLinksMenu);
expect(menu.length).toEqual(1);
});
it('render referenced to by single span', () => {
@ -121,7 +128,7 @@ describe('<SpanBarRow>', () => {
{
subsidiarilyReferencedBy: [
{
refType: 'CHILD_OF',
refType: 'FOLLOWS_FROM',
traceID: 'trace1',
spanID: 'span0',
span: {
@ -132,13 +139,21 @@ describe('<SpanBarRow>', () => {
},
props.span
);
const spanRow = shallow(<SpanBarRow {...props} span={span} />)
const spanRow = shallow(
<SpanBarRow
{...props}
span={span}
createSpanLink={() => ({
traceLinks: [{ content: 'This span is referenced by another span', href: 'href' }],
})}
/>
)
.dive()
.dive()
.dive();
const refButton = spanRow.find(ReferencesButton);
expect(refButton.length).toEqual(1);
expect(refButton.at(0).props().tooltipText).toEqual('This span is referenced by another span');
const menu = spanRow.find(`a[href="href"]`);
expect(menu.length).toEqual(1);
expect(menu.at(0).text()).toEqual('This span is referenced by another span');
});
it('render referenced to by multiple span', () => {
@ -146,7 +161,7 @@ describe('<SpanBarRow>', () => {
{
subsidiarilyReferencedBy: [
{
refType: 'CHILD_OF',
refType: 'FOLLOWS_FROM',
traceID: 'trace1',
spanID: 'span0',
span: {
@ -154,7 +169,7 @@ describe('<SpanBarRow>', () => {
},
},
{
refType: 'CHILD_OF',
refType: 'FOLLOWS_FROM',
traceID: 'trace1',
spanID: 'span1',
span: {
@ -165,12 +180,19 @@ describe('<SpanBarRow>', () => {
},
props.span
);
const spanRow = shallow(<SpanBarRow {...props} span={span} />)
const spanRow = shallow(
<SpanBarRow
{...props}
span={span}
createSpanLink={() => ({
traceLinks: [{ href: 'href' }, { href: 'href' }],
})}
/>
)
.dive()
.dive()
.dive();
const refButton = spanRow.find(ReferencesButton);
expect(refButton.length).toEqual(1);
expect(refButton.at(0).props().tooltipText).toEqual('This span is referenced by multiple other spans');
const menu = spanRow.find(SpanLinksMenu);
expect(menu.length).toEqual(1);
});
});

View File

@ -17,17 +17,17 @@ import cx from 'classnames';
import * as React from 'react';
import IoAlert from 'react-icons/lib/io/alert';
import IoArrowRightA from 'react-icons/lib/io/arrow-right-a';
import MdFileUpload from 'react-icons/lib/md/file-upload';
import { GrafanaTheme2 } from '@grafana/data';
import { Icon, stylesFactory, withTheme2 } from '@grafana/ui';
import { stylesFactory, withTheme2 } from '@grafana/ui';
import { autoColor } from '../Theme';
import { SpanLinkFunc, TNil } from '../types';
import { SpanLinks } from '../types/links';
import { TraceSpan } from '../types/trace';
import ReferencesButton from './ReferencesButton';
import SpanBar from './SpanBar';
import { SpanLinksMenu } from './SpanLinks';
import SpanTreeOffset from './SpanTreeOffset';
import Ticks from './Ticks';
import TimelineRow from './TimelineRow';
@ -322,7 +322,6 @@ type SpanBarRowProps = {
getViewedBounds: ViewedBoundsFunctionType;
traceStartTime: number;
span: TraceSpan;
focusSpan: (spanID: string) => void;
hoverIndentGuideIds: Set<string>;
addHoverIndentGuideId: (spanID: string) => void;
removeHoverIndentGuideId: (spanID: string) => void;
@ -370,7 +369,6 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
getViewedBounds,
traceStartTime,
span,
focusSpan,
hoverIndentGuideIds,
addHoverIndentGuideId,
removeHoverIndentGuideId,
@ -402,6 +400,14 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
hintClassName = styles.labelRight;
}
const countLinks = (links?: SpanLinks): number => {
if (!links) {
return 0;
}
return Object.values(links).reduce((count, arr) => count + arr.length, 0);
};
return (
<TimelineRow
className={cx(
@ -476,8 +482,14 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
</a>
{createSpanLink &&
(() => {
const link = createSpanLink(span);
if (link) {
const links = createSpanLink(span);
const count = countLinks(links);
if (links && count === 1) {
const link = links.logLinks?.[0] ?? links.metricLinks?.[0] ?? links.traceLinks?.[0] ?? undefined;
if (!link) {
return null;
}
return (
<a
href={link.href}
@ -499,31 +511,12 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
{link.content}
</a>
);
} else if (links && count > 1) {
return <SpanLinksMenu links={links} />;
} else {
return null;
}
})()}
{span.references && span.references.length > 1 && (
<ReferencesButton
references={span.references}
tooltipText="Contains multiple references"
focusSpan={focusSpan}
>
<Icon name="link" />
</ReferencesButton>
)}
{span.subsidiarilyReferencedBy && span.subsidiarilyReferencedBy.length > 0 && (
<ReferencesButton
references={span.subsidiarilyReferencedBy}
tooltipText={`This span is referenced by ${
span.subsidiarilyReferencedBy.length === 1 ? 'another span' : 'multiple other spans'
}`}
focusSpan={focusSpan}
>
<MdFileUpload />
</ReferencesButton>
)}
</div>
</TimelineRow.Cell>
<TimelineRow.Cell

View File

@ -190,7 +190,7 @@ export default function SpanDetail(props: SpanDetailProps) {
: []),
];
const styles = useStyles2(getStyles);
const link = createSpanLink?.(span);
const links = createSpanLink?.(span);
const focusSpanLink = createFocusSpanLink(traceID, spanID);
return (
@ -201,8 +201,11 @@ export default function SpanDetail(props: SpanDetailProps) {
<LabeledList className={ubTxRightAlign} divider={true} items={overviewItems} />
</div>
</div>
{link ? (
<DataLinkButton link={{ ...link, title: 'Logs for this span' } as any} buttonProps={{ icon: 'gf-logs' }} />
{links?.logLinks?.[0] ? (
<DataLinkButton
link={{ ...links?.logLinks?.[0], title: 'Logs for this span' } as any}
buttonProps={{ icon: 'gf-logs' }}
/>
) : null}
<Divider className={ubMy1} type={'horizontal'} />
<div>

View File

@ -0,0 +1,121 @@
import { css } from '@emotion/css';
import React, { useState } from 'react';
import { useStyles2, MenuGroup, MenuItem, Icon, ContextMenu } from '@grafana/ui';
import { SpanLinks } from '../types/links';
interface SpanLinksProps {
links: SpanLinks;
}
const renderMenuItems = (links: SpanLinks, styles: ReturnType<typeof getStyles>, closeMenu: () => void) => {
return (
<>
{!!links.logLinks?.length ? (
<MenuGroup label="Logs">
{links.logLinks.map((link, i) => (
<MenuItem
key={i}
label="Logs for this span"
onClick={(e) => {
if (link.onClick) {
link.onClick(e);
}
closeMenu();
}}
url={link.href}
className={styles.menuItem}
/>
))}
</MenuGroup>
) : null}
{!!links.metricLinks?.length ? (
<MenuGroup label="Metrics">
{links.metricLinks.map((link, i) => (
<MenuItem
key={i}
label="Metrics for this span"
onClick={(e) => {
if (link.onClick) {
link.onClick(e);
}
closeMenu();
}}
url={link.href}
className={styles.menuItem}
/>
))}
</MenuGroup>
) : null}
{!!links.traceLinks?.length ? (
<MenuGroup label="Traces">
{links.traceLinks.map((link, i) => (
<MenuItem
key={i}
label={link.title ?? 'View linked span'}
onClick={(e) => {
if (link.onClick) {
link.onClick(e);
}
closeMenu();
}}
url={link.href}
className={styles.menuItem}
/>
))}
</MenuGroup>
) : null}
</>
);
};
export const SpanLinksMenu = ({ links }: SpanLinksProps) => {
const styles = useStyles2(getStyles);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const [menuPosition, setMenuPosition] = useState({ x: 0, y: 0 });
const closeMenu = () => setIsMenuOpen(false);
return (
<>
<button
onClick={(e) => {
setIsMenuOpen(true);
setMenuPosition({
x: e.pageX,
y: e.pageY,
});
}}
className={styles.button}
>
<Icon name="link" className={styles.button} />
</button>
{isMenuOpen ? (
<ContextMenu
onClose={() => setIsMenuOpen(false)}
renderMenuItems={() => renderMenuItems(links, styles, closeMenu)}
focusOnOpen={true}
x={menuPosition.x}
y={menuPosition.y}
/>
) : null}
</>
);
};
const getStyles = () => {
return {
button: css`
background: transparent;
border: none;
padding: 0;
margin: 0 3px 0 0;
`,
menuItem: css`
max-width: 60ch;
overflow: hidden;
`,
};
};

View File

@ -387,7 +387,6 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
findMatchesIDs,
spanNameColumnWidth,
trace,
focusSpan,
hoverIndentGuideIds,
addHoverIndentGuideId,
removeHoverIndentGuideId,
@ -455,7 +454,6 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
getViewedBounds={this.getViewedBounds()}
traceStartTime={trace.startTime}
span={span}
focusSpan={focusSpan}
hoverIndentGuideIds={hoverIndentGuideIds}
addHoverIndentGuideId={addHoverIndentGuideId}
removeHoverIndentGuideId={removeHoverIndentGuideId}

View File

@ -6,6 +6,13 @@ export type SpanLinkDef = {
href: string;
onClick?: (event: any) => void;
content: React.ReactNode;
title?: string;
};
export type SpanLinkFunc = (span: TraceSpan) => SpanLinkDef | undefined;
export type SpanLinks = {
logLinks?: SpanLinkDef[];
traceLinks?: SpanLinkDef[];
metricLinks?: SpanLinkDef[];
};
export type SpanLinkFunc = (span: TraceSpan) => SpanLinks | undefined;

View File

@ -248,5 +248,11 @@ var (
RequiresDevMode: true,
FrontendOnly: true,
},
{
Name: "traceToMetrics",
Description: "Enable trace to metrics links",
State: FeatureStateAlpha,
FrontendOnly: true,
},
}
)

View File

@ -182,4 +182,8 @@ const (
// FlagAzureMonitorExperimentalUI
// Use grafana-experimental UI in Azure Monitor
FlagAzureMonitorExperimentalUI = "azureMonitorExperimentalUI"
// FlagTraceToMetrics
// Enable trace to metrics links
FlagTraceToMetrics = "traceToMetrics"
)

View File

@ -40,7 +40,7 @@ export function TraceToLogsSettings({ options, onOptionsChange }: Props) {
<h3 className="page-heading">Trace to logs</h3>
<div className={styles.infoText}>
Trace to logs lets you navigate from a trace span to the selected data source&apos;s log.
Trace to logs lets you navigate from a trace span to the selected data source&apos;s logs.
</div>
<InlineFieldRow>

View File

@ -0,0 +1,80 @@
import { css } from '@emotion/css';
import React from 'react';
import {
DataSourceJsonData,
DataSourcePluginOptionsEditorProps,
GrafanaTheme,
updateDatasourcePluginJsonDataOption,
} from '@grafana/data';
import { DataSourcePicker } from '@grafana/runtime';
import { Button, InlineField, InlineFieldRow, useStyles } from '@grafana/ui';
export interface TraceToMetricsOptions {
datasourceUid?: string;
}
export interface TraceToMetricsData extends DataSourceJsonData {
tracesToMetrics?: TraceToMetricsOptions;
}
interface Props extends DataSourcePluginOptionsEditorProps<TraceToMetricsData> {}
export function TraceToMetricsSettings({ options, onOptionsChange }: Props) {
const styles = useStyles(getStyles);
return (
<div className={css({ width: '100%' })}>
<h3 className="page-heading">Trace to metrics</h3>
<div className={styles.infoText}>
Trace to metrics lets you navigate from a trace span to the selected data source.
</div>
<InlineFieldRow className={styles.row}>
<InlineField tooltip="The data source the trace is going to navigate to" label="Data source" labelWidth={26}>
<DataSourcePicker
inputId="trace-to-metrics-data-source-picker"
pluginId="prometheus"
current={options.jsonData.tracesToMetrics?.datasourceUid}
noDefault={true}
width={40}
onChange={(ds) =>
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
datasourceUid: ds.uid,
})
}
/>
</InlineField>
{options.jsonData.tracesToMetrics?.datasourceUid ? (
<Button
type={'button'}
variant={'secondary'}
size={'sm'}
fill={'text'}
onClick={() => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToMetrics', {
...options.jsonData.tracesToMetrics,
datasourceUid: undefined,
});
}}
>
Clear
</Button>
) : null}
</InlineFieldRow>
</div>
);
}
const getStyles = (theme: GrafanaTheme) => ({
infoText: css`
padding-bottom: ${theme.spacing.md};
color: ${theme.colors.textSemiWeak};
`,
row: css`
label: row;
align-items: baseline;
`,
});

View File

@ -20,6 +20,7 @@ import { getTemplateSrv } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
import { Trace, TracePageHeader, TraceTimelineViewer, TTraceTimeline } from '@jaegertracing/jaeger-ui-components';
import { TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
import { TraceToMetricsData } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { getTimeZone } from 'app/features/profile/state/selectors';
import { StoreState } from 'app/types';
@ -117,12 +118,20 @@ export function TraceView(props: Props) {
[childrenHiddenIDs, detailStates, hoverIndentGuideIds, spanNameColumnWidth, props.traceProp?.traceID]
);
const traceToLogsOptions = (getDatasourceSrv().getInstanceSettings(datasource?.name)?.jsonData as TraceToLogsData)
?.tracesToLogs;
const instanceSettings = getDatasourceSrv().getInstanceSettings(datasource?.name);
const traceToLogsOptions = (instanceSettings?.jsonData as TraceToLogsData)?.tracesToLogs;
const traceToMetricsOptions = (instanceSettings?.jsonData as TraceToMetricsData)?.tracesToMetrics;
const createSpanLink = useMemo(
() =>
createSpanLinkFactory({ splitOpenFn: props.splitOpenFn!, traceToLogsOptions, dataFrame: props.dataFrames[0] }),
[props.splitOpenFn, traceToLogsOptions, props.dataFrames]
createSpanLinkFactory({
splitOpenFn: props.splitOpenFn!,
traceToLogsOptions,
traceToMetricsOptions,
dataFrame: props.dataFrames[0],
createFocusSpanLink,
}),
[props.splitOpenFn, traceToLogsOptions, traceToMetricsOptions, props.dataFrames, createFocusSpanLink]
);
const onSlimViewClicked = useCallback(() => setSlim(!slim), [slim]);
const timeZone = useSelector((state: StoreState) => getTimeZone(state.user));

View File

@ -1,6 +1,7 @@
import { DataSourceInstanceSettings, MutableDataFrame } from '@grafana/data';
import { setDataSourceSrv, setTemplateSrv } from '@grafana/runtime';
import { TraceSpan } from '@jaegertracing/jaeger-ui-components';
import { DatasourceSrv } from 'app/features/plugins/datasource_srv';
import { TraceToLogsOptions } from '../../../core/components/TraceToLogs/TraceToLogsSettings';
import { LinkSrv, setLinkSrv } from '../../panel/panellinks/link_srv';
@ -9,10 +10,13 @@ import { TemplateSrv } from '../../templating/template_srv';
import { createSpanLinkFactory } from './createSpanLink';
describe('createSpanLinkFactory', () => {
it('returns undefined if there is no data source uid', () => {
it('returns no links if there is no data source uid', () => {
const splitOpenFn = jest.fn();
const createLink = createSpanLinkFactory({ splitOpenFn: splitOpenFn });
expect(createLink).not.toBeDefined();
const links = createLink!(createTraceSpan());
expect(links?.logLinks).toBeUndefined();
expect(links?.metricLinks).toBeUndefined();
expect(links?.traceLinks).toHaveLength(0);
});
describe('should return loki link', () => {
@ -30,7 +34,9 @@ describe('createSpanLinkFactory', () => {
it('with default keys when tags not configured', () => {
const createLink = setupSpanLinkFactory();
expect(createLink).toBeDefined();
const linkDef = createLink!(createTraceSpan());
const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
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":""}],"panelsState":{}}'
@ -43,7 +49,7 @@ describe('createSpanLinkFactory', () => {
tags: ['ip', 'newTag'],
});
expect(createLink).toBeDefined();
const linkDef = createLink!(
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@ -54,6 +60,8 @@ describe('createSpanLinkFactory', () => {
},
})
);
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
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":""}],"panelsState":{}}'
@ -66,7 +74,7 @@ describe('createSpanLinkFactory', () => {
tags: ['ip', 'host'],
});
expect(createLink).toBeDefined();
const linkDef = createLink!(
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@ -77,6 +85,8 @@ describe('createSpanLinkFactory', () => {
},
})
);
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
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":""}],"panelsState":{}}'
@ -90,7 +100,7 @@ describe('createSpanLinkFactory', () => {
spanEndTimeShift: '1m',
});
expect(createLink).toBeDefined();
const linkDef = createLink!(
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@ -101,6 +111,8 @@ describe('createSpanLinkFactory', () => {
},
})
);
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
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":""}],"panelsState":{}}'
@ -114,8 +126,10 @@ describe('createSpanLinkFactory', () => {
filterByTraceID: true,
});
expect(createLink).toBeDefined();
const linkDef = createLink!(createTraceSpan());
const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
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":""}],"panelsState":{}}'
@ -139,8 +153,10 @@ describe('createSpanLinkFactory', () => {
}),
});
expect(createLink).toBeDefined();
const linkDef = createLink!(createTraceSpan());
const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe('testSpanId');
});
@ -153,7 +169,7 @@ describe('createSpanLinkFactory', () => {
],
});
expect(createLink).toBeDefined();
const linkDef = createLink!(
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@ -164,6 +180,9 @@ describe('createSpanLinkFactory', () => {
},
})
);
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
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":"{service=\\"serviceName\\", pod=\\"podName\\"}","refId":""}],"panelsState":{}}'
@ -180,7 +199,7 @@ describe('createSpanLinkFactory', () => {
],
});
expect(createLink).toBeDefined();
const linkDef = createLink!(
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@ -191,6 +210,9 @@ describe('createSpanLinkFactory', () => {
},
})
);
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
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":"{service.name=\\"serviceName\\", pod=\\"podName\\"}","refId":""}],"panelsState":{}}'
@ -203,7 +225,7 @@ describe('createSpanLinkFactory', () => {
tags: [],
});
expect(createLink).toBeDefined();
const linkDef = createLink!(
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@ -214,7 +236,7 @@ describe('createSpanLinkFactory', () => {
},
})
);
expect(linkDef).toBeUndefined();
expect(links?.logLinks).toBeUndefined();
});
});
@ -236,7 +258,10 @@ describe('createSpanLinkFactory', () => {
const createLink = setupSpanLinkFactory({
datasourceUid: splunkUID,
});
const linkDef = createLink!(createTraceSpan());
const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
expect(linkDef!.href).toContain(`${encodeURIComponent('datasource":"Splunk 8","queries":[{"query"')}`);
expect(linkDef!.href).not.toContain(`${encodeURIComponent('datasource":"Splunk 8","queries":[{"expr"')}`);
});
@ -245,7 +270,10 @@ describe('createSpanLinkFactory', () => {
const createLink = setupSpanLinkFactory({
datasourceUid: splunkUID,
});
const linkDef = createLink!(createTraceSpan());
const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
expect(linkDef!.href).toContain(
`${encodeURIComponent('{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"}')}`
);
@ -262,8 +290,10 @@ describe('createSpanLinkFactory', () => {
});
expect(createLink).toBeDefined();
const linkDef = createLink!(createTraceSpan());
const links = createLink!(createTraceSpan());
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"cluster=\\"cluster1\\" hostname=\\"hostname1\\" \\"7946b05c2e2e4e5a\\" \\"6605c7b08e715d6c\\"","refId":""}],"panelsState":{}}'
@ -276,7 +306,7 @@ describe('createSpanLinkFactory', () => {
tags: ['ip'],
});
expect(createLink).toBeDefined();
const linkDef = createLink!(
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@ -285,6 +315,8 @@ describe('createSpanLinkFactory', () => {
})
);
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"ip=\\"192.168.0.1\\"","refId":""}],"panelsState":{}}'
@ -297,7 +329,7 @@ describe('createSpanLinkFactory', () => {
tags: ['ip', 'hostname'],
});
expect(createLink).toBeDefined();
const linkDef = createLink!(
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@ -309,6 +341,8 @@ describe('createSpanLinkFactory', () => {
})
);
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"hostname=\\"hostname1\\" ip=\\"192.168.0.1\\"","refId":""}],"panelsState":{}}'
@ -325,7 +359,7 @@ describe('createSpanLinkFactory', () => {
],
});
expect(createLink).toBeDefined();
const linkDef = createLink!(
const links = createLink!(
createTraceSpan({
process: {
serviceName: 'service',
@ -336,6 +370,9 @@ describe('createSpanLinkFactory', () => {
},
})
);
const linkDef = links?.logLinks?.[0];
expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"Splunk 8","queries":[{"query":"service=\\"serviceName\\" pod=\\"podName\\"","refId":""}],"panelsState":{}}'
@ -343,6 +380,94 @@ describe('createSpanLinkFactory', () => {
);
});
});
describe('should return metric link', () => {
beforeAll(() => {
setDataSourceSrv({
getInstanceSettings(uid: string): DataSourceInstanceSettings | undefined {
return { uid: 'prom1', name: 'prom1', type: 'prometheus' } as any;
},
} as any);
setLinkSrv(new LinkSrv());
setTemplateSrv(new TemplateSrv());
});
it('returns query with span', () => {
const splitOpenFn = jest.fn();
const createLink = createSpanLinkFactory({
splitOpenFn,
traceToMetricsOptions: {
datasourceUid: 'prom1',
},
});
expect(createLink).toBeDefined();
const links = createLink!(createTraceSpan());
const linkDef = links?.metricLinks?.[0];
expect(linkDef).toBeDefined();
expect(linkDef!.href).toBe(
`/explore?left=${encodeURIComponent(
'{"range":{"from":"2020-10-14T01:00:00.000Z","to":"2020-10-14T01:00:01.000Z"},"datasource":"prom1","queries":[{"expr":"histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation=\\"operation\\"}[5m])) by (le))","refId":""}],"panelsState":{}}'
)}`
);
});
});
describe('should return span links', () => {
beforeAll(() => {
setDataSourceSrv(new DatasourceSrv());
setLinkSrv(new LinkSrv());
setTemplateSrv(new TemplateSrv());
});
it('ignores parent span link', () => {
const createLink = setupSpanLinkFactory();
expect(createLink).toBeDefined();
const links = createLink!(
createTraceSpan({ references: [{ refType: 'CHILD_OF', spanID: 'parent', traceID: 'traceID' }] })
);
const traceLinks = links?.traceLinks;
expect(traceLinks).toBeDefined();
expect(traceLinks).toHaveLength(0);
});
it('returns links for references and subsidiarilyReferencedBy references', () => {
const createLink = setupSpanLinkFactory();
expect(createLink).toBeDefined();
const links = createLink!(
createTraceSpan({
references: [
{
refType: 'FOLLOWS_FROM',
spanID: 'span1',
traceID: 'traceID',
span: { operationName: 'SpanName' } as any,
},
],
subsidiarilyReferencedBy: [{ refType: 'FOLLOWS_FROM', spanID: 'span3', traceID: 'traceID2' }],
})
);
const traceLinks = links?.traceLinks;
expect(traceLinks).toBeDefined();
expect(traceLinks).toHaveLength(2);
expect(traceLinks![0]).toEqual(
expect.objectContaining({
href: 'traceID-span1',
title: 'SpanName',
})
);
expect(traceLinks![1]).toEqual(
expect.objectContaining({
href: 'traceID2-span3',
title: 'View linked span',
})
);
});
});
});
function setupSpanLinkFactory(options: Partial<TraceToLogsOptions> = {}, datasourceUid = 'lokiUid') {
@ -353,6 +478,11 @@ function setupSpanLinkFactory(options: Partial<TraceToLogsOptions> = {}, datasou
datasourceUid,
...options,
},
createFocusSpanLink: (traceId, spanId) => {
return {
href: `${traceId}-${spanId}`,
} as any;
},
});
}

View File

@ -1,3 +1,4 @@
import { SpanLinks } from '@jaegertracing/jaeger-ui-components/src/types/links';
import React from 'react';
import {
@ -5,6 +6,7 @@ import {
DataLink,
DataQuery,
DataSourceInstanceSettings,
DataSourceJsonData,
dateTime,
Field,
KeyValue,
@ -16,9 +18,11 @@ import {
} from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { Icon } from '@grafana/ui';
import { SpanLinkDef, SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components';
import { SpanLinkFunc, TraceSpan } from '@jaegertracing/jaeger-ui-components';
import { TraceToLogsOptions } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
import { TraceToMetricsOptions } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
import { PromQuery } from 'app/plugins/datasource/prometheus/types';
import { LokiQuery } from '../../../plugins/datasource/loki/types';
import { getFieldLinksForExplore } from '../utils/links';
@ -31,18 +35,22 @@ import { getFieldLinksForExplore } from '../utils/links';
export function createSpanLinkFactory({
splitOpenFn,
traceToLogsOptions,
traceToMetricsOptions,
dataFrame,
createFocusSpanLink,
}: {
splitOpenFn: SplitOpen;
traceToLogsOptions?: TraceToLogsOptions;
traceToMetricsOptions?: TraceToMetricsOptions;
dataFrame?: DataFrame;
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>;
}): 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);
return legacyCreateSpanLinkFactory(splitOpenFn, traceToLogsOptions, traceToMetricsOptions, createFocusSpanLink);
} else {
return function SpanLink(span: TraceSpan): SpanLinkDef | undefined {
return function SpanLink(span: TraceSpan): SpanLinks | 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 {
@ -55,9 +63,13 @@ export function createSpanLinkFactory({
});
return {
href: links[0].href,
onClick: links[0].onClick,
content: <Icon name="gf-logs" title="Explore the logs for this in split view" />,
logLinks: [
{
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
@ -68,65 +80,144 @@ export function createSpanLinkFactory({
}
}
function legacyCreateSpanLinkFactory(splitOpenFn: SplitOpen, traceToLogsOptions?: TraceToLogsOptions) {
// We should return if dataSourceUid is undefined otherwise getInstanceSettings would return testDataSource.
if (!traceToLogsOptions?.datasourceUid) {
return undefined;
function legacyCreateSpanLinkFactory(
splitOpenFn: SplitOpen,
traceToLogsOptions?: TraceToLogsOptions,
traceToMetricsOptions?: TraceToMetricsOptions,
createFocusSpanLink?: (traceId: string, spanId: string) => LinkModel<Field>
) {
let logsDataSourceSettings: DataSourceInstanceSettings<DataSourceJsonData> | undefined;
const isSplunkDS = logsDataSourceSettings?.type === 'grafana-splunk-datasource';
if (traceToLogsOptions?.datasourceUid) {
logsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToLogsOptions.datasourceUid);
}
const dataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToLogsOptions.datasourceUid);
const isSplunkDS = dataSourceSettings?.type === 'grafana-splunk-datasource';
if (!dataSourceSettings) {
return undefined;
let metricsDataSourceSettings: DataSourceInstanceSettings<DataSourceJsonData> | undefined;
if (traceToMetricsOptions?.datasourceUid) {
metricsDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToMetricsOptions.datasourceUid);
}
return function SpanLink(span: TraceSpan): SpanLinkDef | undefined {
return function SpanLink(span: TraceSpan): SpanLinks {
const links: SpanLinks = { traceLinks: [] };
// 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
// it manually here instead of leaving it for the data source to supply the config.
let dataLink: DataLink<LokiQuery | DataQuery> | undefined = {} as DataLink<LokiQuery | DataQuery> | undefined;
let link: LinkModel<Field>;
switch (dataSourceSettings?.type) {
case 'loki':
dataLink = getLinkForLoki(span, traceToLogsOptions, dataSourceSettings);
if (!dataLink) {
return undefined;
}
break;
case 'grafana-splunk-datasource':
dataLink = getLinkForSplunk(span, traceToLogsOptions, dataSourceSettings);
break;
default:
return undefined;
// Get logs link
if (logsDataSourceSettings && traceToLogsOptions) {
switch (logsDataSourceSettings?.type) {
case 'loki':
dataLink = getLinkForLoki(span, traceToLogsOptions, logsDataSourceSettings);
break;
case 'grafana-splunk-datasource':
dataLink = getLinkForSplunk(span, traceToLogsOptions, logsDataSourceSettings);
break;
}
if (dataLink) {
const link = mapInternalLinkToExplore({
link: dataLink,
internalLink: dataLink.internal!,
scopedVars: {},
range: getTimeRangeFromSpan(
span,
{
startMs: traceToLogsOptions.spanStartTimeShift
? rangeUtil.intervalToMs(traceToLogsOptions.spanStartTimeShift)
: 0,
endMs: traceToLogsOptions.spanEndTimeShift
? rangeUtil.intervalToMs(traceToLogsOptions.spanEndTimeShift)
: 0,
},
isSplunkDS
),
field: {} as Field,
onClickFn: splitOpenFn,
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
});
links.logLinks = [
{
href: link.href,
onClick: link.onClick,
content: <Icon name="gf-logs" title="Explore the logs for this in split view" />,
},
];
}
}
link = mapInternalLinkToExplore({
link: dataLink,
internalLink: dataLink?.internal!,
scopedVars: {},
range: getTimeRangeFromSpan(
span,
{
startMs: traceToLogsOptions.spanStartTimeShift
? rangeUtil.intervalToMs(traceToLogsOptions.spanStartTimeShift)
: 0,
endMs: traceToLogsOptions.spanEndTimeShift ? rangeUtil.intervalToMs(traceToLogsOptions.spanEndTimeShift) : 0,
// Get metrics links
if (metricsDataSourceSettings && traceToMetricsOptions) {
const dataLink: DataLink<PromQuery> = {
title: metricsDataSourceSettings.name,
url: '',
internal: {
datasourceUid: metricsDataSourceSettings.uid,
datasourceName: metricsDataSourceSettings.name,
query: {
expr: `histogram_quantile(0.5, sum(rate(tempo_spanmetrics_latency_bucket{operation="${span.operationName}"}[5m])) by (le))`,
refId: '',
},
},
isSplunkDS
),
field: {} as Field,
onClickFn: splitOpenFn,
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
});
};
return {
href: link.href,
onClick: link.onClick,
content: <Icon name="gf-logs" title="Explore the logs for this in split view" />,
};
const link = mapInternalLinkToExplore({
link: dataLink,
internalLink: dataLink.internal!,
scopedVars: {},
range: getTimeRangeFromSpan(span, {
startMs: 0,
endMs: 0,
}),
field: {} as Field,
onClickFn: splitOpenFn,
replaceVariables: getTemplateSrv().replace.bind(getTemplateSrv()),
});
links.metricLinks = [
{
href: link.href,
onClick: link.onClick,
content: <Icon name="chart-line" title="Explore metrics for this span" />,
},
];
}
// Get trace links
if (span.references && createFocusSpanLink) {
for (const reference of span.references) {
// Ignore parent-child links
if (reference.refType === 'CHILD_OF') {
continue;
}
const link = createFocusSpanLink(reference.traceID, reference.spanID);
links.traceLinks!.push({
href: link.href,
title: reference.span ? reference.span.operationName : 'View linked span',
content: <Icon name="link" title="View linked span" />,
onClick: link.onClick,
});
}
}
if (span.subsidiarilyReferencedBy && createFocusSpanLink) {
for (const reference of span.subsidiarilyReferencedBy) {
const link = createFocusSpanLink(reference.traceID, reference.spanID);
links.traceLinks!.push({
href: link.href,
title: reference.span ? reference.span.operationName : 'View linked span',
content: <Icon name="link" title="View linked span" />,
onClick: link.onClick,
});
}
}
return links;
};
}

View File

@ -1,9 +1,11 @@
import React from 'react';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { config } from '@grafana/runtime';
import { DataSourceHttpSettings } from '@grafana/ui';
import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings';
import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
import { TraceToMetricsSettings } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
export type Props = DataSourcePluginOptionsEditorProps;
@ -20,6 +22,13 @@ export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
<div className="gf-form-group">
<TraceToLogsSettings options={options} onOptionsChange={onOptionsChange} />
</div>
{config.featureToggles.traceToMetrics ? (
<div className="gf-form-group">
<TraceToMetricsSettings options={options} onOptionsChange={onOptionsChange} />
</div>
) : null}
<div className="gf-form-group">
<NodeGraphSettings options={options} onOptionsChange={onOptionsChange} />
</div>

View File

@ -5,6 +5,7 @@ import { config } from '@grafana/runtime';
import { DataSourceHttpSettings } from '@grafana/ui';
import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings';
import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
import { TraceToMetricsSettings } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
import { LokiSearchSettings } from './LokiSearchSettings';
import { SearchSettings } from './SearchSettings';
@ -25,19 +26,29 @@ export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
<div className="gf-form-group">
<TraceToLogsSettings options={options} onOptionsChange={onOptionsChange} />
</div>
{config.featureToggles.traceToMetrics ? (
<div className="gf-form-group">
<TraceToMetricsSettings options={options} onOptionsChange={onOptionsChange} />
</div>
) : null}
{config.featureToggles.tempoServiceGraph && (
<div className="gf-form-group">
<ServiceGraphSettings options={options} onOptionsChange={onOptionsChange} />
</div>
)}
{config.featureToggles.tempoSearch && (
<div className="gf-form-group">
<SearchSettings options={options} onOptionsChange={onOptionsChange} />
</div>
)}
<div className="gf-form-group">
<NodeGraphSettings options={options} onOptionsChange={onOptionsChange} />
</div>
<div className="gf-form-group">
<LokiSearchSettings options={options} onOptionsChange={onOptionsChange} />
</div>

View File

@ -39,19 +39,21 @@ export function ServiceGraphSettings({ options, onOptionsChange }: Props) {
}
/>
</InlineField>
<Button
type={'button'}
variant={'secondary'}
size={'sm'}
fill={'text'}
onClick={() => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'serviceMap', {
datasourceUid: undefined,
});
}}
>
Clear
</Button>
{options.jsonData.serviceMap?.datasourceUid ? (
<Button
type={'button'}
variant={'secondary'}
size={'sm'}
fill={'text'}
onClick={() => {
updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'serviceMap', {
datasourceUid: undefined,
});
}}
>
Clear
</Button>
) : null}
</InlineFieldRow>
</div>
);

View File

@ -1,9 +1,11 @@
import React from 'react';
import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
import { config } from '@grafana/runtime';
import { DataSourceHttpSettings } from '@grafana/ui';
import { NodeGraphSettings } from 'app/core/components/NodeGraphSettings';
import { TraceToLogsSettings } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
import { TraceToMetricsSettings } from 'app/core/components/TraceToMetrics/TraceToMetricsSettings';
export type Props = DataSourcePluginOptionsEditorProps;
@ -21,6 +23,12 @@ export const ConfigEditor: React.FC<Props> = ({ options, onOptionsChange }) => {
<TraceToLogsSettings options={options} onOptionsChange={onOptionsChange} />
</div>
{config.featureToggles.traceToMetrics ? (
<div className="gf-form-group">
<TraceToMetricsSettings options={options} onOptionsChange={onOptionsChange} />
</div>
) : null}
<div className="gf-form-group">
<NodeGraphSettings options={options} onOptionsChange={onOptionsChange} />
</div>