From 82bcfb492867138bcc4744f88d36eb081c1b6811 Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Tue, 28 Feb 2023 15:41:40 +0000 Subject: [PATCH] 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 --- .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + pkg/services/featuremgmt/registry.go | 6 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../features/explore/TraceView/TraceView.tsx | 35 +++-- .../NewTracePageHeader.test.tsx | 56 ++++++++ .../TracePageHeader/NewTracePageHeader.tsx | 130 ++++++++++++++++++ .../TracePageHeader/TracePageHeader.test.tsx | 85 ++++++++++-- .../TracePageHeader/TracePageHeader.tsx | 48 +++---- .../components/TracePageHeader/index.tsx | 1 + .../explore/TraceView/components/index.ts | 1 + .../components/model/find-trace-name.test.ts | 64 ++++++++- .../components/model/trace-viewer.ts | 28 ++++ 13 files changed, 416 insertions(+), 44 deletions(-) create mode 100644 public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.test.tsx create mode 100644 public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx diff --git a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md index 05c47ceca3a..9c44bf9632f 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -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 | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 8864262c8c8..882192e7665 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -41,6 +41,7 @@ export interface FeatureToggles { export?: boolean; exploreMixedDatasource?: boolean; tracing?: boolean; + newTraceView?: boolean; correlations?: boolean; cloudWatchDynamicLabels?: boolean; datasourceQueryMultiStatus?: boolean; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 8a4a2bc59e6..6fe8d0cd349 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -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", diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index 05a93d8fc11..2e72cce6bef 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -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" diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index ffee1e0d8f3..1718e323cdc 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -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 ? ( <> - + {config.featureToggles.newTraceView ? ( + + ) : ( + + )} { + const defaultProps = { + trace, + timeZone: '', + viewRange: { time: { current: [10, 20] as [number, number] } }, + updateNextViewRangeTime: () => {}, + updateViewRangeTime: () => {}, + ...propOverrides, + }; + + return render(); +}; + +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(); + }); +}); diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx new file mode 100644 index 00000000000..5097e23cec3 --- /dev/null +++ b/public/app/features/explore/TraceView/components/TracePageHeader/NewTracePageHeader.tsx @@ -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 = ( +

+ + + | + {formatDuration(trace.duration)} + +

+ ); + + 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 ( +
+
+ {links && links.length > 0 && } + {title} +
+ +
+ {timestamp(trace, timeZone, styles)} + {method || status || url ? | : undefined} + {method && method.length > 0 && ( + + + + + + )} + {status && status.length > 0 && ( + + + + + + )} + {url && url.length > 0 && ( + + {url[0].value} + + )} +
+ + +
+ ); +} diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx index 407d97c2759..70664101d09 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.test.tsx @@ -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); diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx index 5d4b04b3b37..cbeb41d1670 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/TracePageHeader.tsx @@ -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) => { + // 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 ? ( + + {match[1]} + {match[2]} + + ) : ( + dateStr + ); +}; + export const HEADER_ITEMS = [ { key: 'timestamp', label: 'Trace Start:', - renderer(trace: Trace, timeZone: TimeZone, styles: ReturnType) { - // 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 ? ( - - {match[1]} - {match[2]} - - ) : ( - dateStr - ); - }, + renderer: timestamp, }, { key: 'duration', @@ -185,7 +185,7 @@ export default function TracePageHeader(props: TracePageHeaderEmbedProps) { return (
-
+
{links && links.length > 0 && } {title}
diff --git a/public/app/features/explore/TraceView/components/TracePageHeader/index.tsx b/public/app/features/explore/TraceView/components/TracePageHeader/index.tsx index 8d42d318acc..afa360a0a8e 100644 --- a/public/app/features/explore/TraceView/components/TracePageHeader/index.tsx +++ b/public/app/features/explore/TraceView/components/TracePageHeader/index.tsx @@ -13,3 +13,4 @@ // limitations under the License. export { default } from './TracePageHeader'; +export { NewTracePageHeader } from './NewTracePageHeader'; diff --git a/public/app/features/explore/TraceView/components/index.ts b/public/app/features/explore/TraceView/components/index.ts index bf4885b2b08..e2ea228bac6 100644 --- a/public/app/features/explore/TraceView/components/index.ts +++ b/public/app/features/explore/TraceView/components/index.ts @@ -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'; diff --git a/public/app/features/explore/TraceView/components/model/find-trace-name.test.ts b/public/app/features/explore/TraceView/components/model/find-trace-name.test.ts index d27cc98bdc4..8dedcd277a9 100644 --- a/public/app/features/explore/TraceView/components/model/find-trace-name.test.ts +++ b/public/app/features/explore/TraceView/components/model/find-trace-name.test.ts @@ -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: [], + }); + }); }); diff --git a/public/app/features/explore/TraceView/components/model/trace-viewer.ts b/public/app/features/explore/TraceView/components/model/trace-viewer.ts index 857c4750587..9e68aa9ea45 100644 --- a/public/app/features/explore/TraceView/components/model/trace-viewer.ts +++ b/public/app/features/explore/TraceView/components/model/trace-viewer.ts @@ -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; +});