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
This commit is contained in:
Joey
2023-11-23 13:36:53 +00:00
committed by GitHub
parent be157399d0
commit 4f46fb412c
30 changed files with 375 additions and 65 deletions

View File

@@ -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"],

View File

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

View File

@@ -108,6 +108,7 @@ export interface FeatureToggles {
awsAsyncQueryCaching?: boolean;
splitScopes?: boolean;
traceToProfiles?: boolean;
tracesEmbeddedFlameGraph?: boolean;
permissionsFilterRemoveSubquery?: boolean;
prometheusConfigOverhaulAuth?: boolean;
configurableSchedulerTick?: boolean;

View File

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

View File

@@ -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 && (
<FlameGraphContextMenu
itemData={clickedItemData}
collapsing={collapsing}

View File

@@ -54,6 +54,11 @@ export type Props = {
*/
vertical?: boolean;
/**
* If true only the flamegraph will be rendered.
*/
showFlameGraphOnly?: boolean;
/**
* Disable behaviour where similar items in the same stack will be collapsed into single item.
*/
@@ -70,6 +75,7 @@ const FlameGraphContainer = ({
stickyHeader,
extraHeaderElements,
vertical,
showFlameGraphOnly,
disableCollapsing,
}: Props) => {
const [focusedItemData, setFocusedItemData] = useState<ClickedItemData>();
@@ -143,35 +149,37 @@ const FlameGraphContainer = ({
// isn't already provided.
<ThemeContext.Provider value={theme}>
<div ref={sizeRef} className={styles.container}>
<FlameGraphHeader
search={search}
setSearch={setSearch}
selectedView={selectedView}
setSelectedView={(view) => {
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 && (
<FlameGraphHeader
search={search}
setSearch={setSearch}
selectedView={selectedView}
setSelectedView={(view) => {
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())}
/>
)}
<div className={styles.body}>
{selectedView !== SelectedView.FlameGraph && (
{!showFlameGraphOnly && selectedView !== SelectedView.FlameGraph && (
<FlameGraphTopTableContainer
data={dataContainer}
onSymbolClick={onSymbolClick}
@@ -204,6 +212,7 @@ const FlameGraphContainer = ({
onFocusPillClick={resetFocus}
onSandwichPillClick={resetSandwich}
colorScheme={colorScheme}
showFlameGraphOnly={showFlameGraphOnly}
collapsing={!disableCollapsing}
/>
)}

View File

@@ -1,2 +1,3 @@
export { default as FlameGraph, type Props } from './FlameGraphContainer';
export { checkFields, getMessageCheckFieldsResult } from './FlameGraph/dataTransform';
export { data } from './FlameGraph/testData/dataNestedSet';

View File

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

View File

@@ -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
1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
89 awsAsyncQueryCaching preview @grafana/aws-datasources false false false false
90 splitScopes preview @grafana/identity-access-team false false true false
91 traceToProfiles experimental @grafana/observability-traces-and-profiling false false false true
92 tracesEmbeddedFlameGraph experimental @grafana/observability-traces-and-profiling false false false true
93 permissionsFilterRemoveSubquery experimental @grafana/backend-platform false false false false
94 prometheusConfigOverhaulAuth GA @grafana/observability-metrics false false false false
95 configurableSchedulerTick experimental @grafana/alerting-squad false false true false

View File

@@ -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"

View File

@@ -8,11 +8,9 @@ import { TraceToProfilesData, TraceToProfilesSettings } from './TraceToProfilesS
const defaultOption: DataSourceSettings<TraceToProfilesData> = {
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(<TraceToProfilesSettings options={defaultOption} onOptionsChange={() => {}} />);
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();

View File

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

View File

@@ -516,7 +516,6 @@ export class Explore extends React.PureComponent<Props, ExploreState> {
dataFrames={dataFrames}
splitOpenFn={this.onSplitOpen('traceView')}
scrollElement={this.scrollElement}
queryResponse={queryResponse}
/>
</ContentOutlineItem>
)

View File

@@ -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<HTMLDivElement>();
return (
@@ -28,7 +23,6 @@ function getTraceView(frames: DataFrame[]) {
dataFrames={frames}
splitOpenFn={() => {}}
traceProp={transformDataFrames(frames[0])!}
queryResponse={mockPanelData}
datasource={undefined}
topOfViewRef={topOfViewRef}
/>

View File

@@ -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<DataQuery, DataSourceJsonData, {}> | undefined;
topOfViewRef?: RefObject<HTMLDivElement>;
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<TraceFlameGraphs>({});
const [redrawListView, setRedrawListView] = useState({});
const styles = useStyles2(getStyles);
@@ -190,6 +191,7 @@ export function TraceView(props: Props) {
<TraceTimelineViewer
findMatchesIDs={spanFilterMatches}
trace={traceProp}
traceToProfilesOptions={traceToProfilesOptions}
datasourceType={datasourceType}
spanBarOptions={spanBarOptions?.spanBar}
traceTimeline={traceTimeline}
@@ -225,6 +227,10 @@ export function TraceView(props: Props) {
topOfViewRef={topOfViewRef}
headerHeight={headerHeight}
criticalPath={criticalPath}
traceFlameGraphs={traceFlameGraphs}
setTraceFlameGraphs={setTraceFlameGraphs}
redrawListView={redrawListView}
setRedrawListView={setRedrawListView}
/>
</>
) : (

View File

@@ -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(
<Provider store={store}>
<TraceViewContainer exploreId="left" dataFrames={frames} splitOpenFn={() => {}} queryResponse={mockPanelData} />
<TraceViewContainer exploreId="left" dataFrames={frames} splitOpenFn={() => {}} />
</Provider>
);
return {

View File

@@ -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}
/>
</PanelChrome>

View File

@@ -47,6 +47,7 @@ const props = {
viewBuffer: 10,
viewBufferMin: 5,
windowScroller: true,
redraw: {},
};
describe('<ListView />', () => {

View File

@@ -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.

View File

@@ -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<HTMLDivElement>();
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<Query>, 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<DataSourceJsonData>,
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<DataSourceJsonData> | 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 (
<div className={styles.flameGraph} ref={sizeRef}>
<div className={styles.flameGraphTitle}>Flame graph</div>
<FlameGraph
data={traceFlameGraphs[profileTagValue]}
getTheme={() => config.theme2}
showFlameGraphOnly={true}
disableCollapsing={true}
/>
</div>
);
}
const getStyles = () => {
return {
flameGraph: css({
label: 'flameGraphInSpan',
margin: '5px',
}),
flameGraphTitle: css({
label: 'flameGraphTitleInSpan',
marginBottom: '5px',
fontWeight: 'bold',
}),
};
};

View File

@@ -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('<SpanDetail>', () => {
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('<SpanDetail>', () => {
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('<SpanDetail>', () => {
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('<SpanDetail>', () => {
render(<SpanDetail {...(props as unknown as SpanDetailProps)} />);
expect(screen.getByText('test-spanID')).toBeInTheDocument();
});
it('renders the flame graph', async () => {
config.featureToggles.tracesEmbeddedFlameGraph = true;
render(<SpanDetail {...(props as unknown as SpanDetailProps)} />);
await act(async () => {
expect(screen.getByText(/16.5 Bil/)).toBeInTheDocument();
expect(screen.getByText(/(Count)/)).toBeInTheDocument();
});
});
});

View File

@@ -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 (
<DataLinkButton
@@ -377,6 +392,17 @@ export default function SpanDetail(props: SpanDetailProps) {
createFocusSpanLink={createFocusSpanLink}
/>
)}
{config.featureToggles.tracesEmbeddedFlameGraph &&
span.tags.some((tag) => tag.key === pyroscopeProfileIdTagKey) && (
<SpanFlameGraph
span={span}
timeZone={timeZone}
traceFlameGraphs={traceFlameGraphs}
setTraceFlameGraphs={setTraceFlameGraphs}
traceToProfilesOptions={traceToProfilesOptions}
setRedrawListView={setRedrawListView}
/>
)}
<small className={styles.debugInfo}>
{/* TODO: fix keyboard a11y */}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}

View File

@@ -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(<UnthemedSpanDetailRow {...(props as SpanDetailRowProps)} />);

View File

@@ -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<SpanDetailRowProps> {
@@ -121,6 +126,7 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
warningsToggle,
stackTracesToggle,
span,
traceToProfilesOptions,
timeZone,
tagsToggle,
traceStartTime,
@@ -133,6 +139,9 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
createFocusSpanLink,
datasourceType,
visibleSpanIds,
traceFlameGraphs,
setTraceFlameGraphs,
setRedrawListView,
} = this.props;
const styles = getStyles(theme);
return (
@@ -167,6 +176,7 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
warningsToggle={warningsToggle}
stackTracesToggle={stackTracesToggle}
span={span}
traceToProfilesOptions={traceToProfilesOptions}
timeZone={timeZone}
tagsToggle={tagsToggle}
traceStartTime={traceStartTime}
@@ -174,6 +184,9 @@ export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProp
focusedSpanId={focusedSpanId}
createFocusSpanLink={createFocusSpanLink}
datasourceType={datasourceType}
traceFlameGraphs={traceFlameGraphs}
setTraceFlameGraphs={setTraceFlameGraphs}
setRedrawListView={setRedrawListView}
/>
</div>
</TimelineRow.Cell>

View File

@@ -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<string> | 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<VirtualizedTra
detailToggle,
spanNameColumnWidth,
trace,
traceToProfilesOptions,
timeZone,
hoverIndentGuideIds,
addHoverIndentGuideId,
@@ -547,6 +555,9 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
createFocusSpanLink,
theme,
datasourceType,
traceFlameGraphs,
setTraceFlameGraphs,
setRedrawListView,
} = this.props;
const detailState = detailStates.get(spanID);
if (!trace || !detailState) {
@@ -571,6 +582,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
warningsToggle={detailWarningsToggle}
stackTracesToggle={detailStackTracesToggle}
span={span}
traceToProfilesOptions={traceToProfilesOptions}
timeZone={timeZone}
tagsToggle={detailTagsToggle}
traceStartTime={trace.startTime}
@@ -582,6 +594,9 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
createFocusSpanLink={createFocusSpanLink}
datasourceType={datasourceType}
visibleSpanIds={visibleSpanIds}
traceFlameGraphs={traceFlameGraphs}
setTraceFlameGraphs={setTraceFlameGraphs}
setRedrawListView={setRedrawListView}
/>
</div>
);
@@ -611,7 +626,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
render() {
const styles = getStyles();
const { scrollElement } = this.props;
const { scrollElement, redrawListView } = this.props;
return (
<>
@@ -627,6 +642,7 @@ export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTra
getIndexFromKey={this.getIndexFromKey}
windowScroller={false}
scrollElement={scrollElement}
redraw={redrawListView}
/>
{this.props.topOfViewRef && ( // only for panel as explore uses content outline to scroll to top
<ToolbarButton

View File

@@ -18,6 +18,7 @@ import React, { RefObject } from 'react';
import { GrafanaTheme2, LinkModel, TimeZone } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import { stylesFactory, withTheme2 } from '@grafana/ui';
import { TraceToProfilesOptions } from 'app/core/components/TraceToProfiles/TraceToProfilesSettings';
import { autoColor } from '../Theme';
import { merge as mergeShortcuts } from '../keyboard-shortcuts';
@@ -26,6 +27,7 @@ import { CriticalPathSection, SpanLinkFunc, TNil } from '../types';
import TTraceTimeline from '../types/TTraceTimeline';
import { TraceSpan, Trace, TraceLog, TraceKeyValuePair, TraceLink, TraceSpanReference } from '../types/trace';
import { TraceFlameGraphs } from './SpanDetail';
import TimelineHeaderRow from './TimelineHeaderRow';
import VirtualizedTraceView from './VirtualizedTraceView';
import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from './types';
@@ -70,6 +72,7 @@ export type TProps = {
findMatchesIDs: Set<string> | 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<HTMLDivElement>;
headerHeight: number;
criticalPath: CriticalPathSection[];
traceFlameGraphs: TraceFlameGraphs;
setTraceFlameGraphs: (flameGraphs: TraceFlameGraphs) => void;
redrawListView: {};
setRedrawListView: (redraw: {}) => void;
};
type State = {

View File

@@ -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<TraceSpan> = {}) {
value: 'host',
},
{
key: 'pyroscope.profile.id',
key: pyroscopeProfileIdTagKey,
value: 'hdgfljn23u982nj',
},
],

View File

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

View File

@@ -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 })}

View File

@@ -41,7 +41,6 @@ export const TracesPanel = ({ data, options }: PanelProps<TracesPanelOptions>) =
dataFrames={data.series}
scrollElementClass={styles.wrapper}
traceProp={traceProp}
queryResponse={data}
datasource={dataSource.value}
topOfViewRef={topOfViewRef}
createSpanLink={options.createSpanLink}