diff --git a/src/components/Explore/TracesByService/MiniREDPanel.tsx b/src/components/Explore/TracesByService/MiniREDPanel.tsx index dc0754e..70e50ae 100644 --- a/src/components/Explore/TracesByService/MiniREDPanel.tsx +++ b/src/components/Explore/TracesByService/MiniREDPanel.tsx @@ -15,7 +15,7 @@ import { EmptyStateScene } from 'components/states/EmptyState/EmptyStateScene'; import { LoadingStateScene } from 'components/states/LoadingState/LoadingStateScene'; import { SkeletonComponent } from '../ByFrameRepeater'; import { barsPanelConfig } from '../panels/barsPanel'; -import { rateByWithStatus } from '../queries/rateByWithStatus'; +import { metricByWithStatus } from '../queries/generateMetricsQuery'; import { StepQueryRunner } from '../queries/StepQueryRunner'; import { RadioButtonList, useStyles2 } from '@grafana/ui'; import { css } from '@emotion/css'; @@ -87,7 +87,7 @@ export class MiniREDPanel extends SceneObjectBase { $data: new StepQueryRunner({ maxDataPoints: this.state.metric === 'duration' ? 24 : 64, datasource: explorationDS, - queries: [this.state.metric === 'duration' ? buildHistogramQuery() : rateByWithStatus(this.state.metric)], + queries: [this.state.metric === 'duration' ? buildHistogramQuery() : metricByWithStatus(this.state.metric)], }), transformations: [...exemplarsTransformations(traceExploration.state.locationService)], }), diff --git a/src/components/Explore/TracesByService/REDPanel.tsx b/src/components/Explore/TracesByService/REDPanel.tsx index 5031cf1..35b3e4f 100644 --- a/src/components/Explore/TracesByService/REDPanel.tsx +++ b/src/components/Explore/TracesByService/REDPanel.tsx @@ -16,7 +16,7 @@ import { EmptyStateScene } from 'components/states/EmptyState/EmptyStateScene'; import { LoadingStateScene } from 'components/states/LoadingState/LoadingStateScene'; import { SkeletonComponent } from '../ByFrameRepeater'; import { barsPanelConfig } from '../panels/barsPanel'; -import { rateByWithStatus } from '../queries/rateByWithStatus'; +import { metricByWithStatus } from '../queries/generateMetricsQuery'; import { StepQueryRunner } from '../queries/StepQueryRunner'; import { css } from '@emotion/css'; import { RadioButtonList, useStyles2 } from '@grafana/ui'; @@ -179,7 +179,7 @@ export class REDPanel extends SceneObjectBase { $data: new StepQueryRunner({ maxDataPoints: this.isDuration() ? 24 : 64, datasource: explorationDS, - queries: [this.isDuration() ? buildHistogramQuery() : rateByWithStatus(metric)], + queries: [this.isDuration() ? buildHistogramQuery() : metricByWithStatus(metric)], }), transformations: [...exemplarsTransformations(traceExploration.state.locationService)], }), diff --git a/src/components/Explore/actions/AddToInvestigationButton.tsx b/src/components/Explore/actions/AddToInvestigationButton.tsx index a083930..e3d4112 100644 --- a/src/components/Explore/actions/AddToInvestigationButton.tsx +++ b/src/components/Explore/actions/AddToInvestigationButton.tsx @@ -5,10 +5,6 @@ import { DataQuery, DataSourceRef } from '@grafana/schema'; import Logo from '../../../../src/img/logo.svg'; import { VAR_DATASOURCE_EXPR } from 'utils/shared'; -export const investigationPluginId = 'grafana-explorations-app'; -export const extensionPointId = 'grafana-exploretraces-app/exploration/v1'; -export const addToInvestigationButtonLabel = 'add panel to investigation'; - export interface AddToInvestigationButtonState extends SceneObjectState { dsUid?: string; query?: string; diff --git a/src/components/Explore/layouts/attributeBreakdown.ts b/src/components/Explore/layouts/attributeBreakdown.ts index 0d60c0f..954070e 100644 --- a/src/components/Explore/layouts/attributeBreakdown.ts +++ b/src/components/Explore/layouts/attributeBreakdown.ts @@ -11,12 +11,12 @@ import { VizPanelState, } from '@grafana/scenes'; import { LayoutSwitcher } from '../LayoutSwitcher'; -import { explorationDS, GRID_TEMPLATE_COLUMNS, MetricFunction, VAR_FILTERS_EXPR } from '../../../utils/shared'; +import { explorationDS, GRID_TEMPLATE_COLUMNS, MetricFunction } from '../../../utils/shared'; import { ByFrameRepeater } from '../ByFrameRepeater'; import { formatLabelValue, getLabelValue, getTraceExplorationScene } from '../../../utils/utils'; import { map, Observable } from 'rxjs'; import { DataFrame, PanelData, reduceField, ReducerID } from '@grafana/data'; -import { rateByWithStatus } from '../queries/rateByWithStatus'; +import { generateMetricsQuery, metricByWithStatus } from '../queries/generateMetricsQuery'; import { barsPanelConfig } from '../panels/barsPanel'; import { linesPanelConfig } from '../panels/linesPanel'; import { StepQueryRunner } from '../queries/StepQueryRunner'; @@ -31,7 +31,7 @@ export function buildNormalLayout( ) { const traceExploration = getTraceExplorationScene(scene); const metric = traceExploration.getMetricVariable().getValue() as MetricFunction; - const query = rateByWithStatus(metric, variable.getValueText()); + const query = metricByWithStatus(metric, variable.getValueText()); return new LayoutSwitcher({ $behaviors: [syncYAxis()], @@ -98,11 +98,18 @@ export function buildNormalLayout( export function getLayoutChild( getTitle: (df: DataFrame, labelName: string) => string, variable: CustomVariable, - metric: string, + metric: MetricFunction, actionsFn: (df: DataFrame) => VizPanelState['headerActions'] -) { +) { return (data: PanelData, frame: DataFrame) => { - const query = `{${sceneGraph.interpolate(variable, `${VAR_FILTERS_EXPR} && ${variable.getValueText()}=${formatLabelValue(getLabelValue(frame))}`)}}`; + const query = sceneGraph.interpolate( + variable, + generateMetricsQuery({ + metric, + extraFilters: `${variable.getValueText()}=${formatLabelValue(getLabelValue(frame))}`, + groupByStatus: true, + }) + ); const panel = (metric === 'duration' ? linesPanelConfig().setUnit('s') : barsPanelConfig()) .setTitle(getTitle(frame, variable.getValueText())) diff --git a/src/components/Explore/panels/PanelMenu.tsx b/src/components/Explore/panels/PanelMenu.tsx index 657fd4c..9738dc8 100644 --- a/src/components/Explore/panels/PanelMenu.tsx +++ b/src/components/Explore/panels/PanelMenu.tsx @@ -1,10 +1,17 @@ -import { PanelMenuItem, toURLRange, urlUtil } from "@grafana/data"; -import { SceneObjectBase, VizPanelMenu, SceneObject, SceneComponentProps, sceneGraph, SceneObjectState } from "@grafana/scenes"; -import React from "react"; -import { AddToInvestigationButton } from "../actions/AddToInvestigationButton"; -import { config, getPluginLinkExtensions } from "@grafana/runtime"; -import { reportAppInteraction, USER_EVENTS_PAGES, USER_EVENTS_ACTIONS } from "utils/analytics"; -import { getDataSource, getTraceExplorationScene } from "utils/utils"; +import { PanelMenuItem, toURLRange, urlUtil } from '@grafana/data'; +import { + SceneObjectBase, + VizPanelMenu, + SceneObject, + SceneComponentProps, + sceneGraph, + SceneObjectState, +} from '@grafana/scenes'; +import React from 'react'; +import { AddToInvestigationButton } from '../actions/AddToInvestigationButton'; +import { config, getPluginLinkExtensions } from '@grafana/runtime'; +import { reportAppInteraction, USER_EVENTS_PAGES, USER_EVENTS_ACTIONS } from 'utils/analytics'; +import { getCurrentStep, getDataSource, getTraceExplorationScene } from 'utils/utils'; const ADD_TO_INVESTIGATION_MENU_TEXT = 'Add to investigation'; const extensionPointId = 'grafana-exploretraces-app/exploration/v1'; @@ -44,7 +51,7 @@ export class PanelMenu extends SceneObjectBase implements VizPan const addToInvestigationButton = new AddToInvestigationButton({ query: this.state.query, labelValue: this.state.labelValue, - }) + }); this._subs.add( addToInvestigationButton?.subscribeToState(() => { subscribeToAddToExploration(this); @@ -63,6 +70,7 @@ export class PanelMenu extends SceneObjectBase implements VizPan this.state.body.addItem(item); } } + setItems(items: PanelMenuItem[]): void { if (this.state.body) { this.state.body.setItems(items); @@ -73,9 +81,7 @@ export class PanelMenu extends SceneObjectBase implements VizPan const { body } = model.useState(); if (body) { - return ( - - ); + return ; } return <>; @@ -86,10 +92,12 @@ const getExploreHref = (model: SceneObject) => { const traceExploration = getTraceExplorationScene(model); const datasource = getDataSource(traceExploration); const timeRange = sceneGraph.getTimeRange(model).state.value; + const step = getCurrentStep(model); + const exploreState = JSON.stringify({ ['traces-explore']: { range: toURLRange(timeRange.raw), - queries: [{ refId: 'A', datasource, query: model.state.query}], + queries: [{ refId: 'A', datasource, query: model.state.query, step }], }, }); const subUrl = config.appSubUrl ?? ''; @@ -99,7 +107,7 @@ const getExploreHref = (model: SceneObject) => { const onExploreClick = () => { reportAppInteraction(USER_EVENTS_PAGES.analyse_traces, USER_EVENTS_ACTIONS.analyse_traces.open_in_explore_clicked); -} +}; const getInvestigationLink = (addToExplorations: AddToInvestigationButton) => { const links = getPluginLinkExtensions({ @@ -116,7 +124,10 @@ const onAddToInvestigationClick = (event: React.MouseEvent, addToInvestigationBu link.onClick(event); } - reportAppInteraction(USER_EVENTS_PAGES.analyse_traces, USER_EVENTS_ACTIONS.analyse_traces.add_to_investigation_clicked); + reportAppInteraction( + USER_EVENTS_PAGES.analyse_traces, + USER_EVENTS_ACTIONS.analyse_traces.add_to_investigation_clicked + ); }; function subscribeToAddToExploration(menu: PanelMenu) { diff --git a/src/components/Explore/queries/generateMetricsQuery.ts b/src/components/Explore/queries/generateMetricsQuery.ts new file mode 100644 index 0000000..808f04c --- /dev/null +++ b/src/components/Explore/queries/generateMetricsQuery.ts @@ -0,0 +1,62 @@ +import { ALL, MetricFunction, VAR_FILTERS_EXPR } from '../../../utils/shared'; + +interface QueryOptions { + metric: MetricFunction; + extraFilters?: string; + groupByKey?: string; + groupByStatus?: boolean; +} + +export function generateMetricsQuery({ metric, groupByKey, extraFilters, groupByStatus }: QueryOptions) { + // Generate span set filters + let filters = `${VAR_FILTERS_EXPR}`; + + if (metric === 'errors') { + filters += ' && status=error'; + } + + if (extraFilters) { + filters += ` && ${extraFilters}`; + } + + if (groupByKey && groupByKey !== ALL) { + filters += ` && ${groupByKey} != nil`; + } + + // Generate metrics function + let metricFn = 'rate()'; + switch (metric) { + case 'errors': + metricFn = 'rate()'; + break; + case 'duration': + metricFn = 'quantile_over_time(duration, 0.9)'; + break; + } + + // Generate group by section + let groupByAttrs = []; + if (groupByKey && groupByKey !== ALL) { + groupByAttrs.push(groupByKey); + } + + if (metric !== 'duration' && groupByStatus) { + groupByAttrs.push('status'); + } + + const groupBy = groupByAttrs.length ? `by(${groupByAttrs.join(', ')})` : ''; + + return `{${filters}} | ${metricFn} ${groupBy}`; +} + +export function metricByWithStatus(metric: MetricFunction, tagKey?: string) { + return { + refId: 'A', + query: generateMetricsQuery({ metric, groupByKey: tagKey, groupByStatus: true }), + queryType: 'traceql', + tableType: 'spans', + limit: 100, + spss: 10, + filters: [], + }; +} diff --git a/src/components/Explore/queries/queries.test.ts b/src/components/Explore/queries/queries.test.ts index 0811438..d842201 100644 --- a/src/components/Explore/queries/queries.test.ts +++ b/src/components/Explore/queries/queries.test.ts @@ -1,35 +1,35 @@ -import { comparisonQuery } from "./comparisonQuery"; -import { buildHistogramQuery } from "./histogram"; -import { rateByWithStatus } from "./rateByWithStatus"; +import { comparisonQuery } from './comparisonQuery'; +import { buildHistogramQuery } from './histogram'; +import { metricByWithStatus } from './generateMetricsQuery'; describe('comparisonQuery', () => { it('should return correct query for no selection', () => { const query = comparisonQuery(); - expect(query).toBe("{}"); + expect(query).toBe('{}'); }); it('should return correct query for a selection', () => { const query = comparisonQuery({ - type: "manual", + type: 'manual', raw: { x: { from: 1728987790508.9485, - to: 1728988005770.9075 + to: 1728988005770.9075, }, y: { from: 8.29360465116279, - to: 21.85174418604651 - } + to: 21.85174418604651, + }, }, timeRange: { from: 1728987791, - to: 1728988006 + to: 1728988006, }, duration: { - from: "0ms", - to: "2s" - } + from: '0ms', + to: '2s', + }, }); - expect(query).toBe("{duration >= 0ms && duration <= 2s}, 10, 1728987791000000000, 1728988006000000000"); + expect(query).toBe('{duration >= 0ms && duration <= 2s}, 10, 1728987791000000000, 1728988006000000000'); }); }); @@ -37,54 +37,54 @@ describe('buildHistogramQuery', () => { it('should return correct query', () => { const query = buildHistogramQuery(); expect(query).toEqual({ - filters: [], - limit: 1000, - query: "{${filters}} | histogram_over_time(duration)", - queryType: "traceql", - refId: "A", - spss: 10, - tableType: "spans" + filters: [], + limit: 1000, + query: '{${filters}} | histogram_over_time(duration)', + queryType: 'traceql', + refId: 'A', + spss: 10, + tableType: 'spans', }); }); }); -describe('rateByWithStatus', () => { +describe('metricByWithStatus', () => { it('should return correct query for no tag', () => { - const query = rateByWithStatus('errors'); + const query = metricByWithStatus('errors'); expect(query).toEqual({ - filters: [], - limit: 100, - query: "{${filters} && status=error} | rate() ", - queryType: "traceql", - refId: "A", - spss: 10, - tableType: "spans" + filters: [], + limit: 100, + query: '{${filters} && status=error} | rate() by(status)', + queryType: 'traceql', + refId: 'A', + spss: 10, + tableType: 'spans', }); }); it('should return correct query for errors', () => { - const query = rateByWithStatus('errors', 'service'); + const query = metricByWithStatus('errors', 'service'); expect(query).toEqual({ - filters: [], - limit: 100, - query: "{${filters} && status=error} | rate() by(service, status)", - queryType: "traceql", - refId: "A", - spss: 10, - tableType: "spans" + filters: [], + limit: 100, + query: '{${filters} && status=error && service != nil} | rate() by(service, status)', + queryType: 'traceql', + refId: 'A', + spss: 10, + tableType: 'spans', }); }); it('should return correct query for duration', () => { - const query = rateByWithStatus('duration', 'service'); + const query = metricByWithStatus('duration', 'service'); expect(query).toEqual({ - filters: [], - limit: 100, - query: "{${filters}} | quantile_over_time(duration, 0.9) by(service)", - queryType: "traceql", - refId: "A", - spss: 10, - tableType: "spans" + filters: [], + limit: 100, + query: '{${filters} && service != nil} | quantile_over_time(duration, 0.9) by(service)', + queryType: 'traceql', + refId: 'A', + spss: 10, + tableType: 'spans', }); }); }); diff --git a/src/components/Explore/queries/rateByWithStatus.ts b/src/components/Explore/queries/rateByWithStatus.ts deleted file mode 100644 index 626aec7..0000000 --- a/src/components/Explore/queries/rateByWithStatus.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { ALL, MetricFunction, VAR_FILTERS_EXPR } from '../../../utils/shared'; - -export function rateByWithStatus(metric: MetricFunction, tagKey?: string) { - const rateBy = tagKey && tagKey !== ALL ? tagKey + ',' : ''; - const rateExtraFilter = tagKey && tagKey !== ALL ? `&& ${tagKey} != nil` : ''; - let expr = `{${VAR_FILTERS_EXPR} ${rateExtraFilter}} | rate() by(${rateBy} status)`; - switch (metric) { - case 'errors': - const errorsBy = tagKey && tagKey !== ALL ? `by(${tagKey}, status)` : ''; - expr = `{${VAR_FILTERS_EXPR} && status=error} | rate() ${errorsBy}`; - break; - case 'duration': - const durationBy = tagKey && tagKey !== ALL ? `by(${tagKey})` : ''; - expr = `{${VAR_FILTERS_EXPR}} | quantile_over_time(duration, 0.9) ${durationBy}`; - break; - } - - return { - refId: 'A', - query: expr, - queryType: 'traceql', - tableType: 'spans', - limit: 100, - spss: 10, - filters: [], - }; -} diff --git a/src/utils/utils.ts b/src/utils/utils.ts index 4dbeb22..baef740 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -3,6 +3,7 @@ import { AdHocFiltersVariable, CustomVariable, DataSourceVariable, + SceneDataQuery, SceneDataState, sceneGraph, SceneObject, @@ -96,7 +97,7 @@ export function getLabelValue(frame: DataFrame, labelName?: string) { return 'No labels'; } - const keys = Object.keys(labels); + const keys = Object.keys(labels).filter((k) => k !== 'p'); // remove the percentile label if (keys.length === 0) { return 'No labels'; } @@ -152,6 +153,12 @@ export function getDatasourceVariable(scene: SceneObject): DataSourceVariable { return variable; } +export function getCurrentStep(scene: SceneObject): number | undefined { + const data = sceneGraph.getData(scene).state.data; + const targetQuery = data?.request?.targets[0]; + return targetQuery ? (targetQuery as SceneDataQuery).step : undefined; +} + export function shouldShowSelection(tab?: ActionViewType): boolean { return tab === 'comparison' || tab === 'traceList'; } @@ -171,4 +178,4 @@ export const formatLabelValue = (value: string) => { return `"${value}"`; } return value; -} +};