mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
@@ -104,7 +104,7 @@ exports[`no enzyme tests`] = {
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBar.test.js:2127169675": [
|
||||
[15, 17, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js:2454947085": [
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js:814916029": [
|
||||
[15, 26, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianKeyValues.test.js:2200354834": [
|
||||
@@ -113,7 +113,7 @@ exports[`no enzyme tests`] = {
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianLogs.test.js:3242453659": [
|
||||
[15, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js:3043344541": [
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js:1301875390": [
|
||||
[15, 19, 13, "RegExp match", "2409514259"]
|
||||
],
|
||||
"packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianText.test.js:2881451220": [
|
||||
|
@@ -20,7 +20,8 @@ export enum LoadingState {
|
||||
}
|
||||
|
||||
// Should be kept in sync with grafana-plugin-sdk-go/data/frame_meta.go
|
||||
export type PreferredVisualisationType = 'graph' | 'table' | 'logs' | 'trace' | 'nodeGraph';
|
||||
export const preferredVisualizationTypes = ['graph', 'table', 'logs', 'trace', 'nodeGraph'] as const;
|
||||
export type PreferredVisualisationType = typeof preferredVisualizationTypes[number];
|
||||
|
||||
/**
|
||||
* @public
|
||||
|
@@ -15,6 +15,12 @@ export type TraceLog = {
|
||||
fields: TraceKeyValuePair[];
|
||||
};
|
||||
|
||||
export type TraceSpanReference = {
|
||||
traceID: string;
|
||||
spanID: string;
|
||||
tags?: TraceKeyValuePair[];
|
||||
};
|
||||
|
||||
/**
|
||||
* This describes the structure of the dataframe that should be returned from a tracing data source to show trace
|
||||
* in a TraceView component.
|
||||
@@ -31,7 +37,7 @@ export interface TraceSpanRow {
|
||||
// Milliseconds
|
||||
duration: number;
|
||||
logs?: TraceLog[];
|
||||
|
||||
references?: TraceSpanReference[];
|
||||
// Note: To mark spen as having error add tag error: true
|
||||
tags?: TraceKeyValuePair[];
|
||||
warnings?: string[];
|
||||
|
@@ -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;
|
||||
|
@@ -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()
|
||||
|
@@ -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 && (
|
||||
|
@@ -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 ');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -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">< span in another trace ></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);
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
@@ -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}
|
||||
|
@@ -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}
|
||||
|
@@ -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;
|
||||
|
@@ -44,6 +44,7 @@ export type TraceSpanReference = {
|
||||
span?: TraceSpan | null | undefined;
|
||||
spanID: string;
|
||||
traceID: string;
|
||||
tags?: TraceKeyValuePair[];
|
||||
};
|
||||
|
||||
export type TraceSpanData = {
|
||||
|
@@ -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)}
|
||||
|
@@ -22,6 +22,12 @@ type TraceLog struct {
|
||||
Fields []*KeyValue `json:"fields"`
|
||||
}
|
||||
|
||||
type TraceReference struct {
|
||||
SpanID string `json:"spanID"`
|
||||
TraceID string `json:"traceID"`
|
||||
Tags []*KeyValue `json:"tags"`
|
||||
}
|
||||
|
||||
func TraceToFrame(td pdata.Traces) (*data.Frame, error) {
|
||||
// In open telemetry format the spans are grouped first by resource/service they originated in and inside that
|
||||
// resource they are grouped by the instrumentation library which created them.
|
||||
@@ -44,6 +50,7 @@ func TraceToFrame(td pdata.Traces) (*data.Frame, error) {
|
||||
data.NewField("startTime", nil, []float64{}),
|
||||
data.NewField("duration", nil, []float64{}),
|
||||
data.NewField("logs", nil, []string{}),
|
||||
data.NewField("references", nil, []string{}),
|
||||
data.NewField("tags", nil, []string{}),
|
||||
},
|
||||
Meta: &data.FrameMeta{
|
||||
@@ -127,6 +134,13 @@ func spanToSpanRow(span pdata.Span, libraryTags pdata.InstrumentationLibrary, re
|
||||
return nil, fmt.Errorf("failed to marshal span logs: %w", err)
|
||||
}
|
||||
|
||||
references, err := json.Marshal(spanLinksToReferences(span.Links()))
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal span links: %w", err)
|
||||
}
|
||||
|
||||
// Order matters (look at dataframe order)
|
||||
return []interface{}{
|
||||
traceID,
|
||||
spanID,
|
||||
@@ -137,6 +151,7 @@ func spanToSpanRow(span pdata.Span, libraryTags pdata.InstrumentationLibrary, re
|
||||
startTime,
|
||||
float64(span.EndTimestamp()-span.StartTimestamp()) / 1_000_000,
|
||||
toJSONString(logs),
|
||||
toJSONString(references),
|
||||
toJSONString(spanTags),
|
||||
}, nil
|
||||
}
|
||||
@@ -321,3 +336,33 @@ func spanEventsToLogs(events pdata.SpanEventSlice) []*TraceLog {
|
||||
|
||||
return logs
|
||||
}
|
||||
|
||||
func spanLinksToReferences(links pdata.SpanLinkSlice) []*TraceReference {
|
||||
if links.Len() == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
references := make([]*TraceReference, 0, links.Len())
|
||||
for i := 0; i < links.Len(); i++ {
|
||||
link := links.At(i)
|
||||
|
||||
traceId := link.TraceID().HexString()
|
||||
traceId = strings.TrimLeft(traceId, "0")
|
||||
|
||||
spanId := link.SpanID().HexString()
|
||||
|
||||
tags := make([]*KeyValue, 0, link.Attributes().Len())
|
||||
link.Attributes().Range(func(key string, attr pdata.AttributeValue) bool {
|
||||
tags = append(tags, &KeyValue{Key: key, Value: getAttributeVal(attr)})
|
||||
return true
|
||||
})
|
||||
|
||||
references = append(references, &TraceReference{
|
||||
TraceID: traceId,
|
||||
SpanID: spanId,
|
||||
Tags: tags,
|
||||
})
|
||||
}
|
||||
|
||||
return references
|
||||
}
|
||||
|
@@ -106,5 +106,6 @@ var fields = []string{
|
||||
"startTime",
|
||||
"duration",
|
||||
"logs",
|
||||
"references",
|
||||
"tags",
|
||||
}
|
||||
|
@@ -57,6 +57,7 @@ export function TraceView(props: Props) {
|
||||
detailLogsToggle,
|
||||
detailProcessToggle,
|
||||
detailReferencesToggle,
|
||||
detailReferenceItemToggle,
|
||||
detailTagsToggle,
|
||||
detailWarningsToggle,
|
||||
detailStackTracesToggle,
|
||||
@@ -87,6 +88,11 @@ export function TraceView(props: Props) {
|
||||
datasource,
|
||||
});
|
||||
|
||||
const createLinkToExternalSpan = (traceId: string, spanId: string) => {
|
||||
const link = createFocusSpanLink(traceId, spanId);
|
||||
return link.href;
|
||||
};
|
||||
|
||||
const traceTimeline: TTraceTimeline = useMemo(
|
||||
() => ({
|
||||
childrenHiddenIDs,
|
||||
@@ -144,7 +150,7 @@ export function TraceView(props: Props) {
|
||||
updateViewRangeTime={updateViewRangeTime}
|
||||
viewRange={viewRange}
|
||||
focusSpan={noop}
|
||||
createLinkToExternalSpan={noop as any}
|
||||
createLinkToExternalSpan={createLinkToExternalSpan}
|
||||
setSpanNameColumnWidth={setSpanNameColumnWidth}
|
||||
collapseAll={collapseAll}
|
||||
collapseOne={collapseOne}
|
||||
@@ -157,6 +163,7 @@ export function TraceView(props: Props) {
|
||||
detailWarningsToggle={detailWarningsToggle}
|
||||
detailStackTracesToggle={detailStackTracesToggle}
|
||||
detailReferencesToggle={detailReferencesToggle}
|
||||
detailReferenceItemToggle={detailReferenceItemToggle}
|
||||
detailProcessToggle={detailProcessToggle}
|
||||
detailTagsToggle={detailTagsToggle}
|
||||
detailToggle={toggleDetail}
|
||||
@@ -203,13 +210,20 @@ function transformTraceDataFrame(frame: DataFrame): TraceResponse {
|
||||
traceID: view.get(0).traceID,
|
||||
processes,
|
||||
spans: view.toArray().map((s, index) => {
|
||||
const references = [];
|
||||
if (s.parentSpanID) {
|
||||
references.push({ refType: 'CHILD_OF' as const, spanID: s.parentSpanID, traceID: s.traceID });
|
||||
}
|
||||
if (s.references) {
|
||||
references.push(...s.references.map((reference) => ({ refType: 'FOLLOWS_FROM' as const, ...reference })));
|
||||
}
|
||||
return {
|
||||
...s,
|
||||
duration: s.duration * 1000,
|
||||
startTime: s.startTime * 1000,
|
||||
processID: s.spanID,
|
||||
flags: 0,
|
||||
references: s.parentSpanID ? [{ refType: 'CHILD_OF', spanID: s.parentSpanID, traceID: s.traceID }] : undefined,
|
||||
references,
|
||||
logs: s.logs?.map((l) => ({ ...l, timestamp: l.timestamp * 1000 })) || [],
|
||||
dataFrameRowIndex: index,
|
||||
};
|
||||
|
@@ -44,7 +44,7 @@ describe('useDetailState', () => {
|
||||
const { result } = renderHook(() => useDetailState(sampleFrame));
|
||||
act(() => result.current.toggleDetail('span1'));
|
||||
act(() => result.current.detailReferencesToggle('span1'));
|
||||
expect(result.current.detailStates.get('span1')?.isReferencesOpen).toBe(true);
|
||||
expect(result.current.detailStates.get('span1')?.references.isOpen).toBe(true);
|
||||
});
|
||||
|
||||
it('toggles processes', async () => {
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { useCallback, useState, useEffect } from 'react';
|
||||
import { DataFrame } from '@grafana/data';
|
||||
import { DetailState } from '@jaegertracing/jaeger-ui-components';
|
||||
import { TraceLog } from '@jaegertracing/jaeger-ui-components/src/types/trace';
|
||||
import { TraceLog, TraceSpanReference } from '@jaegertracing/jaeger-ui-components/src/types/trace';
|
||||
|
||||
/**
|
||||
* Keeps state of the span detail. This means whether span details are open but also state of each detail subitem
|
||||
@@ -42,6 +42,20 @@ export function useDetailState(frame: DataFrame) {
|
||||
[detailStates]
|
||||
);
|
||||
|
||||
const detailReferenceItemToggle = useCallback(
|
||||
function detailReferenceItemToggle(spanID: string, reference: TraceSpanReference) {
|
||||
const old = detailStates.get(spanID);
|
||||
if (!old) {
|
||||
return;
|
||||
}
|
||||
const detailState = old.toggleReferenceItem(reference);
|
||||
const newDetailStates = new Map(detailStates);
|
||||
newDetailStates.set(spanID, detailState);
|
||||
return setDetailStates(newDetailStates);
|
||||
},
|
||||
[detailStates]
|
||||
);
|
||||
|
||||
return {
|
||||
detailStates,
|
||||
toggleDetail,
|
||||
@@ -58,6 +72,7 @@ export function useDetailState(frame: DataFrame) {
|
||||
(spanID: string) => makeDetailSubsectionToggle('stackTraces', detailStates, setDetailStates)(spanID),
|
||||
[detailStates]
|
||||
),
|
||||
detailReferenceItemToggle,
|
||||
detailReferencesToggle: useCallback(
|
||||
(spanID: string) => makeDetailSubsectionToggle('references', detailStates, setDetailStates)(spanID),
|
||||
[detailStates]
|
||||
|
@@ -8,6 +8,7 @@ import {
|
||||
MutableDataFrame,
|
||||
TraceKeyValuePair,
|
||||
TraceLog,
|
||||
TraceSpanReference,
|
||||
TraceSpanRow,
|
||||
dateTimeFormat,
|
||||
} from '@grafana/data';
|
||||
@@ -230,6 +231,24 @@ function getSpanTags(
|
||||
return spanTags;
|
||||
}
|
||||
|
||||
function getReferences(span: collectorTypes.opentelemetryProto.trace.v1.Span) {
|
||||
const references: TraceSpanReference[] = [];
|
||||
if (span.links) {
|
||||
for (const link of span.links) {
|
||||
const { traceId, spanId } = link;
|
||||
const tags: TraceKeyValuePair[] = [];
|
||||
if (link.attributes) {
|
||||
for (const attribute of link.attributes) {
|
||||
tags.push({ key: attribute.key, value: getAttributeValue(attribute.value) });
|
||||
}
|
||||
}
|
||||
references.push({ traceID: traceId, spanID: spanId, tags });
|
||||
}
|
||||
}
|
||||
|
||||
return references;
|
||||
}
|
||||
|
||||
function getLogs(span: collectorTypes.opentelemetryProto.trace.v1.Span) {
|
||||
const logs: TraceLog[] = [];
|
||||
if (span.events) {
|
||||
@@ -262,6 +281,7 @@ export function transformFromOTLP(
|
||||
{ name: 'startTime', type: FieldType.number },
|
||||
{ name: 'duration', type: FieldType.number },
|
||||
{ name: 'logs', type: FieldType.other },
|
||||
{ name: 'references', type: FieldType.other },
|
||||
{ name: 'tags', type: FieldType.other },
|
||||
],
|
||||
meta: {
|
||||
@@ -287,6 +307,7 @@ export function transformFromOTLP(
|
||||
duration: (span.endTimeUnixNano! - span.startTimeUnixNano!) / 1000000,
|
||||
tags: getSpanTags(span, librarySpan.instrumentationLibrary),
|
||||
logs: getLogs(span),
|
||||
references: getReferences(span),
|
||||
} as TraceSpanRow);
|
||||
}
|
||||
}
|
||||
@@ -513,7 +534,7 @@ export function transformTrace(response: DataQueryResponse, nodeGraph = false):
|
||||
* Change fields which are json string into JS objects. Modifies the frame in place.
|
||||
*/
|
||||
function parseJsonFields(frame: DataFrame) {
|
||||
for (const fieldName of ['serviceTags', 'logs', 'tags']) {
|
||||
for (const fieldName of ['serviceTags', 'logs', 'tags', 'references']) {
|
||||
const field = frame.fields.find((f) => f.name === fieldName);
|
||||
if (field) {
|
||||
const fieldIndex = frame.fields.indexOf(field);
|
||||
|
@@ -1923,6 +1923,16 @@ export const otlpDataFrameFromResponse = new MutableDataFrame({
|
||||
displayName: 'logs',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'references',
|
||||
type: 'other',
|
||||
config: {},
|
||||
labels: undefined,
|
||||
values: [[]],
|
||||
state: {
|
||||
displayName: 'references',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'tags',
|
||||
type: 'other',
|
||||
|
Reference in New Issue
Block a user