Tempo / Trace Viewer: Support Span Links in Trace Viewer (#45632)

* Support Span Links in Trace Viewer

* Update ReferencesButton styles

* Remove datasource prop

Co-authored-by: Connor Lindsey <cblindsey3@gmail.com>
This commit is contained in:
Shachi Solanki
2022-02-25 12:14:13 -06:00
committed by GitHub
parent 893f9e8ee4
commit 190757b3c6
22 changed files with 335 additions and 127 deletions

View File

@@ -14,16 +14,15 @@
import React from 'react';
import { css } from '@emotion/css';
import { stylesFactory, Tooltip } from '@grafana/ui';
import { Tooltip, useStyles2 } from '@grafana/ui';
import { TraceSpanReference } from '../types/trace';
import ReferenceLink from '../url/ReferenceLink';
export const getStyles = stylesFactory(() => {
export const getStyles = () => {
return {
MultiParent: css`
padding: 0 5px;
color: #000;
& ~ & {
margin-left: 5px;
}
@@ -39,7 +38,7 @@ export const getStyles = stylesFactory(() => {
max-width: none;
`,
};
});
};
type TReferencesButtonProps = {
references: TraceSpanReference[];
@@ -48,19 +47,19 @@ type TReferencesButtonProps = {
focusSpan: (spanID: string) => void;
};
export default class ReferencesButton extends React.PureComponent<TReferencesButtonProps> {
render() {
const { references, children, tooltipText, focusSpan } = this.props;
const styles = getStyles();
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>
);
}
}
// 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

@@ -54,6 +54,7 @@ describe('<SpanBarRow>', () => {
},
spanID,
logs: [],
references: [],
},
};
@@ -84,29 +85,27 @@ describe('<SpanBarRow>', () => {
});
it('render references button', () => {
const span = Object.assign(
{
references: [
{
refType: 'CHILD_OF',
traceID: 'trace1',
const newSpan = Object.assign({}, props.span);
const span = Object.assign(newSpan, {
references: [
{
refType: 'CHILD_OF',
traceID: 'trace1',
spanID: 'span0',
span: {
spanID: 'span0',
span: {
spanID: 'span0',
},
},
{
refType: 'CHILD_OF',
traceID: 'otherTrace',
},
{
refType: 'CHILD_OF',
traceID: 'otherTrace',
spanID: 'span1',
span: {
spanID: 'span1',
span: {
spanID: 'span1',
},
},
],
},
props.span
);
},
],
});
const spanRow = shallow(<SpanBarRow {...props} span={span} />)
.dive()

View File

@@ -13,13 +13,13 @@
// limitations under the License.
import * as React from 'react';
import IoAlert from 'react-icons/lib/io/alert';
import IoArrowRightA from 'react-icons/lib/io/arrow-right-a';
import IoNetwork from 'react-icons/lib/io/network';
import MdFileUpload from 'react-icons/lib/md/file-upload';
import { css, keyframes } from '@emotion/css';
import cx from 'classnames';
import { stylesFactory, withTheme2 } from '@grafana/ui';
import { Icon, stylesFactory, withTheme2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import ReferencesButton from './ReferencesButton';
@@ -510,7 +510,7 @@ export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
tooltipText="Contains multiple references"
focusSpan={focusSpan}
>
<IoNetwork />
<Icon name="link" />
</ReferencesButton>
)}
{span.subsidiarilyReferencedBy && span.subsidiarilyReferencedBy.length > 0 && (

View File

@@ -104,7 +104,7 @@ describe('<References>', () => {
expect(serviceName).toBe(span.process.serviceName);
expect(endpointName).toBe(span.operationName);
} else {
expect(serviceName).toBe('< span in another trace >');
expect(serviceName).toBe('View Linked Span ');
}
});
});

View File

@@ -14,17 +14,51 @@
import * as React from 'react';
import { css } from '@emotion/css';
import cx from 'classnames';
import { useStyles2 } from '@grafana/ui';
import { Icon, useStyles2 } from '@grafana/ui';
import AccordianKeyValues from './AccordianKeyValues';
import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down';
import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right';
import { TraceSpanReference } from '../../types/trace';
import ReferenceLink from '../../url/ReferenceLink';
import { uAlignIcon } from '../../uberUtilityStyles';
import { uAlignIcon, ubMb1 } from '../../uberUtilityStyles';
import { GrafanaTheme2 } from '@grafana/data';
import { autoColor } from '../../Theme';
const getStyles = () => {
const getStyles = (theme: GrafanaTheme2) => {
return {
AccordianReferenceItem: css`
border-bottom: 1px solid ${autoColor(theme, '#d8d8d8')};
`,
AccordianKeyValues: css`
margin-left: 10px;
`,
AccordianReferences: css`
label: AccordianReferences;
border: 1px solid ${autoColor(theme, '#d8d8d8')};
position: relative;
margin-bottom: 0.25rem;
`,
AccordianReferencesHeader: css`
label: AccordianReferencesHeader;
background: ${autoColor(theme, '#e4e4e4')};
color: inherit;
display: block;
padding: 0.25rem 0.5rem;
&:hover {
background: ${autoColor(theme, '#dadada')};
}
`,
AccordianReferencesContent: css`
label: AccordianReferencesContent;
background: ${autoColor(theme, '#f0f0f0')};
border-top: 1px solid ${autoColor(theme, '#d8d8d8')};
padding: 0.5rem 0.5rem 0.25rem 0.5rem;
`,
AccordianReferencesFooter: css`
label: AccordianReferencesFooter;
color: ${autoColor(theme, '#999')};
`,
ReferencesList: css`
background: #fff;
border: 1px solid #ddd;
@@ -53,6 +87,9 @@ const getStyles = () => {
debugInfo: css`
letter-spacing: 0.25px;
margin: 0.5em 0 0;
flex-wrap: wrap;
display: flex;
justify-content: flex-end;
`,
debugLabel: css`
margin: 0 5px 0 5px;
@@ -69,86 +106,117 @@ type AccordianReferencesProps = {
highContrast?: boolean;
interactive?: boolean;
isOpen: boolean;
openedItems?: Set<TraceSpanReference>;
onItemToggle?: (reference: TraceSpanReference) => void;
onToggle?: null | (() => void);
focusSpan: (uiFind: string) => void;
};
type ReferenceItemProps = {
data: TraceSpanReference[];
interactive?: boolean;
openedItems?: Set<TraceSpanReference>;
onItemToggle?: (reference: TraceSpanReference) => void;
focusSpan: (uiFind: string) => void;
};
// export for test
export function References(props: ReferenceItemProps) {
const { data, focusSpan } = props;
const { data, focusSpan, openedItems, onItemToggle, interactive } = props;
const styles = useStyles2(getStyles);
return (
<div className={cx(styles.ReferencesList)}>
<ul className={styles.list}>
{data.map((reference) => {
return (
<li className={styles.item} key={`${reference.spanID}`}>
<ReferenceLink reference={reference} focusSpan={focusSpan}>
<span className={styles.itemContent}>
{reference.span ? (
<span>
<span className="span-svc-name">{reference.span.process.serviceName}</span>
<small className="endpoint-name">{reference.span.operationName}</small>
</span>
) : (
<span className="span-svc-name">&lt; span in another trace &gt;</span>
)}
<small className={styles.debugInfo}>
<span className={styles.debugLabel} data-label="Reference Type:">
{reference.refType}
</span>
<span className={styles.debugLabel} data-label="SpanID:">
{reference.spanID}
</span>
</small>
</span>
</ReferenceLink>
</li>
);
})}
</ul>
<div className={styles.AccordianReferencesContent}>
{data.map((reference, i) => (
<div className={i < data.length - 1 ? styles.AccordianReferenceItem : undefined} key={reference.spanID}>
<div className={styles.item} key={`${reference.spanID}`}>
<ReferenceLink reference={reference} focusSpan={focusSpan}>
<span className={styles.itemContent}>
{reference.span ? (
<span>
<span className="span-svc-name">{reference.span.process.serviceName}</span>
<small className="endpoint-name">{reference.span.operationName}</small>
</span>
) : (
<span className="span-svc-name">
View Linked Span <Icon name="external-link-alt" />
</span>
)}
<small className={styles.debugInfo}>
<span className={styles.debugLabel} data-label="TraceID:">
{reference.traceID}
</span>
<span className={styles.debugLabel} data-label="SpanID:">
{reference.spanID}
</span>
</small>
</span>
</ReferenceLink>
</div>
{!!reference.tags?.length && (
<div className={styles.AccordianKeyValues}>
<AccordianKeyValues
className={i < data.length - 1 ? ubMb1 : null}
data={reference.tags || []}
highContrast
interactive={interactive}
isOpen={openedItems ? openedItems.has(reference) : false}
label={'attributes'}
linksGetter={null}
onToggle={interactive && onItemToggle ? () => onItemToggle(reference) : null}
/>
</div>
)}
</div>
))}
</div>
);
}
export default class AccordianReferences extends React.PureComponent<AccordianReferencesProps> {
static defaultProps: Partial<AccordianReferencesProps> = {
highContrast: false,
interactive: true,
onToggle: null,
};
render() {
const { data, interactive, isOpen, onToggle, focusSpan } = this.props;
const isEmpty = !Array.isArray(data) || !data.length;
const iconCls = uAlignIcon;
let arrow: React.ReactNode | null = null;
let headerProps: {} | null = null;
if (interactive) {
arrow = isOpen ? <IoIosArrowDown className={iconCls} /> : <IoIosArrowRight className={iconCls} />;
headerProps = {
'aria-checked': isOpen,
onClick: isEmpty ? null : onToggle,
role: 'switch',
};
}
return (
<div>
<div {...headerProps}>
{arrow}
<strong>
<span>References</span>
</strong>{' '}
({data.length})
</div>
{isOpen && <References data={data} focusSpan={focusSpan} />}
</div>
);
const AccordianReferences: React.FC<AccordianReferencesProps> = ({
data,
interactive = true,
isOpen,
onToggle,
onItemToggle,
openedItems,
focusSpan,
}) => {
const isEmpty = !Array.isArray(data) || !data.length;
let arrow: React.ReactNode | null = null;
let HeaderComponent: 'span' | 'a' = 'span';
let headerProps: {} | null = null;
if (interactive) {
arrow = isOpen ? <IoIosArrowDown className={uAlignIcon} /> : <IoIosArrowRight className={uAlignIcon} />;
HeaderComponent = 'a';
headerProps = {
'aria-checked': isOpen,
onClick: isEmpty ? null : onToggle,
role: 'switch',
};
}
}
const styles = useStyles2(getStyles);
return (
<div className={styles.AccordianReferences}>
<HeaderComponent className={styles.AccordianReferencesHeader} {...headerProps}>
{arrow}
<strong>
<span>References</span>
</strong>{' '}
({data.length})
</HeaderComponent>
{isOpen && (
<References
data={data}
openedItems={openedItems}
focusSpan={focusSpan}
onItemToggle={onItemToggle}
interactive={interactive}
/>
)}
</div>
);
};
export default React.memo(AccordianReferences);

View File

@@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { TraceLog } from '../../types/trace';
import { TraceLog, TraceSpanReference } from '../../types/trace';
/**
* Which items of a {@link SpanDetail} component are expanded.
@@ -21,6 +21,7 @@ export default class DetailState {
isTagsOpen: boolean;
isProcessOpen: boolean;
logs: { isOpen: boolean; openedItems: Set<TraceLog> };
references: { isOpen: boolean; openedItems: Set<TraceSpanReference> };
isWarningsOpen: boolean;
isStackTracesOpen: boolean;
isReferencesOpen: boolean;
@@ -33,6 +34,7 @@ export default class DetailState {
isWarningsOpen,
isStackTracesOpen,
logs,
references,
}: DetailState | Record<string, undefined> = oldState || {};
this.isTagsOpen = Boolean(isTagsOpen);
this.isProcessOpen = Boolean(isProcessOpen);
@@ -43,6 +45,10 @@ export default class DetailState {
isOpen: Boolean(logs && logs.isOpen),
openedItems: logs && logs.openedItems ? new Set(logs.openedItems) : new Set(),
};
this.references = {
isOpen: Boolean(references && references.isOpen),
openedItems: references && references.openedItems ? new Set(references.openedItems) : new Set(),
};
}
toggleTags() {
@@ -59,7 +65,17 @@ export default class DetailState {
toggleReferences() {
const next = new DetailState(this);
next.isReferencesOpen = !this.isReferencesOpen;
next.references.isOpen = !this.references.isOpen;
return next;
}
toggleReferenceItem(reference: TraceSpanReference) {
const next = new DetailState(this);
if (next.references.openedItems.has(reference)) {
next.references.openedItems.delete(reference);
} else {
next.references.openedItems.add(reference);
}
return next;
}

View File

@@ -26,7 +26,7 @@ import DetailState from './DetailState';
import { formatDuration } from '../utils';
import LabeledList from '../../common/LabeledList';
import { SpanLinkFunc, TNil } from '../../types';
import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan } from '../../types/trace';
import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan, TraceSpanReference } from '../../types/trace';
import AccordianReferences from './AccordianReferences';
import { autoColor } from '../../Theme';
import { Divider } from '../../common/Divider';
@@ -110,6 +110,7 @@ type SpanDetailProps = {
traceStartTime: number;
warningsToggle: (spanID: string) => void;
stackTracesToggle: (spanID: string) => void;
referenceItemToggle: (spanID: string, reference: TraceSpanReference) => void;
referencesToggle: (spanID: string) => void;
focusSpan: (uiFind: string) => void;
createSpanLink?: SpanLinkFunc;
@@ -130,6 +131,7 @@ export default function SpanDetail(props: SpanDetailProps) {
warningsToggle,
stackTracesToggle,
referencesToggle,
referenceItemToggle,
focusSpan,
createSpanLink,
createFocusSpanLink,
@@ -139,7 +141,7 @@ export default function SpanDetail(props: SpanDetailProps) {
isProcessOpen,
logs: logsState,
isWarningsOpen,
isReferencesOpen,
references: referencesState,
isStackTracesOpen,
} = detailState;
const {
@@ -258,8 +260,10 @@ export default function SpanDetail(props: SpanDetailProps) {
{references && references.length > 0 && (references.length > 1 || references[0].refType !== 'CHILD_OF') && (
<AccordianReferences
data={references}
isOpen={isReferencesOpen}
isOpen={referencesState.isOpen}
openedItems={referencesState.openedItems}
onToggle={() => referencesToggle(spanID)}
onItemToggle={(reference) => referenceItemToggle(spanID, reference)}
focusSpan={focusSpan}
/>
)}

View File

@@ -23,7 +23,7 @@ import { autoColor } from '../Theme';
import { stylesFactory, withTheme2 } from '@grafana/ui';
import { GrafanaTheme2, LinkModel } from '@grafana/data';
import { TraceLog, TraceSpan, TraceKeyValuePair, TraceLink } from '../types/trace';
import { TraceLog, TraceSpan, TraceKeyValuePair, TraceLink, TraceSpanReference } from '../types/trace';
import { SpanLinkFunc } from '../types';
const getStyles = stylesFactory((theme: GrafanaTheme2) => {
@@ -77,6 +77,7 @@ type SpanDetailRowProps = {
logItemToggle: (spanID: string, log: TraceLog) => void;
logsToggle: (spanID: string) => void;
processToggle: (spanID: string) => void;
referenceItemToggle: (spanID: string, reference: TraceSpanReference) => void;
referencesToggle: (spanID: string) => void;
warningsToggle: (spanID: string) => void;
stackTracesToggle: (spanID: string) => void;
@@ -111,6 +112,7 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
logItemToggle,
logsToggle,
processToggle,
referenceItemToggle,
referencesToggle,
warningsToggle,
stackTracesToggle,
@@ -156,6 +158,7 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
logItemToggle={logItemToggle}
logsToggle={logsToggle}
processToggle={processToggle}
referenceItemToggle={referenceItemToggle}
referencesToggle={referencesToggle}
warningsToggle={warningsToggle}
stackTracesToggle={stackTracesToggle}

View File

@@ -35,7 +35,7 @@ import {
import { Accessors } from '../ScrollManager';
import { getColorByKey } from '../utils/color-generator';
import { SpanLinkFunc, TNil } from '../types';
import { TraceLog, TraceSpan, Trace, TraceKeyValuePair, TraceLink } from '../types/trace';
import { TraceLog, TraceSpan, Trace, TraceKeyValuePair, TraceLink, TraceSpanReference } from '../types/trace';
import TTraceTimeline from '../types/TTraceTimeline';
import { PEER_SERVICE } from '../constants/tag-keys';
@@ -75,6 +75,7 @@ type TVirtualizedTraceViewOwnProps = {
detailWarningsToggle: (spanID: string) => void;
detailStackTracesToggle: (spanID: string) => void;
detailReferencesToggle: (spanID: string) => void;
detailReferenceItemToggle: (spanID: string, reference: TraceSpanReference) => void;
detailProcessToggle: (spanID: string) => void;
detailTagsToggle: (spanID: string) => void;
detailToggle: (spanID: string) => void;
@@ -440,6 +441,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
detailLogsToggle,
detailProcessToggle,
detailReferencesToggle,
detailReferenceItemToggle,
detailWarningsToggle,
detailStackTracesToggle,
detailStates,
@@ -474,6 +476,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
logItemToggle={detailLogItemToggle}
logsToggle={detailLogsToggle}
processToggle={detailProcessToggle}
referenceItemToggle={detailReferenceItemToggle}
referencesToggle={detailReferencesToggle}
warningsToggle={detailWarningsToggle}
stackTracesToggle={detailStackTracesToggle}

View File

@@ -23,7 +23,7 @@ import { merge as mergeShortcuts } from '../keyboard-shortcuts';
import { Accessors } from '../ScrollManager';
import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from './types';
import { SpanLinkFunc, TNil } from '../types';
import { TraceSpan, Trace, TraceLog, TraceKeyValuePair, TraceLink } from '../types/trace';
import { TraceSpan, Trace, TraceLog, TraceKeyValuePair, TraceLink, TraceSpanReference } from '../types/trace';
import TTraceTimeline from '../types/TTraceTimeline';
import { autoColor } from '../Theme';
import ExternalLinkContext from '../url/externalLinkContext';
@@ -93,6 +93,7 @@ type TProps = TExtractUiFindFromStateReturn & {
detailWarningsToggle: (spanID: string) => void;
detailStackTracesToggle: (spanID: string) => void;
detailReferencesToggle: (spanID: string) => void;
detailReferenceItemToggle: (spanID: string, reference: TraceSpanReference) => void;
detailProcessToggle: (spanID: string) => void;
detailTagsToggle: (spanID: string) => void;
detailToggle: (spanID: string) => void;

View File

@@ -44,6 +44,7 @@ export type TraceSpanReference = {
span?: TraceSpan | null | undefined;
spanID: string;
traceID: string;
tags?: TraceKeyValuePair[];
};
export type TraceSpanData = {

View File

@@ -41,6 +41,7 @@ export default function ReferenceLink(props: ReferenceLinkProps) {
if (!createLinkToExternalSpan) {
throw new Error("ExternalLinkContext does not have a value, you probably forgot to setup it's provider");
}
return (
<a
href={createLinkToExternalSpan(reference.traceID, reference.spanID)}