mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Tracing: Add trace to metrics config behind feature toggle (#46298)
* Add trace to metrics behind feature flag
This commit is contained in:
parent
34fefa1d47
commit
c1b5ea3e54
@ -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": [
|
||||
|
@ -60,4 +60,5 @@ export interface FeatureToggles {
|
||||
cloudWatchDynamicLabels?: boolean;
|
||||
datasourceQueryMultiStatus?: boolean;
|
||||
azureMonitorExperimentalUI?: boolean;
|
||||
traceToMetrics?: boolean;
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
@ -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;
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
`,
|
||||
};
|
||||
};
|
@ -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}
|
||||
|
@ -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;
|
||||
|
@ -248,5 +248,11 @@ var (
|
||||
RequiresDevMode: true,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
{
|
||||
Name: "traceToMetrics",
|
||||
Description: "Enable trace to metrics links",
|
||||
State: FeatureStateAlpha,
|
||||
FrontendOnly: true,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
@ -182,4 +182,8 @@ const (
|
||||
// FlagAzureMonitorExperimentalUI
|
||||
// Use grafana-experimental UI in Azure Monitor
|
||||
FlagAzureMonitorExperimentalUI = "azureMonitorExperimentalUI"
|
||||
|
||||
// FlagTraceToMetrics
|
||||
// Enable trace to metrics links
|
||||
FlagTraceToMetrics = "traceToMetrics"
|
||||
)
|
||||
|
@ -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's log.
|
||||
Trace to logs lets you navigate from a trace span to the selected data source's logs.
|
||||
</div>
|
||||
|
||||
<InlineFieldRow>
|
||||
|
@ -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;
|
||||
`,
|
||||
});
|
@ -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));
|
||||
|
@ -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;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
);
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user