diff --git a/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx b/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx index 5ce78e36621..c82226fc763 100644 --- a/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx +++ b/public/app/features/trails/ActionTabs/MetricOverviewScene.tsx @@ -13,9 +13,9 @@ import { import { Stack, Text, TextLink } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; -import { getUnitFromMetric } from '../AutomaticMetricQueries/units'; import { MetricScene } from '../MetricScene'; import { StatusWrapper } from '../StatusWrapper'; +import { getUnitFromMetric } from '../autoQuery/units'; import { reportExploreMetrics } from '../interactions'; import { updateOtelJoinWithGroupLeft } from '../otel/util'; import { VAR_DATASOURCE_EXPR, VAR_GROUP_BY, VAR_OTEL_GROUP_LEFT } from '../shared'; diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts b/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts deleted file mode 100644 index 94d2287e126..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { getQueryGeneratorFor } from './query-generators/getQueryGeneratorFor'; -import { AutoQueryInfo } from './types'; - -export function getAutoQueriesForMetric(metric: string): AutoQueryInfo { - const metricParts = metric.split('_'); - - const suffix = metricParts.at(-1); - - const generator = getQueryGeneratorFor(suffix); - - if (!generator) { - throw new Error(`Unable to generate queries for metric "${metric}" due to issues with derived suffix "${suffix}"`); - } - - return generator(metricParts); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/graph-builders/heatmap.ts b/public/app/features/trails/AutomaticMetricQueries/graph-builders/heatmap.ts deleted file mode 100644 index 252dba74701..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/graph-builders/heatmap.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { PanelBuilders } from '@grafana/scenes'; -import { HeatmapColorMode } from 'app/plugins/panel/heatmap/types'; - -import { CommonVizParams } from './types'; - -export function heatmapGraphBuilder({ title, unit }: CommonVizParams) { - return PanelBuilders.heatmap() // - .setTitle(title) - .setUnit(unit) - .setOption('calculate', false) - .setOption('color', { - mode: HeatmapColorMode.Scheme, - exponent: 0.5, - scheme: 'Spectral', - steps: 32, - reverse: false, - }); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts b/public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts deleted file mode 100644 index 36578e56ffb..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PanelBuilders } from '@grafana/scenes'; -import { SortOrder } from '@grafana/schema'; -import { TooltipDisplayMode } from '@grafana/ui'; - -import { CommonVizParams } from './types'; - -export function percentilesGraphBuilder({ title, unit }: CommonVizParams) { - return PanelBuilders.timeseries() - .setTitle(title) - .setUnit(unit) - .setCustomFieldConfig('fillOpacity', 9) - .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) - .setOption('legend', { showLegend: false }); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts b/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts deleted file mode 100644 index d04766f23e4..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { PanelBuilders } from '@grafana/scenes'; -import { SortOrder } from '@grafana/schema'; -import { TooltipDisplayMode } from '@grafana/ui'; - -import { CommonVizParams } from './types'; - -export function simpleGraphBuilder({ title, unit }: CommonVizParams) { - return PanelBuilders.timeseries() // - .setTitle(title) - .setUnit(unit) - .setOption('legend', { showLegend: false }) - .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) - .setCustomFieldConfig('fillOpacity', 9); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/graph-builders/types.ts b/public/app/features/trails/AutomaticMetricQueries/graph-builders/types.ts deleted file mode 100644 index 5167b71f003..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/graph-builders/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type CommonVizParams = { - title: string; - unit: string; -}; diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/baseQuery.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/baseQuery.ts deleted file mode 100644 index 5d0759e669d..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/baseQuery.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { VAR_METRIC_EXPR, VAR_FILTERS_EXPR, VAR_OTEL_JOIN_QUERY_EXPR } from 'app/features/trails/shared'; - -const GENERAL_BASE_QUERY = `${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}`; -const GENERAL_RATE_BASE_QUERY = `rate(${GENERAL_BASE_QUERY}[$__rate_interval])`; - -export function getGeneralBaseQuery(rate: boolean) { - return rate - ? `${GENERAL_RATE_BASE_QUERY} ${VAR_OTEL_JOIN_QUERY_EXPR}` - : `${GENERAL_BASE_QUERY} ${VAR_OTEL_JOIN_QUERY_EXPR}`; -} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/default.test.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/default.test.ts deleted file mode 100644 index cddc48925ec..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/default.test.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { VAR_GROUP_BY_EXP } from '../../shared'; -import { AutoQueryDef, AutoQueryInfo } from '../types'; - -import { getGeneralBaseQuery } from './common/baseQuery'; -import { generateQueries } from './default'; - -describe('generateQueries', () => { - const agg = 'sum'; - const unit = 'mockunit'; - - type QueryInfoKey = keyof AutoQueryInfo; - - function testRateIndependentAssertions(queryDef: AutoQueryDef, key: QueryInfoKey) { - describe('regardless of rate', () => { - test(`specified unit must be propagated`, () => expect(queryDef.unit).toBe(unit)); - test(`only one query is expected`, () => expect(queryDef.queries.length).toBe(1)); - const query = queryDef.queries[0]; - test(`specified agg function must be propagated in the query expr`, () => { - const queryAggFunction = query.expr.split('(', 2)[0]; - expect(queryAggFunction).toBe(agg); - }); - if (key === 'breakdown') { - const expectedSuffix = `by(${VAR_GROUP_BY_EXP})`; - test(`breakdown query must end with "${expectedSuffix}"`, () => { - const suffix = query.expr.substring(query.expr.length - expectedSuffix.length); - expect(suffix).toBe(expectedSuffix); - }); - } - }); - } - - function testRateSpecificAssertions(queryDef: AutoQueryDef, rate: boolean, key: QueryInfoKey) { - const query = queryDef.queries[0]; - const firstParen = query.expr.indexOf('('); - const expectedBaseQuery = getGeneralBaseQuery(rate); - const detectedBaseQuery = query.expr.substring(firstParen + 1, firstParen + 1 + expectedBaseQuery.length); - - const inParentheses = rate ? 'overall per-second rate' : 'overall'; - const description = `\${metric} (${inParentheses})`; - - describe(`since rate is ${rate}`, () => { - test(`base query must be "${expectedBaseQuery}"`, () => expect(detectedBaseQuery).toBe(expectedBaseQuery)); - if (key === 'main') { - test(`main panel title contains expected description "${description}"`, () => - expect(queryDef.title).toContain(description)); - } else { - test(`${key} panel title is just "\${metric}"`, () => expect(queryDef.title).toBe('${metric}')); - test(`${key} panel title does not contain description "${description}"`, () => - expect(queryDef.title).not.toContain(description)); - } - - if (key === 'breakdown') { - test(`breakdown query uses "{{\${groupby}}}" as legend`, () => - expect(query.legendFormat).toBe('{{${groupby}}}')); - } else { - test(`preview query uses "${description}" as legend`, () => expect(query.legendFormat).toBe(description)); - } - }); - } - - for (const rate of [true, false]) { - describe(`when rate is ${rate}`, () => { - const queryInfo = generateQueries({ agg, unit, rate }); - - let key: QueryInfoKey; - for (key in queryInfo) { - if (key !== 'variants') { - const queryDef = queryInfo[key]; - describe(`queryInfo.${key}`, () => testRateIndependentAssertions(queryDef, key)); - describe(`queryInfo.${key}`, () => testRateSpecificAssertions(queryDef, rate, key)); - continue; - } - - queryInfo[key].forEach((queryDef, index) => { - describe(`queryInfo.${key}[${index}]`, () => testRateIndependentAssertions(queryDef, key)); - describe(`queryInfo.${key}[${index}]`, () => testRateSpecificAssertions(queryDef, rate, key)); - }); - } - }); - } -}); diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/default.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/default.ts deleted file mode 100644 index b81feb30d62..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/default.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from 'app/features/trails/shared'; - -import { AutoQueryInfo } from '../types'; -import { getPerSecondRateUnit, getUnit } from '../units'; - -import { getGeneralBaseQuery } from './common/baseQuery'; -import { generateCommonAutoQueryInfo } from './common/generator'; - -/** These suffixes will set rate to true */ -const RATE_SUFFIXES = new Set(['count', 'total']); - -const UNSUPPORTED_SUFFIXES = new Set(['sum', 'bucket']); - -/** Non-default aggregation keyed by suffix */ -const SPECIFIC_AGGREGATIONS_FOR_SUFFIX: Record = { - count: 'sum', - total: 'sum', -}; - -function shouldCheckPreviousSuffixForUnit(suffix: string) { - return suffix === 'total'; -} - -const aggLabels: Record = { - avg: 'average', - sum: 'overall', -}; - -function getAggLabel(agg: string) { - return aggLabels[agg] || agg; -} - -export type AutoQueryParameters = { - agg: string; - unit: string; - rate: boolean; -}; - -export function generateQueries({ agg, rate, unit }: AutoQueryParameters): AutoQueryInfo { - const baseQuery = getGeneralBaseQuery(rate); - - const aggregationDescription = rate ? `${getAggLabel(agg)} per-second rate` : `${getAggLabel(agg)}`; - - const description = `${VAR_METRIC_EXPR} (${aggregationDescription})`; - - const mainQueryExpr = `${agg}(${baseQuery})`; - const breakdownQueryExpr = `${agg}(${baseQuery})by(${VAR_GROUP_BY_EXP})`; - - return generateCommonAutoQueryInfo({ - description, - mainQueryExpr, - breakdownQueryExpr, - unit, - }); -} - -export function createDefaultMetricQueryDefs(metricParts: string[]) { - // Get the last part of the metric name - const suffix = metricParts.at(-1); - - // If the suffix is null or is in the set of unsupported suffixes, throw an error because the metric should be delegated to a different generator (summary or histogram) - if (suffix == null || UNSUPPORTED_SUFFIXES.has(suffix)) { - throw new Error(`This function does not support a metric suffix of "${suffix}"`); - } - - // Check if generating rate query and/or aggregation query - const rate = RATE_SUFFIXES.has(suffix); - const agg = SPECIFIC_AGGREGATIONS_FOR_SUFFIX[suffix] || 'avg'; - - // Try to find the unit in the Prometheus metric name - const unitSuffix = shouldCheckPreviousSuffixForUnit(suffix) ? metricParts.at(-2) : suffix; - - // Get the Grafana unit or Grafana rate unit - const unit = rate ? getPerSecondRateUnit(unitSuffix) : getUnit(unitSuffix); - - const params = { - agg, - unit, - rate, - }; - return generateQueries(params); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/getQueryGeneratorFor.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/getQueryGeneratorFor.ts deleted file mode 100644 index e44bfc31e60..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/getQueryGeneratorFor.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { AutoQueryInfo } from '../types'; - -import { createDefaultMetricQueryDefs } from './default'; -import { createHistogramMetricQueryDefs } from './histogram'; -import { createSummaryMetricQueryDefs } from './summary'; - -// TODO: when we have a known unit parameter, use that rather than having the generator functions infer from suffix -export type MetricQueriesGenerator = (metricParts: string[]) => AutoQueryInfo; - -export function getQueryGeneratorFor(suffix?: string): MetricQueriesGenerator { - if (suffix === 'sum') { - return createSummaryMetricQueryDefs; - } - - if (suffix === 'bucket') { - return createHistogramMetricQueryDefs; - } - - return createDefaultMetricQueryDefs; -} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts deleted file mode 100644 index 4fd492d5322..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/histogram.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { PromQuery } from '@grafana/prometheus'; - -import { VAR_FILTERS_EXPR, VAR_GROUP_BY_EXP, VAR_METRIC_EXPR, VAR_OTEL_JOIN_QUERY_EXPR } from '../../shared'; -import { heatmapGraphBuilder } from '../graph-builders/heatmap'; -import { percentilesGraphBuilder } from '../graph-builders/percentiles'; -import { simpleGraphBuilder } from '../graph-builders/simple'; -import { AutoQueryDef } from '../types'; -import { getUnit } from '../units'; - -export function createHistogramMetricQueryDefs(metricParts: string[]) { - const title = `${VAR_METRIC_EXPR}`; - - const unitSuffix = metricParts.at(-2); - - const unit = getUnit(unitSuffix); - - const common = { - title, - unit, - }; - - const p50: AutoQueryDef = { - ...common, - variant: 'p50', - queries: [percentileQuery(50)], - vizBuilder: () => simpleGraphBuilder(p50), - }; - - const breakdown: AutoQueryDef = { - ...common, - variant: 'p50', - queries: [percentileQuery(50, [VAR_GROUP_BY_EXP])], - vizBuilder: () => simpleGraphBuilder(breakdown), - }; - - const percentiles: AutoQueryDef = { - ...common, - variant: 'percentiles', - queries: [99, 90, 50].map((p) => percentileQuery(p)), - vizBuilder: () => percentilesGraphBuilder(percentiles), - }; - - const heatmap: AutoQueryDef = { - ...common, - variant: 'heatmap', - queries: [heatMapQuery()], - vizBuilder: () => heatmapGraphBuilder(heatmap), - }; - - return { preview: heatmap, main: heatmap, variants: [percentiles, heatmap], breakdown: breakdown }; -} - -const BASE_QUERY = `rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])${VAR_OTEL_JOIN_QUERY_EXPR}`; - -function baseQuery(groupings: string[] = []) { - const sumByList = ['le', ...groupings]; - return `sum by(${sumByList.join(', ')}) (${BASE_QUERY})`; -} - -function heatMapQuery(groupings: string[] = []): PromQuery { - return { - refId: 'Heatmap', - expr: baseQuery(groupings), - format: 'heatmap', - }; -} - -function percentileQuery(percentile: number, groupings: string[] = []) { - const percent = percentile / 100; - - let legendFormat = `${percentile}th Percentile`; - - // For the breakdown view, show the label value variable we are grouping by - if (groupings[0]) { - legendFormat = `{{${groupings[0]}}}`; - } - - return { - refId: `Percentile${percentile}`, - expr: `histogram_quantile(${percent}, ${baseQuery(groupings)})`, - legendFormat, - }; -} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts deleted file mode 100644 index cd73d93db25..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/summary.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; -import { AutoQueryInfo } from '../types'; -import { getUnit } from '../units'; - -import { getGeneralBaseQuery } from './common/baseQuery'; -import { generateCommonAutoQueryInfo } from './common/generator'; - -export function createSummaryMetricQueryDefs(metricParts: string[]): AutoQueryInfo { - const suffix = metricParts.at(-1); - if (suffix !== 'sum') { - throw new Error('createSummaryMetricQueryDefs is only to be used for metrics that end in "_sum"'); - } - - const unitSuffix = metricParts.at(-2); - const unit = getUnit(unitSuffix); - - const rate = true; - const baseQuery = getGeneralBaseQuery(rate); - - const subMetric = metricParts.slice(0, -1).join('_'); - const mainQueryExpr = createMeanExpr(`sum(${baseQuery})`); - const breakdownQueryExpr = createMeanExpr(`sum(${baseQuery})by(${VAR_GROUP_BY_EXP})`); - - const operationDescription = `average`; - const description = `${subMetric} (${operationDescription})`; - - function createMeanExpr(expr: string) { - const numerator = expr.replace(VAR_METRIC_EXPR, `${subMetric}_sum`); - const denominator = expr.replace(VAR_METRIC_EXPR, `${subMetric}_count`); - return `${numerator}/${denominator}`; - } - - return generateCommonAutoQueryInfo({ - description, - mainQueryExpr, - breakdownQueryExpr, - unit, - }); -} diff --git a/public/app/features/trails/AutomaticMetricQueries/units.test.ts b/public/app/features/trails/AutomaticMetricQueries/units.test.ts deleted file mode 100644 index 15f61988690..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/units.test.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { getUnitFromMetric } from './units'; - -// Tests for units -describe('getUnitFromMetric', () => { - it('should return the last part of the metric if it is a valid unit', () => { - expect(getUnitFromMetric('go_gc_gomemlimit_bytes')).toBe('bytes'); - expect(getUnitFromMetric('go_gc_duration_seconds')).toBe('seconds'); - }); - - it('should return the second to last part of the metric if it is a valid unit', () => { - expect(getUnitFromMetric('go_gc_heap_allocs_by_size_bytes_count')).toBe('bytes'); - expect(getUnitFromMetric('go_cpu_classes_gc_mark_assist_cpu_seconds_total')).toBe('seconds'); - }); - - it('should return null if no valid unit is found', () => { - expect(getUnitFromMetric('ALERTS')).toBe(null); - }); -}); diff --git a/public/app/features/trails/AutomaticMetricQueries/units.ts b/public/app/features/trails/AutomaticMetricQueries/units.ts deleted file mode 100644 index e025d41af2d..00000000000 --- a/public/app/features/trails/AutomaticMetricQueries/units.ts +++ /dev/null @@ -1,38 +0,0 @@ -const DEFAULT_UNIT = 'short'; - -// Get unit from metric name (e.g. "go_gc_duration_seconds" -> "seconds") -export function getUnitFromMetric(metric: string) { - const metricParts = metric.split('_'); - const suffix = metricParts.at(-1) ?? ''; - const secondToLastSuffix = metricParts.at(-2) ?? ''; - if (UNIT_LIST.includes(suffix)) { - return suffix; - } else if (UNIT_LIST.includes(secondToLastSuffix)) { - return secondToLastSuffix; - } else { - return null; - } -} - -// Get Grafana unit for a panel (e.g. "go_gc_duration_seconds" -> "s") -export function getUnit(metricPart: string | undefined) { - return (metricPart && UNIT_MAP[metricPart]) || DEFAULT_UNIT; -} - -const UNIT_MAP: Record = { - bytes: 'bytes', - seconds: 's', -}; - -const UNIT_LIST = ['bytes', 'seconds']; - -const RATE_UNIT_MAP: Record = { - bytes: 'Bps', // bytes per second - seconds: 'short', // seconds per second is unitless -- this may indicate a count of some resource that is active -}; - -const DEFAULT_RATE_UNIT = 'cps'; // Count per second - -export function getPerSecondRateUnit(metricPart: string | undefined) { - return (metricPart && RATE_UNIT_MAP[metricPart]) || DEFAULT_RATE_UNIT; -} diff --git a/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx b/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx index e655fbdb588..1ab14b84aac 100644 --- a/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx +++ b/public/app/features/trails/Breakdown/LabelBreakdownScene.tsx @@ -29,13 +29,13 @@ import { DataQuery, SortOrder, TooltipDisplayMode } from '@grafana/schema'; import { Alert, Button, Field, LoadingPlaceholder, useStyles2 } from '@grafana/ui'; import { Trans } from 'app/core/internationalization'; -import { getAutoQueriesForMetric } from '../AutomaticMetricQueries/AutoQueryEngine'; -import { AutoQueryDef } from '../AutomaticMetricQueries/types'; import { BreakdownLabelSelector } from '../BreakdownLabelSelector'; import { DataTrail } from '../DataTrail'; import { MetricScene } from '../MetricScene'; import { AddToExplorationButton } from '../MetricSelect/AddToExplorationsButton'; import { StatusWrapper } from '../StatusWrapper'; +import { getAutoQueriesForMetric } from '../autoQuery/getAutoQueriesForMetric'; +import { AutoQueryDef } from '../autoQuery/types'; import { reportExploreMetrics } from '../interactions'; import { updateOtelJoinWithGroupLeft } from '../otel/util'; import { getSortByPreference } from '../services/store'; diff --git a/public/app/features/trails/MetricGraphScene.tsx b/public/app/features/trails/MetricGraphScene.tsx index 69003e9edc5..659d1aaf6c5 100644 --- a/public/app/features/trails/MetricGraphScene.tsx +++ b/public/app/features/trails/MetricGraphScene.tsx @@ -13,8 +13,8 @@ import { } from '@grafana/scenes'; import { useStyles2 } from '@grafana/ui'; -import { AutoVizPanel } from './AutomaticMetricQueries/AutoVizPanel'; import { MetricActionBar } from './MetricScene'; +import { AutoVizPanel } from './autoQuery/components/AutoVizPanel'; import { getTrailSettings } from './utils'; export const MAIN_PANEL_MIN_HEIGHT = 280; diff --git a/public/app/features/trails/MetricScene.tsx b/public/app/features/trails/MetricScene.tsx index 13b3c820cf5..0e3cb6b48f7 100644 --- a/public/app/features/trails/MetricScene.tsx +++ b/public/app/features/trails/MetricScene.tsx @@ -18,13 +18,13 @@ import { getExploreUrl } from '../../core/utils/explore'; import { buildMetricOverviewScene } from './ActionTabs/MetricOverviewScene'; import { buildRelatedMetricsScene } from './ActionTabs/RelatedMetricsScene'; -import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine'; -import { AutoQueryDef, AutoQueryInfo } from './AutomaticMetricQueries/types'; import { buildLabelBreakdownActionScene } from './Breakdown/LabelBreakdownScene'; import { MAIN_PANEL_MAX_HEIGHT, MAIN_PANEL_MIN_HEIGHT, MetricGraphScene } from './MetricGraphScene'; import { buildRelatedLogsScene } from './RelatedLogs/RelatedLogsScene'; import { ShareTrailButton } from './ShareTrailButton'; import { useBookmarkState } from './TrailStore/useBookmarkState'; +import { getAutoQueriesForMetric } from './autoQuery/getAutoQueriesForMetric'; +import { AutoQueryDef, AutoQueryInfo } from './autoQuery/types'; import { reportExploreMetrics } from './interactions'; import { ActionViewDefinition, diff --git a/public/app/features/trails/MetricSelect/previewPanel.ts b/public/app/features/trails/MetricSelect/previewPanel.ts index 791c57ce949..dd5feb9391b 100644 --- a/public/app/features/trails/MetricSelect/previewPanel.ts +++ b/public/app/features/trails/MetricSelect/previewPanel.ts @@ -1,7 +1,7 @@ import { PromQuery } from '@grafana/prometheus'; import { SceneCSSGridItem, SceneQueryRunner, SceneVariableSet } from '@grafana/scenes'; -import { getAutoQueriesForMetric } from '../AutomaticMetricQueries/AutoQueryEngine'; +import { getAutoQueriesForMetric } from '../autoQuery/getAutoQueriesForMetric'; import { getVariablesWithMetricConstant, MDP_METRIC_PREVIEW, trailDS } from '../shared'; import { getColorByIndex } from '../utils'; diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoVizPanel.tsx b/public/app/features/trails/autoQuery/components/AutoVizPanel.tsx similarity index 87% rename from public/app/features/trails/AutomaticMetricQueries/AutoVizPanel.tsx rename to public/app/features/trails/autoQuery/components/AutoVizPanel.tsx index 4912922b8e4..16774e2573c 100644 --- a/public/app/features/trails/AutomaticMetricQueries/AutoVizPanel.tsx +++ b/public/app/features/trails/autoQuery/components/AutoVizPanel.tsx @@ -1,11 +1,11 @@ import { SceneObjectState, SceneObjectBase, SceneComponentProps, VizPanel, SceneQueryRunner } from '@grafana/scenes'; -import { AddToExplorationButton } from '../MetricSelect/AddToExplorationsButton'; -import { MDP_METRIC_OVERVIEW, trailDS } from '../shared'; -import { getMetricSceneFor } from '../utils'; +import { AddToExplorationButton } from '../../MetricSelect/AddToExplorationsButton'; +import { MDP_METRIC_OVERVIEW, trailDS } from '../../shared'; +import { getMetricSceneFor } from '../../utils'; +import { AutoQueryDef } from '../types'; import { AutoVizPanelQuerySelector } from './AutoVizPanelQuerySelector'; -import { AutoQueryDef } from './types'; export interface AutoVizPanelState extends SceneObjectState { panel?: VizPanel; diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoVizPanelQuerySelector.tsx b/public/app/features/trails/autoQuery/components/AutoVizPanelQuerySelector.tsx similarity index 92% rename from public/app/features/trails/AutomaticMetricQueries/AutoVizPanelQuerySelector.tsx rename to public/app/features/trails/autoQuery/components/AutoVizPanelQuerySelector.tsx index 4b31d552681..4a30036c3fa 100644 --- a/public/app/features/trails/AutomaticMetricQueries/AutoVizPanelQuerySelector.tsx +++ b/public/app/features/trails/autoQuery/components/AutoVizPanelQuerySelector.tsx @@ -1,9 +1,8 @@ import { SceneObjectState, SceneObjectBase, SceneComponentProps } from '@grafana/scenes'; import { RadioButtonGroup } from '@grafana/ui'; -import { getMetricSceneFor } from '../utils'; - -import { AutoQueryDef } from './types'; +import { getMetricSceneFor } from '../../utils'; +import { AutoQueryDef } from '../types'; interface QuerySelectorState extends SceneObjectState { queryDef: AutoQueryDef; diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts b/public/app/features/trails/autoQuery/getAutoQueriesForMetric.test.ts similarity index 83% rename from public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts rename to public/app/features/trails/autoQuery/getAutoQueriesForMetric.test.ts index f9bda26a4aa..d110f539f54 100644 --- a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts +++ b/public/app/features/trails/autoQuery/getAutoQueriesForMetric.test.ts @@ -1,4 +1,7 @@ -import { getAutoQueriesForMetric } from './AutoQueryEngine'; +import { VAR_FILTERS_EXPR, VAR_METRIC_EXPR, VAR_OTEL_JOIN_QUERY_EXPR } from '../shared'; + +import { getAutoQueriesForMetric } from './getAutoQueriesForMetric'; +import { generateBaseQuery } from './queryGenerators/baseQuery'; function expandExpr(shortenedExpr: string) { return shortenedExpr.replace('...', '${metric}{${filters}}'); @@ -105,7 +108,7 @@ describe('getAutoQueriesForMetric', () => { test('preview panel has heatmap query', () => { const [{ expr }] = result.preview.queries; - const expected = 'sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query})'; + const expected = 'sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query})'; expect(expr).toBe(expected); }); @@ -161,9 +164,9 @@ describe('getAutoQueriesForMetric', () => { ], // ***WE DEFAULT TO HEATMAP HERE // Bucket - ['PREFIX_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 'short', 1], - ['PREFIX_seconds_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 's', 1], - ['PREFIX_bytes_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 'bytes', 1], + ['PREFIX_bucket', 'sum by(le) (rate(...[$__rate_interval]) ${otel_join_query})', 'short', 1], + ['PREFIX_seconds_bucket', 'sum by(le) (rate(...[$__rate_interval]) ${otel_join_query})', 's', 1], + ['PREFIX_bytes_bucket', 'sum by(le) (rate(...[$__rate_interval]) ${otel_join_query})', 'bytes', 1], ])('Given metric %p expect %p with unit %p', (metric, expr, unit, queryCount) => { const result = getAutoQueriesForMetric(metric); @@ -202,9 +205,9 @@ describe('getAutoQueriesForMetric', () => { 'bytes', ], // Bucket - ['PREFIX_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 'short'], - ['PREFIX_seconds_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 's'], - ['PREFIX_bytes_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 'bytes'], + ['PREFIX_bucket', 'sum by(le) (rate(...[$__rate_interval]) ${otel_join_query})', 'short'], + ['PREFIX_seconds_bucket', 'sum by(le) (rate(...[$__rate_interval]) ${otel_join_query})', 's'], + ['PREFIX_bytes_bucket', 'sum by(le) (rate(...[$__rate_interval]) ${otel_join_query})', 'bytes'], ])('Given metric %p expect %p with unit %p', (metric, expr, unit) => { const result = getAutoQueriesForMetric(metric); @@ -247,17 +250,17 @@ describe('getAutoQueriesForMetric', () => { // Bucket [ 'PREFIX_bucket', - 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])${otel_join_query}))', + 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval]) ${otel_join_query}))', 'short', ], [ 'PREFIX_seconds_bucket', - 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])${otel_join_query}))', + 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval]) ${otel_join_query}))', 's', ], [ 'PREFIX_bytes_bucket', - 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])${otel_join_query}))', + 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval]) ${otel_join_query}))', 'bytes', ], ])('Given metric %p expect %p with unit %p', (metric, expr, unit) => { @@ -292,15 +295,15 @@ describe('getAutoQueriesForMetric', () => { variant: 'percentiles', unit: 'short', exprs: [ - 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', - 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', - 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', + 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', + 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', + 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', ], }, { variant: 'heatmap', unit: 'short', - exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query})'], + exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query})'], }, ], ], @@ -311,15 +314,15 @@ describe('getAutoQueriesForMetric', () => { variant: 'percentiles', unit: 's', exprs: [ - 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', - 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', - 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', + 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', + 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', + 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', ], }, { variant: 'heatmap', unit: 's', - exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query})'], + exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query})'], }, ], ], @@ -330,15 +333,15 @@ describe('getAutoQueriesForMetric', () => { variant: 'percentiles', unit: 'bytes', exprs: [ - 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', - 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', - 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))', + 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', + 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', + 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))', ], }, { variant: 'heatmap', unit: 'bytes', - exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query})'], + exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query})'], }, ], ], @@ -371,3 +374,53 @@ describe('getAutoQueriesForMetric', () => { ); }); }); + +describe('generateBaseQuery', () => { + it('should generate a non-rate non-UTF8 base query', () => { + expect(generateBaseQuery({ isRateQuery: false, isUtf8Metric: false })).toBe( + `${VAR_METRIC_EXPR}{${VAR_FILTERS_EXPR}} ${VAR_OTEL_JOIN_QUERY_EXPR}` + ); + }); + + it('should generate a rate non-UTF8 base query', () => { + expect(generateBaseQuery({ isRateQuery: true, isUtf8Metric: false })).toBe( + `rate(${VAR_METRIC_EXPR}{${VAR_FILTERS_EXPR}}[$__rate_interval]) ${VAR_OTEL_JOIN_QUERY_EXPR}` + ); + }); + + it('should generate a non-rate UTF8 base query', () => { + expect(generateBaseQuery({ isRateQuery: false, isUtf8Metric: true })).toBe( + `{"${VAR_METRIC_EXPR}", ${VAR_FILTERS_EXPR}} ${VAR_OTEL_JOIN_QUERY_EXPR}` + ); + }); + + it('should generate a rate UTF8 base query', () => { + expect(generateBaseQuery({ isRateQuery: true, isUtf8Metric: true })).toBe( + `rate({"${VAR_METRIC_EXPR}", ${VAR_FILTERS_EXPR}}[$__rate_interval]) ${VAR_OTEL_JOIN_QUERY_EXPR}` + ); + }); + + it('should generate a grouped non-UTF8 rate query', () => { + expect( + generateBaseQuery({ + isRateQuery: true, + isUtf8Metric: false, + groupings: ['le', 'job'], + }) + ).toBe( + `sum by(le, job) (rate(${VAR_METRIC_EXPR}{${VAR_FILTERS_EXPR}}[$__rate_interval]) ${VAR_OTEL_JOIN_QUERY_EXPR})` + ); + }); + + it('should generate a grouped UTF8 rate query', () => { + expect( + generateBaseQuery({ + isRateQuery: true, + isUtf8Metric: true, + groupings: ['le', 'instance'], + }) + ).toBe( + `sum by(le, instance) (rate({"${VAR_METRIC_EXPR}", ${VAR_FILTERS_EXPR}}[$__rate_interval]) ${VAR_OTEL_JOIN_QUERY_EXPR})` + ); + }); +}); diff --git a/public/app/features/trails/autoQuery/getAutoQueriesForMetric.ts b/public/app/features/trails/autoQuery/getAutoQueriesForMetric.ts new file mode 100644 index 00000000000..aa09aeb8486 --- /dev/null +++ b/public/app/features/trails/autoQuery/getAutoQueriesForMetric.ts @@ -0,0 +1,36 @@ +import { createDefaultMetricQueryDefs } from './queryGenerators/default'; +import { createHistogramMetricQueryDefs } from './queryGenerators/histogram'; +import { createSummaryMetricQueryDefs } from './queryGenerators/summary'; +import { AutoQueryContext, AutoQueryInfo } from './types'; +import { getUnit } from './units'; + +export function getAutoQueriesForMetric(metric: string): AutoQueryInfo { + const isUtf8Metric = false; + const metricParts = metric.split('_'); + const suffix = metricParts.at(-1); + + // If the suffix is null or is in the set of unsupported suffixes, throw an error because the metric should be delegated to a different generator (summary or histogram) + if (suffix == null) { + throw new Error(`This function does not support a metric suffix of "${suffix}"`); + } + + const unitSuffix = metricParts.at(-2); + const unit = getUnit(unitSuffix); + const ctx: AutoQueryContext = { + metricParts, + isUtf8Metric, + suffix, + unitSuffix, + unit, + }; + + if (suffix === 'sum') { + return createSummaryMetricQueryDefs(ctx); + } + + if (suffix === 'bucket') { + return createHistogramMetricQueryDefs(ctx); + } + + return createDefaultMetricQueryDefs(ctx); +} diff --git a/public/app/features/trails/autoQuery/graphBuilders.ts b/public/app/features/trails/autoQuery/graphBuilders.ts new file mode 100644 index 00000000000..0843499b245 --- /dev/null +++ b/public/app/features/trails/autoQuery/graphBuilders.ts @@ -0,0 +1,41 @@ +import { PanelBuilders } from '@grafana/scenes'; +import { SortOrder, TooltipDisplayMode } from '@grafana/schema/dist/esm/index'; + +import { HeatmapColorMode } from '../../../plugins/panel/heatmap/panelcfg.gen'; + +export type CommonVizParams = { + title: string; + unit: string; +}; + +export function simpleGraphBuilder({ title, unit }: CommonVizParams) { + return PanelBuilders.timeseries() // + .setTitle(title) + .setUnit(unit) + .setOption('legend', { showLegend: false }) + .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) + .setCustomFieldConfig('fillOpacity', 9); +} + +export function heatmapGraphBuilder({ title, unit }: CommonVizParams) { + return PanelBuilders.heatmap() // + .setTitle(title) + .setUnit(unit) + .setOption('calculate', false) + .setOption('color', { + mode: HeatmapColorMode.Scheme, + exponent: 0.5, + scheme: 'Spectral', + steps: 32, + reverse: false, + }); +} + +export function percentilesGraphBuilder({ title, unit }: CommonVizParams) { + return PanelBuilders.timeseries() + .setTitle(title) + .setUnit(unit) + .setCustomFieldConfig('fillOpacity', 9) + .setOption('tooltip', { mode: TooltipDisplayMode.Multi, sort: SortOrder.Descending }) + .setOption('legend', { showLegend: false }); +} diff --git a/public/app/features/trails/autoQuery/queryGenerators/baseQuery.test.ts b/public/app/features/trails/autoQuery/queryGenerators/baseQuery.test.ts new file mode 100644 index 00000000000..dd7ae9a2fe5 --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/baseQuery.test.ts @@ -0,0 +1,43 @@ +import { generateBaseQuery } from './baseQuery'; + +describe('generateBaseQuery', () => { + it('should return base query without rate and groupings', () => { + const result = generateBaseQuery({}); + expect(result).toBe('${metric}{${filters}} ${otel_join_query}'); + }); + + it('should return rate base query without groupings', () => { + const result = generateBaseQuery({ isRateQuery: true }); + expect(result).toBe('rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}'); + }); + + it('should return base query with groupings', () => { + const result = generateBaseQuery({ groupings: ['job', 'instance'] }); + expect(result).toBe('sum by(job, instance) (${metric}{${filters}} ${otel_join_query})'); + }); + + it('should return rate base query with groupings', () => { + const result = generateBaseQuery({ isRateQuery: true, groupings: ['job', 'instance'] }); + expect(result).toBe('sum by(job, instance) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query})'); + }); + + it('should return UTF-8 base query without rate and groupings', () => { + const result = generateBaseQuery({ isUtf8Metric: true }); + expect(result).toBe('{"${metric}", ${filters}} ${otel_join_query}'); + }); + + it('should return UTF-8 rate base query without groupings', () => { + const result = generateBaseQuery({ isRateQuery: true, isUtf8Metric: true }); + expect(result).toBe('rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query}'); + }); + + it('should return UTF-8 base query with groupings', () => { + const result = generateBaseQuery({ isUtf8Metric: true, groupings: ['job', 'instance'] }); + expect(result).toBe('sum by(job, instance) ({"${metric}", ${filters}} ${otel_join_query})'); + }); + + it('should return UTF-8 rate base query with groupings', () => { + const result = generateBaseQuery({ isRateQuery: true, isUtf8Metric: true, groupings: ['job', 'instance'] }); + expect(result).toBe('sum by(job, instance) (rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query})'); + }); +}); diff --git a/public/app/features/trails/autoQuery/queryGenerators/baseQuery.ts b/public/app/features/trails/autoQuery/queryGenerators/baseQuery.ts new file mode 100644 index 00000000000..c57d81b4908 --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/baseQuery.ts @@ -0,0 +1,37 @@ +import { VAR_FILTERS_EXPR, VAR_METRIC_EXPR, VAR_OTEL_JOIN_QUERY_EXPR } from '../../shared'; + +// For usual non-utf8-metrics we use filters in the curly braces +// metric_name{filter_label="filter_value"} +const BASE_QUERY_TEMPLATE = `${VAR_METRIC_EXPR}{${VAR_FILTERS_EXPR}}`; +const RATE_BASE_QUERY_TEMPLATE = `rate(${BASE_QUERY_TEMPLATE}[$__rate_interval])`; + +// For utf8 metrics we need to put the metric name inside curly braces with filters +// {"utf8.metric", filter_label="filter_val"} +const BASE_QUERY_UTF8_METRIC_TEMPLATE = `{"${VAR_METRIC_EXPR}", ${VAR_FILTERS_EXPR}}`; +const RATE_BASE_QUERY_UTF8_METRIC_TEMPLATE = `rate(${BASE_QUERY_UTF8_METRIC_TEMPLATE}[$__rate_interval])`; + +export function generateBaseQuery({ + isRateQuery = false, + groupings = [], + isUtf8Metric = false, +}: { + isRateQuery?: boolean; + groupings?: string[]; + isUtf8Metric?: boolean; +}): string { + // Determine base query template + const baseQuery = isUtf8Metric + ? isRateQuery + ? RATE_BASE_QUERY_UTF8_METRIC_TEMPLATE + : BASE_QUERY_UTF8_METRIC_TEMPLATE + : isRateQuery + ? RATE_BASE_QUERY_TEMPLATE + : BASE_QUERY_TEMPLATE; + + // Apply groupings (e.g., `sum by(le, instance)`) + if (groupings.length > 0) { + return `sum by(${groupings.join(', ')}) (${baseQuery} ${VAR_OTEL_JOIN_QUERY_EXPR})`; + } + + return `${baseQuery} ${VAR_OTEL_JOIN_QUERY_EXPR}`; +} diff --git a/public/app/features/trails/autoQuery/queryGenerators/common.test.ts b/public/app/features/trails/autoQuery/queryGenerators/common.test.ts new file mode 100644 index 00000000000..3cf4d266df0 --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/common.test.ts @@ -0,0 +1,80 @@ +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; + +import { CommonQueryInfoParams, generateCommonAutoQueryInfo } from './common'; + +describe('generateCommonAutoQueryInfo', () => { + const params: CommonQueryInfoParams = { + description: 'Test Description', + mainQueryExpr: 'rate(test_metric[5m])', + breakdownQueryExpr: 'sum by (label) (test_metric)', + unit: 'short', + }; + + it('should generate a valid AutoQueryInfo object with main, preview, and breakdown variants', () => { + const result = generateCommonAutoQueryInfo(params); + + expect(result).toHaveProperty('main'); + expect(result).toHaveProperty('preview'); + expect(result).toHaveProperty('breakdown'); + expect(result).toHaveProperty('variants'); + }); + + it('should configure the main variant correctly', () => { + const result = generateCommonAutoQueryInfo(params); + + const { main } = result; + expect(main).toMatchObject({ + title: params.description, + unit: params.unit, + queries: [ + { + refId: 'A', + expr: params.mainQueryExpr, + legendFormat: params.description, + }, + ], + variant: 'main', + }); + }); + + it('should configure the preview variant correctly', () => { + const result = generateCommonAutoQueryInfo(params); + + const { preview } = result; + expect(preview).toMatchObject({ + title: VAR_METRIC_EXPR, + unit: params.unit, + queries: [ + { + refId: 'A', + expr: params.mainQueryExpr, + legendFormat: params.description, + }, + ], + variant: 'preview', + }); + }); + + it('should configure the breakdown variant correctly', () => { + const result = generateCommonAutoQueryInfo(params); + + const { breakdown } = result; + expect(breakdown).toMatchObject({ + title: VAR_METRIC_EXPR, + unit: params.unit, + queries: [ + { + refId: 'A', + expr: params.breakdownQueryExpr, + legendFormat: `{{${VAR_GROUP_BY_EXP}}}`, + }, + ], + variant: 'breakdown', + }); + }); + + it('should return an empty variants array', () => { + const result = generateCommonAutoQueryInfo(params); + expect(result.variants).toEqual([]); + }); +}); diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/generator.ts b/public/app/features/trails/autoQuery/queryGenerators/common.ts similarity index 80% rename from public/app/features/trails/AutomaticMetricQueries/query-generators/common/generator.ts rename to public/app/features/trails/autoQuery/queryGenerators/common.ts index fbc7ff5d3cb..b0e82b4baa3 100644 --- a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/generator.ts +++ b/public/app/features/trails/autoQuery/queryGenerators/common.ts @@ -1,5 +1,6 @@ -import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../../shared'; -import { simpleGraphBuilder } from '../../graph-builders/simple'; +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; +import { simpleGraphBuilder } from '../graphBuilders'; +import { AutoQueryInfo } from '../types'; export type CommonQueryInfoParams = { description: string; @@ -13,9 +14,9 @@ export function generateCommonAutoQueryInfo({ mainQueryExpr, breakdownQueryExpr, unit, -}: CommonQueryInfoParams) { +}: CommonQueryInfoParams): AutoQueryInfo { const common = { - title: `${VAR_METRIC_EXPR}`, + title: VAR_METRIC_EXPR, unit, }; @@ -34,8 +35,7 @@ export function generateCommonAutoQueryInfo({ }; const preview = { - ...main, - title: `${VAR_METRIC_EXPR}`, + ...common, queries: [{ ...mainQuery, legendFormat: description }], vizBuilder: () => simpleGraphBuilder(preview), variant: 'preview', diff --git a/public/app/features/trails/autoQuery/queryGenerators/default.test.ts b/public/app/features/trails/autoQuery/queryGenerators/default.test.ts new file mode 100644 index 00000000000..a5417eb1e4a --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/default.test.ts @@ -0,0 +1,41 @@ +import { AutoQueryContext } from '../types'; + +import { createDefaultMetricQueryDefs } from './default'; + +describe('createDefaultMetricQueryDefs', () => { + it('should generate correct AutoQueryInfo for rate query with UTF-8 metric', () => { + const context: AutoQueryContext = { + metricParts: ['http.requests', 'total'], + suffix: 'total', + isUtf8Metric: true, + unit: 'cps', + }; + + const result = createDefaultMetricQueryDefs(context); + + expect(result.main.title).toBe('${metric} (overall per-second rate)'); + expect(result.main.queries[0].expr).toBe( + 'sum(rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query})' + ); + expect(result.breakdown.queries[0].expr).toBe( + 'sum(rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})' + ); + expect(result.preview.unit).toBe('cps'); + }); + + it('should generate correct AutoQueryInfo for non-rate query without UTF-8 metric', () => { + const context: AutoQueryContext = { + metricParts: ['cpu', 'usage', 'seconds'], + suffix: 'avg', + isUtf8Metric: false, + unit: 's', + }; + + const result = createDefaultMetricQueryDefs(context); + + expect(result.main.title).toBe('${metric} (average)'); + expect(result.main.queries[0].expr).toBe('avg(${metric}{${filters}} ${otel_join_query})'); + expect(result.breakdown.queries[0].expr).toBe('avg(${metric}{${filters}} ${otel_join_query})by(${groupby})'); + expect(result.preview.unit).toBe('short'); + }); +}); diff --git a/public/app/features/trails/autoQuery/queryGenerators/default.ts b/public/app/features/trails/autoQuery/queryGenerators/default.ts new file mode 100644 index 00000000000..bd8074429c0 --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/default.ts @@ -0,0 +1,46 @@ +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; +import { AutoQueryContext, AutoQueryInfo } from '../types'; +import { getPerSecondRateUnit, getUnit } from '../units'; + +import { generateBaseQuery } from './baseQuery'; +import { generateCommonAutoQueryInfo } from './common'; + +const RATE_SUFFIXES = new Set(['count', 'total']); +const SPECIFIC_AGGREGATIONS_FOR_SUFFIX: Record = { + count: 'sum', + total: 'sum', +}; +const aggLabels: Record = { + avg: 'average', + sum: 'overall', +}; + +function getAggLabel(agg: string): string { + return aggLabels[agg] || agg; +} + +export function createDefaultMetricQueryDefs(context: AutoQueryContext): AutoQueryInfo { + const { metricParts, suffix, isUtf8Metric } = context; + const unitSuffix = suffix === 'total' ? metricParts.at(-2) : suffix; + + // Determine query type and unit + const isRateQuery = RATE_SUFFIXES.has(suffix); + const aggregation = SPECIFIC_AGGREGATIONS_FOR_SUFFIX[suffix] || 'avg'; + const unit = isRateQuery ? getPerSecondRateUnit(unitSuffix) : getUnit(unitSuffix); + + // Generate base query and descriptions + const baseQuery = generateBaseQuery({ isRateQuery, isUtf8Metric }); + const aggregationDescription = `${getAggLabel(aggregation)}${isRateQuery ? ' per-second rate' : ''}`; + const description = `${VAR_METRIC_EXPR} (${aggregationDescription})`; + + // Create query expressions + const mainQueryExpr = `${aggregation}(${baseQuery})`; + const breakdownQueryExpr = `${aggregation}(${baseQuery})by(${VAR_GROUP_BY_EXP})`; + + return generateCommonAutoQueryInfo({ + description, + mainQueryExpr, + breakdownQueryExpr, + unit, + }); +} diff --git a/public/app/features/trails/autoQuery/queryGenerators/histogram.test.ts b/public/app/features/trails/autoQuery/queryGenerators/histogram.test.ts new file mode 100644 index 00000000000..d02436389bf --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/histogram.test.ts @@ -0,0 +1,101 @@ +import { AutoQueryContext } from '../types'; + +import { createHistogramMetricQueryDefs } from './histogram'; + +describe('createHistogramMetricQueryDefs utf8=false', () => { + const ctx: AutoQueryContext = { + metricParts: ['test', 'latency', 'seconds', 'bucket'], + isUtf8Metric: false, + suffix: 'bucket', + unitSuffix: 'seconds', + unit: 's', + }; + + it('should create the correct title and unit for metricParts', () => { + const result = createHistogramMetricQueryDefs(ctx); + expect(result.preview.title).toBe('${metric}'); + expect(result.preview.unit).toBe('s'); + }); + + it('should generate correct p50 AutoQueryDef', () => { + const result = createHistogramMetricQueryDefs(ctx); + const p50Query = result.breakdown.queries[0]; + + expect(p50Query.expr).toBe( + 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + expect(p50Query.legendFormat).toBe('{{${groupby}}}'); + }); + + it('should generate correct percentiles AutoQueryDef', () => { + const result = createHistogramMetricQueryDefs(ctx); + const percentileQueries = result.variants[0].queries; + + expect(percentileQueries[0].expr).toBe( + 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + expect(percentileQueries[1].expr).toBe( + 'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + expect(percentileQueries[2].expr).toBe( + 'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + }); + + it('should generate correct heatmap AutoQueryDef', () => { + const result = createHistogramMetricQueryDefs(ctx); + const heatmapQuery = result.preview.queries[0]; + + expect(heatmapQuery.expr).toBe('sum by(le) (rate(${metric}{${filters}}[$__rate_interval]) ${otel_join_query})'); + expect(result.preview.variant).toBe('heatmap'); + }); +}); + +describe('createHistogramMetricQueryDefs utf8=true', () => { + const ctx: AutoQueryContext = { + metricParts: ['test', 'latency', 'seconds', 'bucket'], + isUtf8Metric: true, + suffix: 'bucket', + unitSuffix: 'seconds', + unit: 's', + }; + + it('should create the correct title and unit for metricParts', () => { + const result = createHistogramMetricQueryDefs(ctx); + expect(result.preview.title).toBe('${metric}'); + expect(result.preview.unit).toBe('s'); + }); + + it('should generate correct p50 AutoQueryDef', () => { + const result = createHistogramMetricQueryDefs(ctx); + const p50Query = result.breakdown.queries[0]; + + expect(p50Query.expr).toBe( + 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + expect(p50Query.legendFormat).toBe('{{${groupby}}}'); + }); + + it('should generate correct percentiles AutoQueryDef', () => { + const result = createHistogramMetricQueryDefs(ctx); + const percentileQueries = result.variants[0].queries; + + expect(percentileQueries[0].expr).toBe( + 'histogram_quantile(0.99, sum by(le) (rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + expect(percentileQueries[1].expr).toBe( + 'histogram_quantile(0.9, sum by(le) (rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + expect(percentileQueries[2].expr).toBe( + 'histogram_quantile(0.5, sum by(le) (rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query}))' + ); + }); + + it('should generate correct heatmap AutoQueryDef', () => { + const result = createHistogramMetricQueryDefs(ctx); + const heatmapQuery = result.preview.queries[0]; + + expect(heatmapQuery.expr).toBe('sum by(le) (rate({"${metric}", ${filters}}[$__rate_interval]) ${otel_join_query})'); + expect(result.preview.variant).toBe('heatmap'); + }); +}); diff --git a/public/app/features/trails/autoQuery/queryGenerators/histogram.ts b/public/app/features/trails/autoQuery/queryGenerators/histogram.ts new file mode 100644 index 00000000000..c4fc17ad331 --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/histogram.ts @@ -0,0 +1,77 @@ +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; +import { heatmapGraphBuilder, percentilesGraphBuilder, simpleGraphBuilder } from '../graphBuilders'; +import { AutoQueryContext, AutoQueryDef } from '../types'; + +import { generateBaseQuery } from './baseQuery'; + +export function createHistogramMetricQueryDefs(context: AutoQueryContext) { + const { unit } = context; + + const common = { + title: VAR_METRIC_EXPR, + unit, + }; + + const p50: AutoQueryDef = { + ...common, + variant: 'p50', + queries: [percentileQuery(context, 50)], + vizBuilder: () => simpleGraphBuilder(p50), + }; + + const breakdown: AutoQueryDef = { + ...common, + variant: 'p50', + queries: [percentileQuery(context, 50, [VAR_GROUP_BY_EXP])], + vizBuilder: () => simpleGraphBuilder(breakdown), + }; + + const percentiles: AutoQueryDef = { + ...common, + variant: 'percentiles', + queries: [99, 90, 50].map((p) => percentileQuery(context, p)), + vizBuilder: () => percentilesGraphBuilder(percentiles), + }; + + const heatmap: AutoQueryDef = { + ...common, + variant: 'heatmap', + queries: [ + { + refId: 'Heatmap', + expr: generateBaseQuery({ + isRateQuery: true, + isUtf8Metric: context.isUtf8Metric, + groupings: ['le'], + }), + format: 'heatmap', + }, + ], + vizBuilder: () => heatmapGraphBuilder(heatmap), + }; + + return { preview: heatmap, main: heatmap, variants: [percentiles, heatmap], breakdown: breakdown }; +} + +function percentileQuery(context: AutoQueryContext, percentile: number, groupings: string[] = []) { + const percent = percentile / 100; + + let legendFormat = `${percentile}th Percentile`; + + // For the breakdown view, show the label value variable we are grouping by + if (groupings[0]) { + legendFormat = `{{${groupings[0]}}}`; + } + + const query = generateBaseQuery({ + isRateQuery: true, + isUtf8Metric: context.isUtf8Metric, + groupings: ['le', ...groupings], + }); + + return { + refId: `Percentile${percentile}`, + expr: `histogram_quantile(${percent}, ${query})`, + legendFormat, + }; +} diff --git a/public/app/features/trails/autoQuery/queryGenerators/summary.test.ts b/public/app/features/trails/autoQuery/queryGenerators/summary.test.ts new file mode 100644 index 00000000000..48fcf4c55a1 --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/summary.test.ts @@ -0,0 +1,49 @@ +import { AutoQueryContext } from '../types'; + +import { createSummaryMetricQueryDefs } from './summary'; + +describe('createSummaryMetricQueryDefs', () => { + it('should generate correct AutoQueryInfo with rate query and UTF-8 metric', () => { + const context: AutoQueryContext = { + metricParts: ['http.requests', 'sum'], + isUtf8Metric: true, + unit: 'ms', + suffix: 'sum', + }; + + const result = createSummaryMetricQueryDefs(context); + + expect(result.preview.title).toBe('${metric}'); + expect(result.main.title).toBe('http.requests (average)'); + expect(result.breakdown.title).toBe('${metric}'); + expect(result.preview.queries[0].expr).toBe( + 'sum(rate({"http.requests_sum", ${filters}}[$__rate_interval]) ${otel_join_query})/sum(rate({"http.requests_count", ${filters}}[$__rate_interval]) ${otel_join_query})' + ); + expect(result.breakdown.queries[0].expr).toBe( + 'sum(rate({"http.requests_sum", ${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})/sum(rate({"http.requests_count", ${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})' + ); + expect(result.preview.unit).toBe('ms'); + }); + + it('should generate correct AutoQueryInfo without UTF-8 metric', () => { + const context: AutoQueryContext = { + metricParts: ['cpu', 'usage', 'seconds', 'sum'], + isUtf8Metric: false, + unit: 's', + suffix: 'sum', + }; + + const result = createSummaryMetricQueryDefs(context); + + expect(result.preview.title).toBe('${metric}'); + expect(result.main.title).toBe('cpu_usage_seconds (average)'); + expect(result.breakdown.title).toBe('${metric}'); + expect(result.preview.queries[0].expr).toBe( + 'sum(rate(cpu_usage_seconds_sum{${filters}}[$__rate_interval]) ${otel_join_query})/sum(rate(cpu_usage_seconds_count{${filters}}[$__rate_interval]) ${otel_join_query})' + ); + expect(result.breakdown.queries[0].expr).toBe( + 'sum(rate(cpu_usage_seconds_sum{${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})/sum(rate(cpu_usage_seconds_count{${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})' + ); + expect(result.preview.unit).toBe('s'); + }); +}); diff --git a/public/app/features/trails/autoQuery/queryGenerators/summary.ts b/public/app/features/trails/autoQuery/queryGenerators/summary.ts new file mode 100644 index 00000000000..589c8228ebb --- /dev/null +++ b/public/app/features/trails/autoQuery/queryGenerators/summary.ts @@ -0,0 +1,27 @@ +import { VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared'; +import { AutoQueryContext, AutoQueryInfo } from '../types'; + +import { generateBaseQuery } from './baseQuery'; +import { generateCommonAutoQueryInfo } from './common'; + +export function createSummaryMetricQueryDefs(context: AutoQueryContext): AutoQueryInfo { + const { metricParts, isUtf8Metric, unit } = context; + const subMetric = metricParts.slice(0, -1).join('_'); + const description = `${subMetric} (average)`; + const baseQuery = generateBaseQuery({ isRateQuery: true, isUtf8Metric }); + const mainQueryExpr = createMeanExpr(`sum(${baseQuery})`, subMetric); + const breakdownQueryExpr = createMeanExpr(`sum(${baseQuery})by(${VAR_GROUP_BY_EXP})`, subMetric); + + return generateCommonAutoQueryInfo({ + description, + mainQueryExpr, + breakdownQueryExpr, + unit, + }); +} + +function createMeanExpr(expr: string, subMetric: string): string { + const numerator = expr.replace(VAR_METRIC_EXPR, `${subMetric}_sum`); + const denominator = expr.replace(VAR_METRIC_EXPR, `${subMetric}_count`); + return `${numerator}/${denominator}`; +} diff --git a/public/app/features/trails/AutomaticMetricQueries/types.ts b/public/app/features/trails/autoQuery/types.ts similarity index 75% rename from public/app/features/trails/AutomaticMetricQueries/types.ts rename to public/app/features/trails/autoQuery/types.ts index e104dd6dab2..d6161524159 100644 --- a/public/app/features/trails/AutomaticMetricQueries/types.ts +++ b/public/app/features/trails/autoQuery/types.ts @@ -17,3 +17,11 @@ export interface AutoQueryInfo { } export type VizBuilder = () => VizPanelBuilder<{}, {}>; + +export type AutoQueryContext = { + metricParts: string[]; + isUtf8Metric: boolean; + unit: string; + suffix: string; + unitSuffix?: string; +}; diff --git a/public/app/features/trails/autoQuery/units.test.ts b/public/app/features/trails/autoQuery/units.test.ts new file mode 100644 index 00000000000..d039253b33c --- /dev/null +++ b/public/app/features/trails/autoQuery/units.test.ts @@ -0,0 +1,94 @@ +import { DEFAULT_RATE_UNIT, DEFAULT_UNIT, getPerSecondRateUnit, getUnit, getUnitFromMetric } from './units'; + +describe('getUnitFromMetric', () => { + it('should return null for an empty string input', () => { + expect(getUnitFromMetric('')).toBe(null); + }); + + it('should return the last part of the metric if it is a valid unit', () => { + expect(getUnitFromMetric('go_gc_gomemlimit_bytes')).toBe('bytes'); + expect(getUnitFromMetric('go_gc_duration_seconds')).toBe('seconds'); + }); + + it('should return the second to last part of the metric if it is a valid unit', () => { + expect(getUnitFromMetric('go_gc_heap_allocs_by_size_bytes_count')).toBe('bytes'); + expect(getUnitFromMetric('go_cpu_classes_gc_mark_assist_cpu_seconds_total')).toBe('seconds'); + }); + + it('should return null if no valid unit is found', () => { + expect(getUnitFromMetric('ALERTS')).toBe(null); + expect(getUnitFromMetric('utf8 metric with.dot')).toBe(null); + }); + + it('should handle metrics with extra underscores', () => { + expect(getUnitFromMetric('go_gc__duration__seconds')).toBe('seconds'); + }); + + it('should return null if the metric ends with an invalid unit', () => { + expect(getUnitFromMetric('go_gc_duration_invalidunit')).toBe(null); + }); + + it('should return the last unit if the metric contains only valid units', () => { + expect(getUnitFromMetric('bytes_seconds')).toBe('seconds'); + }); +}); + +describe('getUnit', () => { + it('should return the mapped unit for a valid metric part', () => { + expect(getUnit('bytes')).toBe('bytes'); + expect(getUnit('seconds')).toBe('s'); + }); + + it('should return the default unit if the metric part is undefined', () => { + expect(getUnit(undefined)).toBe(DEFAULT_UNIT); + }); + + it('should return the default unit if the metric part is an empty string', () => { + expect(getUnit('')).toBe(DEFAULT_UNIT); + }); + + it('should return the default unit if the metric part is not in UNIT_MAP', () => { + expect(getUnit('invalidPart')).toBe(DEFAULT_UNIT); + }); + + it('should handle case sensitivity correctly', () => { + expect(getUnit('BYTES')).toBe(DEFAULT_UNIT); + expect(getUnit('Seconds')).toBe(DEFAULT_UNIT); + }); + + it('should not throw errors for unusual input', () => { + expect(() => getUnit('123')).not.toThrow(); + expect(() => getUnit('some_random_string')).not.toThrow(); + expect(() => getUnit(undefined)).not.toThrow(); + }); +}); + +describe('getPerSecondRateUnit', () => { + it('should return the mapped rate unit for a valid metric part', () => { + expect(getPerSecondRateUnit('bytes')).toBe('Bps'); + expect(getPerSecondRateUnit('seconds')).toBe('short'); + }); + + it('should return the default rate unit if the metric part is undefined', () => { + expect(getPerSecondRateUnit(undefined)).toBe(DEFAULT_RATE_UNIT); + }); + + it('should return the default rate unit if the metric part is an empty string', () => { + expect(getPerSecondRateUnit('')).toBe(DEFAULT_RATE_UNIT); + }); + + it('should return the default rate unit if the metric part is not in RATE_UNIT_MAP', () => { + expect(getPerSecondRateUnit('invalidPart')).toBe(DEFAULT_RATE_UNIT); + }); + + it('should handle case sensitivity correctly', () => { + expect(getPerSecondRateUnit('BYTES')).toBe(DEFAULT_RATE_UNIT); + expect(getPerSecondRateUnit('Seconds')).toBe(DEFAULT_RATE_UNIT); + }); + + it('should not throw errors for unusual input', () => { + expect(() => getPerSecondRateUnit('123')).not.toThrow(); + expect(() => getPerSecondRateUnit('some_random_string')).not.toThrow(); + expect(() => getPerSecondRateUnit(undefined)).not.toThrow(); + }); +}); diff --git a/public/app/features/trails/autoQuery/units.ts b/public/app/features/trails/autoQuery/units.ts new file mode 100644 index 00000000000..ea9b819b4a7 --- /dev/null +++ b/public/app/features/trails/autoQuery/units.ts @@ -0,0 +1,35 @@ +export const DEFAULT_UNIT = 'short'; +export const DEFAULT_RATE_UNIT = 'cps'; // Count per second + +const UNIT_MAP: Record = { bytes: 'bytes', seconds: 's' }; +const UNIT_LIST = Object.keys(UNIT_MAP); +const RATE_UNIT_MAP: Record = { + bytes: 'Bps', // bytes per second + // seconds per second is unitless + // this may indicate a count of some resource that is active + seconds: 'short', +}; + +// Get unit from metric name (e.g. "go_gc_duration_seconds" -> "seconds") +export function getUnitFromMetric(metric: string) { + if (!metric) { + return null; + } + + const metricParts = metric.toLowerCase().split('_').slice(-2); // Get last two parts + for (let i = metricParts.length - 1; i >= 0; i--) { + if (UNIT_LIST.includes(metricParts[i])) { + return metricParts[i]; + } + } + return null; +} + +// Get Grafana unit for a panel (e.g. "go_gc_duration_seconds" -> "s") +export function getUnit(metricPart: string | undefined) { + return (metricPart && UNIT_MAP[metricPart]) || DEFAULT_UNIT; +} + +export function getPerSecondRateUnit(metricPart: string | undefined) { + return (metricPart && RATE_UNIT_MAP[metricPart]) || DEFAULT_RATE_UNIT; +} diff --git a/public/app/features/trails/shared.ts b/public/app/features/trails/shared.ts index 69af2ab8930..aab8bc4c736 100644 --- a/public/app/features/trails/shared.ts +++ b/public/app/features/trails/shared.ts @@ -15,7 +15,7 @@ export const TRAILS_ROUTE = '/explore/metrics/trail'; export const HOME_ROUTE = '/explore/metrics'; export const VAR_FILTERS = 'filters'; -export const VAR_FILTERS_EXPR = '{${filters}}'; +export const VAR_FILTERS_EXPR = '${filters}'; export const VAR_METRIC = 'metric'; export const VAR_METRIC_EXPR = '${metric}'; export const VAR_GROUP_BY = 'groupby';