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
21 changed files with 635 additions and 273 deletions
@@ -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;