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>
pull/45939/head
Shachi Solanki 3 years ago committed by GitHub
parent 893f9e8ee4
commit 190757b3c6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      .betterer.results
  2. 3
      packages/grafana-data/src/types/data.ts
  3. 8
      packages/grafana-data/src/types/trace.ts
  4. 37
      packages/jaeger-ui-components/src/TraceTimelineViewer/ReferencesButton.tsx
  5. 37
      packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js
  6. 6
      packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx
  7. 2
      packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js
  8. 204
      packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx
  9. 20
      packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/DetailState.tsx
  10. 10
      packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx
  11. 5
      packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx
  12. 5
      packages/jaeger-ui-components/src/TraceTimelineViewer/VirtualizedTraceView.tsx
  13. 3
      packages/jaeger-ui-components/src/TraceTimelineViewer/index.tsx
  14. 1
      packages/jaeger-ui-components/src/types/trace.ts
  15. 1
      packages/jaeger-ui-components/src/url/ReferenceLink.tsx
  16. 45
      pkg/tsdb/tempo/trace_transform.go
  17. 1
      pkg/tsdb/tempo/trace_transform_test.go
  18. 18
      public/app/features/explore/TraceView/TraceView.tsx
  19. 2
      public/app/features/explore/TraceView/useDetailState.test.ts
  20. 17
      public/app/features/explore/TraceView/useDetailState.ts
  21. 23
      public/app/plugins/datasource/tempo/resultTransformer.ts
  22. 10
      public/app/plugins/datasource/tempo/testResponse.ts

@ -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">&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);

@ -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',

Loading…
Cancel
Save