diff --git a/.betterer.results b/.betterer.results index 9bbf07ee673..f228dec2e0f 100644 --- a/.betterer.results +++ b/.betterer.results @@ -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": [ diff --git a/packages/grafana-data/src/types/data.ts b/packages/grafana-data/src/types/data.ts index f6941ede4f7..642c3cfee5e 100644 --- a/packages/grafana-data/src/types/data.ts +++ b/packages/grafana-data/src/types/data.ts @@ -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 diff --git a/packages/grafana-data/src/types/trace.ts b/packages/grafana-data/src/types/trace.ts index 363584adab7..db73ff9eebe 100644 --- a/packages/grafana-data/src/types/trace.ts +++ b/packages/grafana-data/src/types/trace.ts @@ -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[]; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/ReferencesButton.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/ReferencesButton.tsx index 34fc50f4750..f48b86a243b 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/ReferencesButton.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/ReferencesButton.tsx @@ -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 { - 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 ( - - - {children} - - - ); - } -} + // TODO: handle multiple items with some dropdown + const ref = references[0]; + return ( + + + {children} + + + ); +}; + +export default ReferencesButton; diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js index 5cbc0735b95..6c6c2ff78c3 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.test.js @@ -54,6 +54,7 @@ describe('', () => { }, spanID, logs: [], + references: [], }, }; @@ -84,29 +85,27 @@ describe('', () => { }); 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() .dive() diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx index 0a5caf1dce4..0244e410252 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanBarRow.tsx @@ -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 { tooltipText="Contains multiple references" focusSpan={focusSpan} > - + )} {span.subsidiarilyReferencedBy && span.subsidiarilyReferencedBy.length > 0 && ( diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js index 774b0d57a0d..b69fda5e9e2 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.test.js @@ -104,7 +104,7 @@ describe('', () => { 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 '); } }); }); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx index 5adf26e802e..8796d0d60b5 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/AccordianReferences.tsx @@ -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; + onItemToggle?: (reference: TraceSpanReference) => void; onToggle?: null | (() => void); focusSpan: (uiFind: string) => void; }; type ReferenceItemProps = { data: TraceSpanReference[]; + interactive?: boolean; + openedItems?: Set; + 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 ( -
-
    - {data.map((reference) => { - return ( -
  • - - - {reference.span ? ( - - {reference.span.process.serviceName} - {reference.span.operationName} - - ) : ( - < span in another trace > - )} - - - {reference.refType} - - - {reference.spanID} - - - - -
  • - ); - })} -
+
+ {data.map((reference, i) => ( +
+
+ + + {reference.span ? ( + + {reference.span.process.serviceName} + {reference.span.operationName} + + ) : ( + + View Linked Span + + )} + + + {reference.traceID} + + + {reference.spanID} + + + + +
+ {!!reference.tags?.length && ( +
+ onItemToggle(reference) : null} + /> +
+ )} +
+ ))}
); } -export default class AccordianReferences extends React.PureComponent { - static defaultProps: Partial = { - 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 ? : ; - headerProps = { - 'aria-checked': isOpen, - onClick: isEmpty ? null : onToggle, - role: 'switch', - }; - } - return ( -
-
- {arrow} - - References - {' '} - ({data.length}) -
- {isOpen && } -
- ); +const AccordianReferences: React.FC = ({ + 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 ? : ; + HeaderComponent = 'a'; + headerProps = { + 'aria-checked': isOpen, + onClick: isEmpty ? null : onToggle, + role: 'switch', + }; } -} + + const styles = useStyles2(getStyles); + return ( +
+ + {arrow} + + References + {' '} + ({data.length}) + + {isOpen && ( + + )} +
+ ); +}; + +export default React.memo(AccordianReferences); diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/DetailState.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/DetailState.tsx index 22459037132..da040f4d2dc 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/DetailState.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/DetailState.tsx @@ -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 }; + references: { isOpen: boolean; openedItems: Set }; isWarningsOpen: boolean; isStackTracesOpen: boolean; isReferencesOpen: boolean; @@ -33,6 +34,7 @@ export default class DetailState { isWarningsOpen, isStackTracesOpen, logs, + references, }: DetailState | Record = 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; } diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx index 6555b51bc84..f423349d98b 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetail/index.tsx @@ -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') && ( referencesToggle(spanID)} + onItemToggle={(reference) => referenceItemToggle(spanID, reference)} focusSpan={focusSpan} /> )} diff --git a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx index d254f34d554..c82ca42087f 100644 --- a/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx +++ b/packages/jaeger-ui-components/src/TraceTimelineViewer/SpanDetailRow.tsx @@ -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 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 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; diff --git a/packages/jaeger-ui-components/src/types/trace.ts b/packages/jaeger-ui-components/src/types/trace.ts index 0b54d267cff..62e0a899827 100644 --- a/packages/jaeger-ui-components/src/types/trace.ts +++ b/packages/jaeger-ui-components/src/types/trace.ts @@ -44,6 +44,7 @@ export type TraceSpanReference = { span?: TraceSpan | null | undefined; spanID: string; traceID: string; + tags?: TraceKeyValuePair[]; }; export type TraceSpanData = { diff --git a/packages/jaeger-ui-components/src/url/ReferenceLink.tsx b/packages/jaeger-ui-components/src/url/ReferenceLink.tsx index b6149269ff7..631cd86e2b1 100644 --- a/packages/jaeger-ui-components/src/url/ReferenceLink.tsx +++ b/packages/jaeger-ui-components/src/url/ReferenceLink.tsx @@ -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 ( { + 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, }; diff --git a/public/app/features/explore/TraceView/useDetailState.test.ts b/public/app/features/explore/TraceView/useDetailState.test.ts index 6433dbd5f0d..0a641f461c8 100644 --- a/public/app/features/explore/TraceView/useDetailState.test.ts +++ b/public/app/features/explore/TraceView/useDetailState.test.ts @@ -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 () => { diff --git a/public/app/features/explore/TraceView/useDetailState.ts b/public/app/features/explore/TraceView/useDetailState.ts index 802ec4a74c3..c58e91f6d0d 100644 --- a/public/app/features/explore/TraceView/useDetailState.ts +++ b/public/app/features/explore/TraceView/useDetailState.ts @@ -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] diff --git a/public/app/plugins/datasource/tempo/resultTransformer.ts b/public/app/plugins/datasource/tempo/resultTransformer.ts index 89aa62437f1..8902cc1025d 100644 --- a/public/app/plugins/datasource/tempo/resultTransformer.ts +++ b/public/app/plugins/datasource/tempo/resultTransformer.ts @@ -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); diff --git a/public/app/plugins/datasource/tempo/testResponse.ts b/public/app/plugins/datasource/tempo/testResponse.ts index d099abd7b20..eb922a36c8c 100644 --- a/public/app/plugins/datasource/tempo/testResponse.ts +++ b/public/app/plugins/datasource/tempo/testResponse.ts @@ -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',