From 4f46fb412ca4e96b0b0ef3f462aead3fc146389e Mon Sep 17 00:00:00 2001 From: Joey <90795735+joey-grafana@users.noreply.github.com> Date: Thu, 23 Nov 2023 13:36:53 +0000 Subject: [PATCH] Tempo: Embed flame graph in span details (#77537) * Embed flame graph * Update test * Update test * Use toggle * Update test * Add tests * Use const * Cleanup * Update profile tag * Move flame graph out of tags, remove request and other cleanup + tests * Update test * Set flame graph by profile id and simplify logic * Cleanup and redrawListView * Create/use feature toggle --- .betterer.results | 3 + .../feature-toggles/index.md | 1 + .../src/types/featureToggles.gen.ts | 1 + .../src/FlameGraph/FlameGraph.tsx | 3 + .../src/FlameGraph/FlameGraphCanvas.tsx | 4 +- .../src/FlameGraphContainer.tsx | 63 ++++--- packages/grafana-flamegraph/src/index.ts | 1 + pkg/services/featuremgmt/registry.go | 7 + pkg/services/featuremgmt/toggles_gen.csv | 1 + pkg/services/featuremgmt/toggles_gen.go | 4 + .../TraceToProfilesSettings.test.tsx | 5 +- .../TraceToProfilesSettings.tsx | 1 - public/app/features/explore/Explore.tsx | 1 - .../explore/TraceView/TraceView.test.tsx | 8 +- .../features/explore/TraceView/TraceView.tsx | 10 +- .../TraceView/TraceViewContainer.test.tsx | 9 +- .../explore/TraceView/TraceViewContainer.tsx | 6 +- .../ListView/index.test.tsx | 1 + .../TraceTimelineViewer/ListView/index.tsx | 4 + .../SpanDetail/SpanFlameGraph.tsx | 169 ++++++++++++++++++ .../SpanDetail/index.test.tsx | 55 +++++- .../TraceTimelineViewer/SpanDetail/index.tsx | 32 +++- .../SpanDetailRow.test.tsx | 2 + .../TraceTimelineViewer/SpanDetailRow.tsx | 15 +- .../VirtualizedTraceView.tsx | 18 +- .../components/TraceTimelineViewer/index.tsx | 7 + .../explore/TraceView/createSpanLink.test.ts | 4 +- .../explore/TraceView/createSpanLink.tsx | 3 +- .../panel/flamegraph/FlameGraphPanel.tsx | 1 + .../app/plugins/panel/traces/TracesPanel.tsx | 1 - 30 files changed, 375 insertions(+), 65 deletions(-) create mode 100644 public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanFlameGraph.tsx diff --git a/.betterer.results b/.betterer.results index 980d4bc2cb8..7fa954579a6 100644 --- a/.betterer.results +++ b/.betterer.results @@ -3878,6 +3878,9 @@ exports[`better eslint`] = { [0, 0, 0, "Styles should be written using objects.", "4"], [0, 0, 0, "Styles should be written using objects.", "5"] ], + "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanFlameGraph.tsx:5381": [ + [0, 0, 0, "Do not use any type assertions.", "0"] + ], "public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/TextList.tsx:5381": [ [0, 0, 0, "Styles should be written using objects.", "0"], [0, 0, 0, "Styles should be written using objects.", "1"], 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 3c0861d1837..76c9e8fb931 100644 --- a/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md +++ b/docs/sources/setup-grafana/configure-grafana/feature-toggles/index.md @@ -127,6 +127,7 @@ Experimental features might be changed or removed without prior notice. | `grafanaAPIServerWithExperimentalAPIs` | Register experimental APIs with the k8s API server | | `featureToggleAdminPage` | Enable admin page for managing feature toggles from the Grafana front-end | | `traceToProfiles` | Enables linking between traces and profiles | +| `tracesEmbeddedFlameGraph` | Enables embedding a flame graph in traces | | `permissionsFilterRemoveSubquery` | Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder | | `influxdbSqlSupport` | Enable InfluxDB SQL query language support with new querying UI | | `angularDeprecationUI` | Display new Angular deprecation-related UI features | diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index 5ca588e103e..05ad75b6bfb 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -108,6 +108,7 @@ export interface FeatureToggles { awsAsyncQueryCaching?: boolean; splitScopes?: boolean; traceToProfiles?: boolean; + tracesEmbeddedFlameGraph?: boolean; permissionsFilterRemoveSubquery?: boolean; prometheusConfigOverhaulAuth?: boolean; configurableSchedulerTick?: boolean; diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx index 52632d478bf..31b0b11e58c 100644 --- a/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraph.tsx @@ -44,6 +44,7 @@ type Props = { onFocusPillClick: () => void; onSandwichPillClick: () => void; colorScheme: ColorScheme | ColorSchemeDiff; + showFlameGraphOnly?: boolean; collapsing?: boolean; }; @@ -62,6 +63,7 @@ const FlameGraph = ({ onFocusPillClick, onSandwichPillClick, colorScheme, + showFlameGraphOnly, collapsing, }: Props) => { const styles = getStyles(); @@ -117,6 +119,7 @@ const FlameGraph = ({ totalProfileTicks, totalProfileTicksRight, totalViewTicks, + showFlameGraphOnly, collapsedMap, setCollapsedMap, collapsing, diff --git a/packages/grafana-flamegraph/src/FlameGraph/FlameGraphCanvas.tsx b/packages/grafana-flamegraph/src/FlameGraph/FlameGraphCanvas.tsx index f242f064f6f..a4d60083c44 100644 --- a/packages/grafana-flamegraph/src/FlameGraph/FlameGraphCanvas.tsx +++ b/packages/grafana-flamegraph/src/FlameGraph/FlameGraphCanvas.tsx @@ -32,6 +32,7 @@ type Props = { totalProfileTicks: number; totalProfileTicksRight?: number; totalViewTicks: number; + showFlameGraphOnly?: boolean; collapsedMap: CollapsedMap; setCollapsedMap: (collapsedMap: CollapsedMap) => void; @@ -56,6 +57,7 @@ const FlameGraphCanvas = ({ root, direction, depth, + showFlameGraphOnly, collapsedMap, setCollapsedMap, collapsing, @@ -182,7 +184,7 @@ const FlameGraphCanvas = ({ totalTicks={totalViewTicks} collapseConfig={tooltipItem ? collapsedMap.get(tooltipItem) : undefined} /> - {clickedItemData && ( + {!showFlameGraphOnly && clickedItemData && ( { const [focusedItemData, setFocusedItemData] = useState(); @@ -143,35 +149,37 @@ const FlameGraphContainer = ({ // isn't already provided.
- { - setSelectedView(view); - onViewSelected?.(view); - }} - containerWidth={containerWidth} - onReset={() => { - resetFocus(); - resetSandwich(); - }} - textAlign={textAlign} - onTextAlignChange={(align) => { - setTextAlign(align); - onTextAlignSelected?.(align); - }} - showResetButton={Boolean(focusedItemData || sandwichItem)} - colorScheme={colorScheme} - onColorSchemeChange={setColorScheme} - stickyHeader={Boolean(stickyHeader)} - extraHeaderElements={extraHeaderElements} - vertical={vertical} - isDiffMode={Boolean(dataContainer.isDiffFlamegraph())} - /> + {!showFlameGraphOnly && ( + { + setSelectedView(view); + onViewSelected?.(view); + }} + containerWidth={containerWidth} + onReset={() => { + resetFocus(); + resetSandwich(); + }} + textAlign={textAlign} + onTextAlignChange={(align) => { + setTextAlign(align); + onTextAlignSelected?.(align); + }} + showResetButton={Boolean(focusedItemData || sandwichItem)} + colorScheme={colorScheme} + onColorSchemeChange={setColorScheme} + stickyHeader={Boolean(stickyHeader)} + extraHeaderElements={extraHeaderElements} + vertical={vertical} + isDiffMode={Boolean(dataContainer.isDiffFlamegraph())} + /> + )}
- {selectedView !== SelectedView.FlameGraph && ( + {!showFlameGraphOnly && selectedView !== SelectedView.FlameGraph && ( )} diff --git a/packages/grafana-flamegraph/src/index.ts b/packages/grafana-flamegraph/src/index.ts index 1e91bd43c65..ab6407c5e49 100644 --- a/packages/grafana-flamegraph/src/index.ts +++ b/packages/grafana-flamegraph/src/index.ts @@ -1,2 +1,3 @@ export { default as FlameGraph, type Props } from './FlameGraphContainer'; export { checkFields, getMessageCheckFieldsResult } from './FlameGraph/dataTransform'; +export { data } from './FlameGraph/testData/dataNestedSet'; diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 81791f22a9c..5397469d9f7 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -671,6 +671,13 @@ var ( FrontendOnly: true, Owner: grafanaObservabilityTracesAndProfilingSquad, }, + { + Name: "tracesEmbeddedFlameGraph", + Description: "Enables embedding a flame graph in traces", + Stage: FeatureStageExperimental, + FrontendOnly: true, + Owner: grafanaObservabilityTracesAndProfilingSquad, + }, { Name: "permissionsFilterRemoveSubquery", Description: "Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder", diff --git a/pkg/services/featuremgmt/toggles_gen.csv b/pkg/services/featuremgmt/toggles_gen.csv index 8f9a770964e..9bdfe1533e2 100644 --- a/pkg/services/featuremgmt/toggles_gen.csv +++ b/pkg/services/featuremgmt/toggles_gen.csv @@ -89,6 +89,7 @@ featureToggleAdminPage,experimental,@grafana/grafana-operator-experience-squad,f awsAsyncQueryCaching,preview,@grafana/aws-datasources,false,false,false,false splitScopes,preview,@grafana/identity-access-team,false,false,true,false traceToProfiles,experimental,@grafana/observability-traces-and-profiling,false,false,false,true +tracesEmbeddedFlameGraph,experimental,@grafana/observability-traces-and-profiling,false,false,false,true permissionsFilterRemoveSubquery,experimental,@grafana/backend-platform,false,false,false,false prometheusConfigOverhaulAuth,GA,@grafana/observability-metrics,false,false,false,false configurableSchedulerTick,experimental,@grafana/alerting-squad,false,false,true,false diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index cfef22fc19a..49daca3b6b5 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -367,6 +367,10 @@ const ( // Enables linking between traces and profiles FlagTraceToProfiles = "traceToProfiles" + // FlagTracesEmbeddedFlameGraph + // Enables embedding a flame graph in traces + FlagTracesEmbeddedFlameGraph = "tracesEmbeddedFlameGraph" + // FlagPermissionsFilterRemoveSubquery // Alternative permission filter implementation that does not use subqueries for fetching the dashboard folder FlagPermissionsFilterRemoveSubquery = "permissionsFilterRemoveSubquery" diff --git a/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.test.tsx b/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.test.tsx index 121c0c91590..0cf648def33 100644 --- a/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.test.tsx +++ b/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.test.tsx @@ -8,11 +8,9 @@ import { TraceToProfilesData, TraceToProfilesSettings } from './TraceToProfilesS const defaultOption: DataSourceSettings = { jsonData: { - tracesToProfilesV2: { + tracesToProfiles: { datasourceUid: 'profiling1_uid', tags: [{ key: 'someTag', value: 'newName' }], - spanStartTimeShift: '1m', - spanEndTimeShift: '1m', customQuery: true, query: '{${__tags}}', }, @@ -48,7 +46,6 @@ describe('TraceToProfilesSettings', () => { it('should render all options', () => { render( {}} />); - expect(screen.getByText('Select data source')).toBeInTheDocument(); expect(screen.getByText('Tags')).toBeInTheDocument(); expect(screen.getByText('Profile type')).toBeInTheDocument(); expect(screen.getByText('Use custom query')).toBeInTheDocument(); diff --git a/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.tsx b/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.tsx index e64bc152fc4..41d5c7d7d57 100644 --- a/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.tsx +++ b/public/app/core/components/TraceToProfiles/TraceToProfilesSettings.tsx @@ -78,7 +78,6 @@ export function TraceToProfilesSettings({ options, onOptionsChange }: Props) { noDefault={true} width={40} onChange={(ds: DataSourceInstanceSettings) => { - console.log(options.jsonData.tracesToProfiles, ds); updateDatasourcePluginJsonDataOption({ onOptionsChange, options }, 'tracesToProfiles', { ...options.jsonData.tracesToProfiles, datasourceUid: ds.uid, diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index aa37d9fe1a6..6a21db4beb4 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -516,7 +516,6 @@ export class Explore extends React.PureComponent { dataFrames={dataFrames} splitOpenFn={this.onSplitOpen('traceView')} scrollElement={this.scrollElement} - queryResponse={queryResponse} /> ) diff --git a/public/app/features/explore/TraceView/TraceView.test.tsx b/public/app/features/explore/TraceView/TraceView.test.tsx index 1323554019b..6a767bafbdf 100644 --- a/public/app/features/explore/TraceView/TraceView.test.tsx +++ b/public/app/features/explore/TraceView/TraceView.test.tsx @@ -3,7 +3,7 @@ import userEvent from '@testing-library/user-event'; import React, { createRef } from 'react'; import { Provider } from 'react-redux'; -import { DataFrame, MutableDataFrame, getDefaultTimeRange, LoadingState } from '@grafana/data'; +import { DataFrame, MutableDataFrame } from '@grafana/data'; import { DataSourceSrv, setDataSourceSrv } from '@grafana/runtime'; import { configureStore } from '../../../store/configureStore'; @@ -14,11 +14,6 @@ import { transformDataFrames } from './utils/transform'; function getTraceView(frames: DataFrame[]) { const store = configureStore(); - const mockPanelData = { - state: LoadingState.Done, - series: [], - timeRange: getDefaultTimeRange(), - }; const topOfViewRef = createRef(); return ( @@ -28,7 +23,6 @@ function getTraceView(frames: DataFrame[]) { dataFrames={frames} splitOpenFn={() => {}} traceProp={transformDataFrames(frames[0])!} - queryResponse={mockPanelData} datasource={undefined} topOfViewRef={topOfViewRef} /> diff --git a/public/app/features/explore/TraceView/TraceView.tsx b/public/app/features/explore/TraceView/TraceView.tsx index db72d6f4beb..f925a11f753 100644 --- a/public/app/features/explore/TraceView/TraceView.tsx +++ b/public/app/features/explore/TraceView/TraceView.tsx @@ -12,7 +12,6 @@ import { GrafanaTheme2, LinkModel, mapInternalLinkToExplore, - PanelData, SplitOpen, } from '@grafana/data'; import { getTemplateSrv } from '@grafana/runtime'; @@ -38,6 +37,7 @@ import { } from './components'; import memoizedTraceCriticalPath from './components/CriticalPath'; import SpanGraph from './components/TracePageHeader/SpanGraph'; +import { TraceFlameGraphs } from './components/TraceTimelineViewer/SpanDetail'; import { createSpanLinkFactory } from './createSpanLink'; import { useChildrenState } from './useChildrenState'; import { useDetailState } from './useDetailState'; @@ -63,7 +63,6 @@ type Props = { scrollElement?: Element; scrollElementClass?: string; traceProp: Trace; - queryResponse: PanelData; datasource: DataSourceApi | undefined; topOfViewRef?: RefObject; createSpanLink?: SpanLinkFunc; @@ -94,6 +93,8 @@ export function TraceView(props: Props) { const [showSpanFilterMatchesOnly, setShowSpanFilterMatchesOnly] = useState(false); const [showCriticalPathSpansOnly, setShowCriticalPathSpansOnly] = useState(false); const [headerHeight, setHeaderHeight] = useState(100); + const [traceFlameGraphs, setTraceFlameGraphs] = useState({}); + const [redrawListView, setRedrawListView] = useState({}); const styles = useStyles2(getStyles); @@ -190,6 +191,7 @@ export function TraceView(props: Props) { ) : ( diff --git a/public/app/features/explore/TraceView/TraceViewContainer.test.tsx b/public/app/features/explore/TraceView/TraceViewContainer.test.tsx index b9d5cf46755..bfe300b3f37 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.test.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.test.tsx @@ -3,8 +3,6 @@ import userEvent from '@testing-library/user-event'; import React from 'react'; import { Provider } from 'react-redux'; -import { getDefaultTimeRange, LoadingState } from '@grafana/data'; - import { configureStore } from '../../../store/configureStore'; import { frameOld } from './TraceView.test'; @@ -19,15 +17,10 @@ jest.mock('@grafana/runtime', () => { function renderTraceViewContainer(frames = [frameOld]) { const store = configureStore(); - const mockPanelData = { - state: LoadingState.Done, - series: [], - timeRange: getDefaultTimeRange(), - }; const { container, baseElement } = render( - {}} queryResponse={mockPanelData} /> + {}} /> ); return { diff --git a/public/app/features/explore/TraceView/TraceViewContainer.tsx b/public/app/features/explore/TraceView/TraceViewContainer.tsx index 42e4d1fa1de..9056d8658ad 100644 --- a/public/app/features/explore/TraceView/TraceViewContainer.tsx +++ b/public/app/features/explore/TraceView/TraceViewContainer.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; -import { DataFrame, SplitOpen, PanelData } from '@grafana/data'; +import { DataFrame, SplitOpen } from '@grafana/data'; import { PanelChrome } from '@grafana/ui/src/components/PanelChrome/PanelChrome'; import { StoreState, useSelector } from 'app/types'; @@ -12,13 +12,12 @@ interface Props { splitOpenFn: SplitOpen; exploreId: string; scrollElement?: Element; - queryResponse: PanelData; } export function TraceViewContainer(props: Props) { // At this point we only show single trace const frame = props.dataFrames[0]; - const { dataFrames, splitOpenFn, exploreId, scrollElement, queryResponse } = props; + const { dataFrames, splitOpenFn, exploreId, scrollElement } = props; const traceProp = useMemo(() => transformDataFrames(frame), [frame]); const datasource = useSelector( (state: StoreState) => state.explore.panes[props.exploreId]?.datasourceInstance ?? undefined @@ -36,7 +35,6 @@ export function TraceViewContainer(props: Props) { splitOpenFn={splitOpenFn} scrollElement={scrollElement} traceProp={traceProp} - queryResponse={queryResponse} datasource={datasource} /> diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.test.tsx index 0ce12997d51..aea197eb6a1 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.test.tsx @@ -47,6 +47,7 @@ const props = { viewBuffer: 10, viewBufferMin: 5, windowScroller: true, + redraw: {}, }; describe('', () => { diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.tsx index 67ae5344c5e..91a57172395 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/ListView/index.tsx @@ -46,6 +46,10 @@ export type TListViewProps = { * Number of items to draw and add to the DOM, initially. */ initialDraw?: number; + /** + * Trigger a redraw of the list view. + */ + redraw: {}; /** * The parent provides fallback height measurements when there is not a * rendered element to measure. diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanFlameGraph.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanFlameGraph.tsx new file mode 100644 index 00000000000..f2bbf3b700e --- /dev/null +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/SpanFlameGraph.tsx @@ -0,0 +1,169 @@ +import { css } from '@emotion/css'; +import React, { useCallback, useEffect } from 'react'; +import { useMeasure } from 'react-use'; +import { lastValueFrom } from 'rxjs'; + +import { + CoreApp, + DataFrame, + DataQueryRequest, + DataSourceInstanceSettings, + DataSourceJsonData, + dateTime, + TimeZone, +} from '@grafana/data'; +import { FlameGraph } from '@grafana/flamegraph'; +import { config } from '@grafana/runtime'; +import { useStyles2 } from '@grafana/ui'; +import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings'; +import { getDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { PyroscopeQueryType } from 'app/plugins/datasource/grafana-pyroscope-datasource/dataquery.gen'; +import { PyroscopeDataSource } from 'app/plugins/datasource/grafana-pyroscope-datasource/datasource'; +import { Query } from 'app/plugins/datasource/grafana-pyroscope-datasource/types'; + +import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; +import { TraceSpan } from '../../types/trace'; + +import { TraceFlameGraphs } from '.'; + +export type SpanFlameGraphProps = { + span: TraceSpan; + traceToProfilesOptions?: TraceToProfilesOptions; + timeZone: TimeZone; + traceFlameGraphs: TraceFlameGraphs; + setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void; + setRedrawListView: (redraw: {}) => void; +}; + +export default function SpanFlameGraph(props: SpanFlameGraphProps) { + const { span, traceToProfilesOptions, timeZone, traceFlameGraphs, setTraceFlameGraphs, setRedrawListView } = props; + const [sizeRef, { height: containerHeight }] = useMeasure(); + const styles = useStyles2(getStyles); + + const profileTag = span.tags.filter((tag) => tag.key === pyroscopeProfileIdTagKey); + const profileTagValue = profileTag.length > 0 ? profileTag[0].value : undefined; + + const getTimeRangeForProfile = useCallback(() => { + const spanStartMs = Math.floor(span.startTime / 1000) - 30000; + const spanEndMs = (span.startTime + span.duration) / 1000 + 30000; + const to = dateTime(spanEndMs); + const from = dateTime(spanStartMs); + + return { + from, + to, + raw: { + from, + to, + }, + }; + }, [span.duration, span.startTime]); + + const getFlameGraphData = async (request: DataQueryRequest, datasourceUid: string) => { + const ds = await getDatasourceSrv().get(datasourceUid); + if (ds instanceof PyroscopeDataSource) { + const result = await lastValueFrom(ds.query(request)); + const frame = result.data.find((x: DataFrame) => { + return x.name === 'response'; + }); + if (frame && frame.length > 1) { + return frame; + } + } + }; + + const queryFlameGraph = useCallback( + async ( + profilesDataSourceSettings: DataSourceInstanceSettings, + traceToProfilesOptions: TraceToProfilesOptions + ) => { + const request = { + requestId: 'span-flamegraph-requestId', + interval: '2s', + intervalMs: 2000, + range: getTimeRangeForProfile(), + scopedVars: {}, + app: CoreApp.Unknown, + timezone: timeZone, + startTime: span.startTime, + targets: [ + { + labelSelector: '{}', + groupBy: [], + profileTypeId: traceToProfilesOptions.profileTypeId ?? '', + queryType: 'profile' as PyroscopeQueryType, + spanSelector: [profileTagValue], + refId: 'span-flamegraph-refId', + datasource: { + type: profilesDataSourceSettings.type, + uid: profilesDataSourceSettings.uid, + }, + }, + ], + }; + const flameGraph = await getFlameGraphData(request, profilesDataSourceSettings.uid); + + if (flameGraph && flameGraph.length > 0) { + setTraceFlameGraphs({ ...traceFlameGraphs, [profileTagValue]: flameGraph }); + } + }, + [getTimeRangeForProfile, profileTagValue, setTraceFlameGraphs, span.startTime, timeZone, traceFlameGraphs] + ); + + useEffect(() => { + if (config.featureToggles.traceToProfiles && !Object.keys(traceFlameGraphs).includes(profileTagValue)) { + let profilesDataSourceSettings: DataSourceInstanceSettings | undefined; + if (traceToProfilesOptions && traceToProfilesOptions?.datasourceUid) { + profilesDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToProfilesOptions.datasourceUid); + } + if (traceToProfilesOptions && profilesDataSourceSettings) { + queryFlameGraph(profilesDataSourceSettings, traceToProfilesOptions); + } + } + }, [ + setTraceFlameGraphs, + span.tags, + traceFlameGraphs, + traceToProfilesOptions, + getTimeRangeForProfile, + span.startTime, + timeZone, + span.spanID, + queryFlameGraph, + profileTagValue, + ]); + + useEffect(() => { + setRedrawListView({}); + }, [containerHeight, setRedrawListView]); + + if (!traceFlameGraphs[profileTagValue]) { + return <>; + } + + return ( +
+
Flame graph
+ config.theme2} + showFlameGraphOnly={true} + disableCollapsing={true} + /> +
+ ); +} + +const getStyles = () => { + return { + flameGraph: css({ + label: 'flameGraphInSpan', + margin: '5px', + }), + flameGraphTitle: css({ + label: 'flameGraphTitleInSpan', + marginBottom: '5px', + fontWeight: 'bold', + }), + }; +}; diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx index 0348e2ed939..f937dcd3810 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.test.tsx @@ -14,10 +14,15 @@ jest.mock('../utils'); -import { render, screen } from '@testing-library/react'; +import { act, render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import React from 'react'; +import { createDataFrame, DataSourceInstanceSettings } from '@grafana/data'; +import { data } from '@grafana/flamegraph'; +import { config, DataSourceSrv, setDataSourceSrv } from '@grafana/runtime'; + +import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; import traceGenerator from '../../demo/trace-generators'; import transformTraceData from '../../model/transform-trace-data'; import { TraceSpanReference } from '../../types/trace'; @@ -33,10 +38,29 @@ describe('', () => { const detailState = new DetailState().toggleLogs().toggleProcess().toggleReferences().toggleTags(); const traceStartTime = 5; const topOfExploreViewRef = jest.fn(); + const request = { + targets: [{ refId: 'A', target: 'query' }], + }; + const traceToProfilesOptions = { + datasourceUid: 'profiling1_uid', + tags: [{ key: 'someTag', value: 'newName' }], + customQuery: true, + query: '{${__tags}}', + type: 'grafana-pyroscope-datasource', + }; + const pyroSettings = { + uid: 'profiling1_uid', + name: 'profiling1', + type: 'grafana-pyroscope-datasource', + meta: { info: { logos: { small: '' } } }, + } as unknown as DataSourceInstanceSettings; + const props = { detailState, span, traceStartTime, + request, + traceToProfilesOptions, topOfExploreViewRef, logItemToggle: jest.fn(), logsToggle: jest.fn(), @@ -45,8 +69,18 @@ describe('', () => { warningsToggle: jest.fn(), referencesToggle: jest.fn(), createFocusSpanLink: jest.fn().mockReturnValue({}), + traceFlameGraphs: { [span.spanID]: createDataFrame(data) }, + setRedrawListView: jest.fn(), }; + span.tags = [ + ...span.tags, + { + key: pyroscopeProfileIdTagKey, + value: span.spanID, + }, + ]; + span.spanID = 'test-spanID'; span.kind = 'test-kind'; span.statusCode = 2; @@ -122,6 +156,15 @@ describe('', () => { props.processToggle.mockReset(); props.logsToggle.mockReset(); props.logItemToggle.mockReset(); + + setDataSourceSrv({ + getList() { + return [pyroSettings]; + }, + getInstanceSettings() { + return pyroSettings; + }, + } as unknown as DataSourceSrv); }); it('renders without exploding', () => { @@ -197,4 +240,14 @@ describe('', () => { render(); expect(screen.getByText('test-spanID')).toBeInTheDocument(); }); + + it('renders the flame graph', async () => { + config.featureToggles.tracesEmbeddedFlameGraph = true; + + render(); + await act(async () => { + expect(screen.getByText(/16.5 Bil/)).toBeInTheDocument(); + expect(screen.getByText(/(Count)/)).toBeInTheDocument(); + }); + }); }); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx index ab9510322d7..691e788f77e 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetail/index.tsx @@ -17,11 +17,13 @@ import { SpanStatusCode } from '@opentelemetry/api'; import cx from 'classnames'; import React from 'react'; -import { dateTimeFormat, GrafanaTheme2, IconName, LinkModel, TimeZone } from '@grafana/data'; +import { DataFrame, dateTimeFormat, GrafanaTheme2, IconName, LinkModel, TimeZone } from '@grafana/data'; import { config, locationService, reportInteraction } from '@grafana/runtime'; import { DataLinkButton, Icon, TextArea, useStyles2 } from '@grafana/ui'; +import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings'; import { RelatedProfilesTitle } from 'app/plugins/datasource/tempo/resultTransformer'; +import { pyroscopeProfileIdTagKey } from '../../../createSpanLink'; import { autoColor } from '../../Theme'; import { Divider } from '../../common/Divider'; import LabeledList from '../../common/LabeledList'; @@ -37,6 +39,7 @@ import AccordianLogs from './AccordianLogs'; import AccordianReferences from './AccordianReferences'; import AccordianText from './AccordianText'; import DetailState from './DetailState'; +import SpanFlameGraph from './SpanFlameGraph'; const getStyles = (theme: GrafanaTheme2) => { return { @@ -106,6 +109,10 @@ const getStyles = (theme: GrafanaTheme2) => { }; }; +export type TraceFlameGraphs = { + [spanID: string]: DataFrame; +}; + export type SpanDetailProps = { detailState: DetailState; linksGetter: ((links: TraceKeyValuePair[], index: number) => TraceLink[]) | TNil; @@ -113,6 +120,7 @@ export type SpanDetailProps = { logsToggle: (spanID: string) => void; processToggle: (spanID: string) => void; span: TraceSpan; + traceToProfilesOptions?: TraceToProfilesOptions; timeZone: TimeZone; tagsToggle: (spanID: string) => void; traceStartTime: number; @@ -124,6 +132,9 @@ export type SpanDetailProps = { focusedSpanId?: string; createFocusSpanLink: (traceId: string, spanId: string) => LinkModel; datasourceType: string; + traceFlameGraphs: TraceFlameGraphs; + setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void; + setRedrawListView: (redraw: {}) => void; }; export default function SpanDetail(props: SpanDetailProps) { @@ -143,6 +154,10 @@ export default function SpanDetail(props: SpanDetailProps) { createSpanLink, createFocusSpanLink, datasourceType, + traceFlameGraphs, + setTraceFlameGraphs, + traceToProfilesOptions, + setRedrawListView, } = props; const { isTagsOpen, @@ -194,6 +209,8 @@ export default function SpanDetail(props: SpanDetailProps) { : []), ]; + const styles = useStyles2(getStyles); + if (span.kind) { overviewItems.push({ key: KIND, @@ -237,8 +254,6 @@ export default function SpanDetail(props: SpanDetailProps) { }); } - const styles = useStyles2(getStyles); - const createLinkButton = (link: SpanLinkDef, type: SpanLinkType, title: string, icon: IconName) => { return ( )} + {config.featureToggles.tracesEmbeddedFlameGraph && + span.tags.some((tag) => tag.key === pyroscopeProfileIdTagKey) && ( + + )} {/* TODO: fix keyboard a11y */} {/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */} diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx index a73543d3c5d..07d93ade138 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.test.tsx @@ -25,6 +25,7 @@ const testSpan = { spanID: 'testSpanID', traceID: 'testTraceID', depth: 3, + tags: [], process: { serviceName: 'some-service', tags: [{ key: 'tag-key', value: 'tag-value' }], @@ -46,6 +47,7 @@ const setup = (propOverrides?: SpanDetailRowProps) => { tagsToggle: jest.fn(), traceStartTime: 1000, theme: createTheme(), + traceFlameGraphs: {}, ...propOverrides, }; return render(); diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx index 477e0f1837f..0ca3064649e 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/SpanDetailRow.tsx @@ -18,12 +18,13 @@ import React from 'react'; import { GrafanaTheme2, LinkModel, TimeZone } from '@grafana/data'; import { Button, clearButtonStyles, stylesFactory, withTheme2 } from '@grafana/ui'; +import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings'; import { autoColor } from '../Theme'; import { SpanLinkFunc } from '../types'; import { TraceLog, TraceSpan, TraceKeyValuePair, TraceLink, TraceSpanReference } from '../types/trace'; -import SpanDetail from './SpanDetail'; +import SpanDetail, { TraceFlameGraphs } from './SpanDetail'; import DetailState from './SpanDetail/DetailState'; import SpanTreeOffset from './SpanTreeOffset'; import TimelineRow from './TimelineRow'; @@ -84,6 +85,7 @@ export type SpanDetailRowProps = { warningsToggle: (spanID: string) => void; stackTracesToggle: (spanID: string) => void; span: TraceSpan; + traceToProfilesOptions?: TraceToProfilesOptions; timeZone: TimeZone; tagsToggle: (spanID: string) => void; traceStartTime: number; @@ -96,6 +98,9 @@ export type SpanDetailRowProps = { createFocusSpanLink: (traceId: string, spanId: string) => LinkModel; datasourceType: string; visibleSpanIds: string[]; + traceFlameGraphs: TraceFlameGraphs; + setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void; + setRedrawListView: (redraw: {}) => void; }; export class UnthemedSpanDetailRow extends React.PureComponent { @@ -121,6 +126,7 @@ export class UnthemedSpanDetailRow extends React.PureComponent
diff --git a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx index cc8d57f38cc..2d69ccf38d2 100644 --- a/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx +++ b/public/app/features/explore/TraceView/components/TraceTimelineViewer/VirtualizedTraceView.tsx @@ -21,6 +21,7 @@ import { RefObject } from 'react'; import { GrafanaTheme2, LinkModel, TimeZone } from '@grafana/data'; import { config, reportInteraction } from '@grafana/runtime'; import { stylesFactory, withTheme2, ToolbarButton } from '@grafana/ui'; +import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings'; import { PEER_SERVICE } from '../constants/tag-keys'; import { CriticalPathSection, SpanBarOptions, SpanLinkFunc, TNil } from '../types'; @@ -30,6 +31,7 @@ import { getColorByKey } from '../utils/color-generator'; import ListView from './ListView'; import SpanBarRow from './SpanBarRow'; +import { TraceFlameGraphs } from './SpanDetail'; import DetailState from './SpanDetail/DetailState'; import SpanDetailRow from './SpanDetailRow'; import { @@ -75,6 +77,7 @@ type TVirtualizedTraceViewOwnProps = { timeZone: TimeZone; findMatchesIDs: Set | TNil; trace: Trace; + traceToProfilesOptions?: TraceToProfilesOptions; spanBarOptions: SpanBarOptions | undefined; linksGetter: (span: TraceSpan, items: TraceKeyValuePair[], itemIndex: number) => TraceLink[]; childrenToggle: (spanID: string) => void; @@ -103,6 +106,10 @@ type TVirtualizedTraceViewOwnProps = { datasourceType: string; headerHeight: number; criticalPath: CriticalPathSection[]; + traceFlameGraphs: TraceFlameGraphs; + setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void; + redrawListView: {}; + setRedrawListView: (redraw: {}) => void; }; export type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TTraceTimeline; @@ -537,6 +544,7 @@ export class UnthemedVirtualizedTraceView extends React.Component
); @@ -611,7 +626,7 @@ export class UnthemedVirtualizedTraceView extends React.Component @@ -627,6 +642,7 @@ export class UnthemedVirtualizedTraceView extends React.Component {this.props.topOfViewRef && ( // only for panel as explore uses content outline to scroll to top | TNil; traceTimeline: TTraceTimeline; trace: Trace; + traceToProfilesOptions?: TraceToProfilesOptions; datasourceType: string; spanBarOptions: SpanBarOptions | undefined; updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void; @@ -107,6 +110,10 @@ export type TProps = { topOfViewRef?: RefObject; headerHeight: number; criticalPath: CriticalPathSection[]; + traceFlameGraphs: TraceFlameGraphs; + setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void; + redrawListView: {}; + setRedrawListView: (redraw: {}) => void; }; type State = { diff --git a/public/app/features/explore/TraceView/createSpanLink.test.ts b/public/app/features/explore/TraceView/createSpanLink.test.ts index 9fd18ad6d5d..8f37213d227 100644 --- a/public/app/features/explore/TraceView/createSpanLink.test.ts +++ b/public/app/features/explore/TraceView/createSpanLink.test.ts @@ -17,7 +17,7 @@ import { TemplateSrv } from '../../templating/template_srv'; import { Trace, TraceSpan } from './components'; import { SpanLinkType } from './components/types/links'; -import { createSpanLinkFactory } from './createSpanLink'; +import { createSpanLinkFactory, pyroscopeProfileIdTagKey } from './createSpanLink'; const dummyTraceData = { duration: 10, traceID: 'trace1', traceName: 'test trace' } as unknown as Trace; const dummyDataFrame = createDataFrame({ @@ -1555,7 +1555,7 @@ function createTraceSpan(overrides: Partial = {}) { value: 'host', }, { - key: 'pyroscope.profile.id', + key: pyroscopeProfileIdTagKey, value: 'hdgfljn23u982nj', }, ], diff --git a/public/app/features/explore/TraceView/createSpanLink.tsx b/public/app/features/explore/TraceView/createSpanLink.tsx index 4feca94f259..02e60b0d95c 100644 --- a/public/app/features/explore/TraceView/createSpanLink.tsx +++ b/public/app/features/explore/TraceView/createSpanLink.tsx @@ -85,7 +85,7 @@ export function createSpanLinkFactory({ profilesDataSourceSettings = getDatasourceSrv().getInstanceSettings(traceToProfilesOptions.datasourceUid); } const hasConfiguredPyroscopeDS = profilesDataSourceSettings?.type === 'grafana-pyroscope-datasource'; - const hasPyroscopeProfile = span.tags.filter((tag) => tag.key === 'pyroscope.profile.id').length > 0; + const hasPyroscopeProfile = span.tags.some((tag) => tag.key === pyroscopeProfileIdTagKey); const shouldCreatePyroscopeLink = hasConfiguredPyroscopeDS && hasPyroscopeProfile; let links: ExploreFieldLinkModel[] = []; @@ -135,6 +135,7 @@ const formatDefaultKeys = (keys: string[]) => { }; const defaultKeys = formatDefaultKeys(['cluster', 'hostname', 'namespace', 'pod', 'service.name', 'service.namespace']); const defaultProfilingKeys = formatDefaultKeys(['service.name', 'service.namespace']); +export const pyroscopeProfileIdTagKey = 'pyroscope.profile.id'; function legacyCreateSpanLinkFactory( splitOpenFn: SplitOpen, diff --git a/public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx b/public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx index 3ca960473cb..a284ee92d82 100644 --- a/public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx +++ b/public/app/plugins/panel/flamegraph/FlameGraphPanel.tsx @@ -25,6 +25,7 @@ export const FlameGraphPanel = (props: PanelProps) => { data={props.data.series[0]} stickyHeader={false} getTheme={() => config.theme2} + showFlameGraphOnly={props.options?.showFlameGraphOnly ?? false} onTableSymbolClick={() => interaction('table_item_selected')} onViewSelected={(view: string) => interaction('view_selected', { view })} onTextAlignSelected={(align: string) => interaction('text_align_selected', { align })} diff --git a/public/app/plugins/panel/traces/TracesPanel.tsx b/public/app/plugins/panel/traces/TracesPanel.tsx index ecf8261c1fa..62a02cce75d 100644 --- a/public/app/plugins/panel/traces/TracesPanel.tsx +++ b/public/app/plugins/panel/traces/TracesPanel.tsx @@ -41,7 +41,6 @@ export const TracesPanel = ({ data, options }: PanelProps) = dataFrames={data.series} scrollElementClass={styles.wrapper} traceProp={traceProp} - queryResponse={data} datasource={dataSource.value} topOfViewRef={topOfViewRef} createSpanLink={options.createSpanLink}