TraceView: Reworked header (#63105)

* Reworked header

* Remove toggle from merge

* Update test

* Update how span is retrived

* Tests

* Update tests

* Move new trace page header into its own component

* Remove tests already covered in TracePageHeader.test.tsx

* Update findHeaderTags

* Tooltip updates
pull/63882/head
Joey 2 years ago committed by GitHub
parent 16503a719b
commit 82bcfb4928
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md
  2. 1
      packages/grafana-data/src/types/featureToggles.gen.ts
  3. 6
      pkg/services/featuremgmt/registry.go
  4. 4
      pkg/services/featuremgmt/toggles_gen.go
  5. 35
      public/app/features/explore/TraceView/TraceView.tsx
  6. 56
      public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.test.tsx
  7. 130
      public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx
  8. 85
      public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx
  9. 48
      public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx
  10. 1
      public/app/features/explore/TraceView/components/TracePageHeader/index.tsx
  11. 1
      public/app/features/explore/TraceView/components/index.ts
  12. 64
      public/app/features/explore/TraceView/components/model/find-trace-name.test.ts
  13. 28
      public/app/features/explore/TraceView/components/model/trace-viewer.ts

@ -67,6 +67,7 @@ Alpha features might be changed or removed without prior notice.
| `storage` | Configurable storage for dashboards, datasources, and resources |
| `exploreMixedDatasource` | Enable mixed datasource in Explore |
| `tracing` | Adds trace ID to error notifications |
| `newTraceView` | Shows the new trace view design |
| `correlations` | Correlations page |
| `datasourceQueryMultiStatus` | Introduce HTTP 207 Multi Status for api/ds/query |
| `traceToMetrics` | Enable trace to metrics links |

@ -41,6 +41,7 @@ export interface FeatureToggles {
export?: boolean;
exploreMixedDatasource?: boolean;
tracing?: boolean;
newTraceView?: boolean;
correlations?: boolean;
cloudWatchDynamicLabels?: boolean;
datasourceQueryMultiStatus?: boolean;

@ -143,6 +143,12 @@ var (
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "newTraceView",
Description: "Shows the new trace view design",
State: FeatureStateAlpha,
FrontendOnly: true,
},
{
Name: "correlations",
Description: "Correlations page",

@ -107,6 +107,10 @@ const (
// Adds trace ID to error notifications
FlagTracing = "tracing"
// FlagNewTraceView
// Shows the new trace view design
FlagNewTraceView = "newTraceView"
// FlagCorrelations
// Correlations page
FlagCorrelations = "correlations"

@ -13,7 +13,7 @@ import {
PanelData,
SplitOpen,
} from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { config, getTemplateSrv } from '@grafana/runtime';
import { DataQuery } from '@grafana/schema';
import { useStyles2 } from '@grafana/ui';
import { getTraceToLogsOptions, TraceToLogsData } from 'app/core/components/TraceToLogs/TraceToLogsSettings';
@ -26,7 +26,14 @@ import { ExploreId } from 'app/types/explore';
import { changePanelState } from '../state/explorePane';
import { SpanBarOptionsData, Trace, TracePageHeader, TraceTimelineViewer, TTraceTimeline } from './components';
import {
SpanBarOptionsData,
Trace,
TracePageHeader,
NewTracePageHeader,
TraceTimelineViewer,
TTraceTimeline,
} from './components';
import { TopOfViewRefType } from './components/TraceTimelineViewer/VirtualizedTraceView';
import { createSpanLinkFactory } from './createSpanLink';
import { useChildrenState } from './useChildrenState';
@ -134,13 +141,23 @@ export function TraceView(props: Props) {
<>
{props.dataFrames?.length && props.dataFrames[0]?.meta?.preferredVisualisationType === 'trace' && traceProp ? (
<>
<TracePageHeader
trace={traceProp}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
viewRange={viewRange}
timeZone={timeZone}
/>
{config.featureToggles.newTraceView ? (
<NewTracePageHeader
trace={traceProp}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
viewRange={viewRange}
timeZone={timeZone}
/>
) : (
<TracePageHeader
trace={traceProp}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
viewRange={viewRange}
timeZone={timeZone}
/>
)}
<TraceTimelineViewer
registerAccessors={noop}
scrollToFirstVisibleSpan={noop}

@ -0,0 +1,56 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { getAllByText, getByText, render } from '@testing-library/react';
import React from 'react';
import config from 'app/core/config';
import { NewTracePageHeader } from './NewTracePageHeader';
import { TracePageHeaderEmbedProps } from './TracePageHeader';
import { trace } from './TracePageHeader.test';
const setup = (propOverrides?: TracePageHeaderEmbedProps) => {
const defaultProps = {
trace,
timeZone: '',
viewRange: { time: { current: [10, 20] as [number, number] } },
updateNextViewRangeTime: () => {},
updateViewRangeTime: () => {},
...propOverrides,
};
return render(<NewTracePageHeader {...defaultProps} />);
};
describe('NewTracePageHeader test', () => {
it('should render the new trace header', () => {
config.featureToggles.newTraceView = true;
setup();
const header = document.querySelector('header');
const method = getByText(header!, 'POST');
const status = getByText(header!, '200');
const url = getByText(header!, '/v2/gamma/792edh2w897y2huehd2h89');
const duration = getAllByText(header!, '2.36s');
const timestampPart1 = getByText(header!, '2023-02-05 08:50');
const timestampPart2 = getByText(header!, ':56.289');
expect(method).toBeInTheDocument();
expect(status).toBeInTheDocument();
expect(url).toBeInTheDocument();
expect(duration.length).toBe(2);
expect(timestampPart1).toBeInTheDocument();
expect(timestampPart2).toBeInTheDocument();
});
});

@ -0,0 +1,130 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { css } from '@emotion/css';
import cx from 'classnames';
import { get as _get, maxBy as _maxBy, values as _values } from 'lodash';
import * as React from 'react';
import { Badge, BadgeColor, Tooltip, useStyles2 } from '@grafana/ui';
import ExternalLinks from '../common/ExternalLinks';
import TraceName from '../common/TraceName';
import { getTraceLinks } from '../model/link-patterns';
import { getHeaderTags, getTraceName } from '../model/trace-viewer';
import { formatDuration } from '../utils/date';
import SpanGraph from './SpanGraph';
import { TracePageHeaderEmbedProps, timestamp, getStyles } from './TracePageHeader';
const getNewStyles = () => {
return {
subtitle: css`
flex: 1;
line-height: 1em;
margin: -0.5em 0 1.5em 0.5em;
`,
tag: css`
margin: 0 0.5em 0 0;
`,
url: css`
margin: -2.5px 0.3em;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
max-width: 30%;
display: inline-block;
`,
divider: css`
margin: 0 0.75em;
`,
};
};
export function NewTracePageHeader(props: TracePageHeaderEmbedProps) {
const { trace, updateNextViewRangeTime, updateViewRangeTime, viewRange, timeZone } = props;
const styles = { ...useStyles2(getStyles), ...useStyles2(getNewStyles) };
const links = React.useMemo(() => {
if (!trace) {
return [];
}
return getTraceLinks(trace);
}, [trace]);
if (!trace) {
return null;
}
const { method, status, url } = getHeaderTags(trace.spans);
const title = (
<h1 className={cx(styles.TracePageHeaderTitle)}>
<TraceName traceName={getTraceName(trace.spans)} />
<small>
<span className={styles.divider}>|</span>
{formatDuration(trace.duration)}
</small>
</h1>
);
let statusColor: BadgeColor = 'green';
if (status && status.length > 0 && Number.isInteger(status[0].value)) {
if (status[0].value.toString().charAt(0) === '4') {
statusColor = 'orange';
} else if (status[0].value.toString().charAt(0) === '5') {
statusColor = 'red';
}
}
return (
<header className={styles.TracePageHeader}>
<div className={styles.TracePageHeaderTitleRow}>
{links && links.length > 0 && <ExternalLinks links={links} className={styles.TracePageHeaderBack} />}
{title}
</div>
<div className={styles.subtitle}>
{timestamp(trace, timeZone, styles)}
{method || status || url ? <span className={styles.divider}>|</span> : undefined}
{method && method.length > 0 && (
<Tooltip content={'http.method'} interactive={true}>
<span className={styles.tag}>
<Badge text={method[0].value} color="blue" />
</span>
</Tooltip>
)}
{status && status.length > 0 && (
<Tooltip content={'http.status_code'} interactive={true}>
<span className={styles.tag}>
<Badge text={status[0].value} color={statusColor} />
</span>
</Tooltip>
)}
{url && url.length > 0 && (
<Tooltip content={'http.url or http.target or http.path'} interactive={true}>
<span className={styles.url}>{url[0].value}</span>
</Tooltip>
)}
</div>
<SpanGraph
trace={trace}
viewRange={viewRange}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
/>
</header>
);
}

@ -15,22 +15,87 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import traceGenerator from '../demo/trace-generators';
import { getTraceName } from '../model/trace-viewer';
import transformTraceData from '../model/transform-trace-data';
import TracePageHeader, { TracePageHeaderEmbedProps } from './TracePageHeader';
const trace = transformTraceData(traceGenerator.trace({}));
export const trace = {
services: [{ name: 'serviceA', numberOfSpans: 1 }],
spans: [
{
traceID: '164afda25df92413',
spanID: '164afda25df92413',
operationName: 'HTTP Client',
serviceName: 'serviceA',
subsidiarilyReferencedBy: [],
startTime: 1675602037286989,
duration: 5685,
logs: [],
references: [],
tags: [],
processID: '164afda25df92413',
flags: 0,
process: {
serviceName: 'lb',
tags: [],
},
relativeStartTime: 0,
depth: 0,
hasChildren: false,
childSpanCount: 0,
warnings: [],
},
{
traceID: '164afda25df92413',
spanID: '164afda25df92413',
operationName: 'HTTP Client',
serviceName: 'serviceB',
subsidiarilyReferencedBy: [],
startTime: 1675602037286989,
duration: 5685,
logs: [],
references: [],
tags: [
{
key: 'http.url',
type: 'String',
value: `/v2/gamma/792edh2w897y2huehd2h89`,
},
{
key: 'http.method',
type: 'String',
value: `POST`,
},
{
key: 'http.status_code',
type: 'String',
value: `200`,
},
],
processID: '164afda25df92413',
flags: 0,
process: {
serviceName: 'lb',
tags: [],
},
relativeStartTime: 0,
depth: 0,
hasChildren: false,
childSpanCount: 0,
warnings: [],
},
],
traceID: '8bb35a31-eb64-512d-aaed-ddd61887bb2b',
traceName: 'serviceA: GET',
processes: {},
duration: 2355515,
startTime: 1675605056289000,
endTime: 1675605058644515,
};
const setup = (propOverrides?: TracePageHeaderEmbedProps) => {
const defaultProps = {
canCollapse: false,
hideSummary: false,
onSlimViewClicked: () => {},
onTraceGraphViewClicked: () => {},
slimView: false,
trace,
hideMap: false,
timeZone: '',
viewRange: { time: { current: [10, 20] as [number, number] } },
updateNextViewRangeTime: () => {},
@ -90,7 +155,7 @@ describe('TracePageHeader test', () => {
{...({
trace: trace,
viewRange: { time: { current: [10, 20] } },
} as TracePageHeaderEmbedProps)}
} as unknown as TracePageHeaderEmbedProps)}
/>
);
expect(screen.queryAllByRole('listitem')).toHaveLength(5);

@ -32,17 +32,11 @@ import { formatDuration } from '../utils/date';
import SpanGraph from './SpanGraph';
const getStyles = (theme: GrafanaTheme2) => {
export const getStyles = (theme: GrafanaTheme2) => {
return {
theme,
TracePageHeader: css`
label: TracePageHeader;
& > :first-child {
border-bottom: 1px solid ${autoColor(theme, '#e8e8e8')};
}
& > :nth-child(2) {
background-color: ${autoColor(theme, '#eee')};
border-bottom: 1px solid ${autoColor(theme, '#e4e4e4')};
}
& > :last-child {
border-bottom: 1px solid ${autoColor(theme, '#ccc')};
}
@ -75,12 +69,13 @@ const getStyles = (theme: GrafanaTheme2) => {
flex: 1;
font-size: 1.7em;
line-height: 1em;
margin: 0 0 0 0.5em;
margin: 0 0 0 0.3em;
padding-bottom: 0.5em;
`,
TracePageHeaderOverviewItems: css`
label: TracePageHeaderOverviewItems;
border-bottom: 1px solid #e4e4e4;
background-color: ${autoColor(theme, '#eee')};
border-bottom: 1px solid ${autoColor(theme, '#e4e4e4')};
padding: 0.25rem 0.5rem !important;
`,
TracePageHeaderOverviewItemValueDetail: cx(
@ -105,6 +100,9 @@ const getStyles = (theme: GrafanaTheme2) => {
label: TracePageHeaderTraceId;
white-space: nowrap;
`,
titleBorderBottom: css`
border-bottom: 1px solid ${autoColor(theme, '#e8e8e8')};
`,
};
};
@ -116,23 +114,25 @@ export type TracePageHeaderEmbedProps = {
timeZone: TimeZone;
};
export const timestamp = (trace: Trace, timeZone: TimeZone, styles: ReturnType<typeof getStyles>) => {
// Convert date from micro to milli seconds
const dateStr = dateTimeFormat(trace.startTime / 1000, { timeZone, defaultWithMS: true });
const match = dateStr.match(/^(.+)(:\d\d\.\d+)$/);
return match ? (
<span className={styles.TracePageHeaderOverviewItemValue}>
{match[1]}
<span className={styles.TracePageHeaderOverviewItemValueDetail}>{match[2]}</span>
</span>
) : (
dateStr
);
};
export const HEADER_ITEMS = [
{
key: 'timestamp',
label: 'Trace Start:',
renderer(trace: Trace, timeZone: TimeZone, styles: ReturnType<typeof getStyles>) {
// Convert date from micro to milli seconds
const dateStr = dateTimeFormat(trace.startTime / 1000, { timeZone, defaultWithMS: true });
const match = dateStr.match(/^(.+)(:\d\d\.\d+)$/);
return match ? (
<span className={styles.TracePageHeaderOverviewItemValue}>
{match[1]}
<span className={styles.TracePageHeaderOverviewItemValueDetail}>{match[2]}</span>
</span>
) : (
dateStr
);
},
renderer: timestamp,
},
{
key: 'duration',
@ -185,7 +185,7 @@ export default function TracePageHeader(props: TracePageHeaderEmbedProps) {
return (
<header className={styles.TracePageHeader}>
<div className={styles.TracePageHeaderTitleRow}>
<div className={cx(styles.TracePageHeaderTitleRow, styles.titleBorderBottom)}>
{links && links.length > 0 && <ExternalLinks links={links} className={styles.TracePageHeaderBack} />}
{title}
</div>

@ -13,3 +13,4 @@
// limitations under the License.
export { default } from './TracePageHeader';
export { NewTracePageHeader } from './NewTracePageHeader';

@ -1,5 +1,6 @@
export { default as TraceTimelineViewer } from './TraceTimelineViewer';
export { default as TracePageHeader } from './TracePageHeader';
export { NewTracePageHeader } from './TracePageHeader';
export { default as SpanBarSettings } from './settings/SpanBarSettings';
export * from './types';
export * from './TraceTimelineViewer/types';

@ -14,7 +14,7 @@
import { TraceSpan } from '../types';
import { _getTraceNameImpl as getTraceName } from './trace-viewer';
import { getHeaderTags, _getTraceNameImpl as getTraceName } from './trace-viewer';
describe('getTraceName', () => {
const firstSpanId = 'firstSpanId';
@ -221,6 +221,50 @@ describe('getTraceName', () => {
],
},
];
const spansWithHeaderTags = [
{
spanID: firstSpanId,
traceID: currentTraceId,
startTime: t + 200,
process: {},
references: [],
tags: [],
},
{
spanID: secondSpanId,
traceID: currentTraceId,
startTime: t + 100,
process: {},
references: [],
tags: [
{
key: 'http.method',
value: 'POST',
},
{
key: 'http.status_code',
value: '200',
},
],
},
{
spanID: thirdSpanId,
traceID: currentTraceId,
startTime: t,
process: {},
references: [],
tags: [
{
key: 'http.status_code',
value: '400',
},
{
key: 'http.url',
value: '/test:80',
},
],
},
];
const fullTraceName = `${serviceName}: ${operationName}`;
@ -243,4 +287,22 @@ describe('getTraceName', () => {
it('returns an id of root span with no refs', () => {
expect(getTraceName(spansWithOneRootWithNoRefs as unknown as TraceSpan[])).toEqual(fullTraceName);
});
it('returns span with header tags', () => {
expect(getHeaderTags(spansWithHeaderTags as unknown as TraceSpan[])).toEqual({
method: [
{
key: 'http.method',
value: 'POST',
},
],
status: [
{
key: 'http.status_code',
value: '200',
},
],
url: [],
});
});
});

@ -55,3 +55,31 @@ export const getTraceName = memoize(_getTraceNameImpl, (spans: TraceSpan[]) => {
}
return spans[0].traceID;
});
export function findHeaderTags(spans: TraceSpan[]) {
for (let i = 0; i < spans.length; i++) {
const method = spans[i].tags.filter((tag) => {
return tag.key === 'http.method';
});
const status = spans[i].tags.filter((tag) => {
return tag.key === 'http.status_code';
});
const url = spans[i].tags.filter((tag) => {
return tag.key === 'http.url' || tag.key === 'http.target' || tag.key === 'http.path';
});
if (method.length > 0 || status.length > 0 || url.length > 0) {
return { method, status, url };
}
}
return {};
}
export const getHeaderTags = memoize(findHeaderTags, (spans: TraceSpan[]) => {
if (!spans.length) {
return 0;
}
return spans[0].traceID;
});

Loading…
Cancel
Save