From 890d6a960f47a53d246e27c3bd95cb251990b3b1 Mon Sep 17 00:00:00 2001 From: Darren Janeczek <38694490+darrenjaneczek@users.noreply.github.com> Date: Mon, 8 Jan 2024 09:48:08 -0500 Subject: [PATCH] refactor: data-trails auto query logic (#79435) * refactor: data-trails auto query logic for most currently identified metric suffixes (excluding `_bucket`) --- .../AutoQueryEngine.test.ts | 170 ++++++++++++++++ .../AutomaticMetricQueries/AutoQueryEngine.ts | 182 +----------------- .../AutomaticMetricQueries/AutoVizPanel.tsx | 2 +- .../graph-builders/heatmap.ts | 18 ++ .../graph-builders/percentiles.ts | 7 + .../graph-builders/simple.ts | 11 ++ .../query-generators/bucket/index.ts | 86 +++++++++ .../query-generators/common/index.ts | 9 + .../query-generators/common/queries.test.ts | 62 ++++++ .../query-generators/common/queries.ts | 48 +++++ .../query-generators/common/rules.ts | 39 ++++ .../query-generators/common/types.ts | 5 + .../query-generators/index.ts | 14 ++ .../query-generators/types.ts | 3 + .../trails/AutomaticMetricQueries/types.ts | 17 ++ .../trails/AutomaticMetricQueries/units.ts | 21 ++ public/app/features/trails/BreakdownScene.tsx | 3 +- 17 files changed, 523 insertions(+), 174 deletions(-) create mode 100644 public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/graph-builders/heatmap.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/bucket/index.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/common/index.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.test.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/common/rules.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/common/types.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/index.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/query-generators/types.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/types.ts create mode 100644 public/app/features/trails/AutomaticMetricQueries/units.ts diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts b/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts new file mode 100644 index 00000000000..ec0a5600516 --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.test.ts @@ -0,0 +1,170 @@ +import { getAutoQueriesForMetric } from './AutoQueryEngine'; + +function expandExpr(shortenedExpr: string) { + return shortenedExpr.replace('...', '${metric}{${filters}}'); +} + +describe('getAutoQueriesForMetric', () => { + describe('Consider result.main query (only first)', () => { + it.each([ + // no rate + ['my_metric_general', 'avg(...)', 'short', 1], + ['my_metric_bytes', 'avg(...)', 'bytes', 1], + ['my_metric_seconds', 'avg(...)', 's', 1], + // rate with counts per second + ['my_metric_count', 'sum(rate(...[$__rate_interval]))', 'cps', 1], // cps = counts per second + ['my_metric_total', 'sum(rate(...[$__rate_interval]))', 'cps', 1], + ['my_metric_seconds_count', 'sum(rate(...[$__rate_interval]))', 'cps', 1], + // rate with seconds per second + ['my_metric_seconds_total', 'sum(rate(...[$__rate_interval]))', 'short', 1], // s/s + ['my_metric_seconds_sum', 'avg(rate(...[$__rate_interval]))', 'short', 1], + // rate with bytes per second + ['my_metric_bytes_total', 'sum(rate(...[$__rate_interval]))', 'Bps', 1], // bytes/s + ['my_metric_bytes_sum', 'avg(rate(...[$__rate_interval]))', 'Bps', 1], + // Bucket + ['my_metric_bucket', 'histogram_quantile(0.99, sum by(le) (rate(...[$__rate_interval])))', 'short', 3], + ['my_metric_seconds_bucket', 'histogram_quantile(0.99, sum by(le) (rate(...[$__rate_interval])))', 's', 3], + ])('Given metric %p expect %p with unit %p', (metric, expr, unit, queryCount) => { + const result = getAutoQueriesForMetric(metric); + + const queryDef = result.main; + + const expected = { expr: expandExpr(expr), unit, queryCount }; + const actual = { expr: queryDef.queries[0].expr, unit: queryDef.unit, queryCount: queryDef.queries.length }; + + expect(actual).toStrictEqual(expected); + }); + }); + + describe('Consider result.preview query (only first)', () => { + it.each([ + // no rate + ['my_metric_general', 'avg(...)', 'short'], + ['my_metric_bytes', 'avg(...)', 'bytes'], + ['my_metric_seconds', 'avg(...)', 's'], + // rate with counts per second + ['my_metric_count', 'sum(rate(...[$__rate_interval]))', 'cps'], // cps = counts per second + ['my_metric_total', 'sum(rate(...[$__rate_interval]))', 'cps'], + ['my_metric_seconds_count', 'sum(rate(...[$__rate_interval]))', 'cps'], + // rate with seconds per second + ['my_metric_seconds_total', 'sum(rate(...[$__rate_interval]))', 'short'], // s/s + ['my_metric_seconds_sum', 'avg(rate(...[$__rate_interval]))', 'short'], + // rate with bytes per second + ['my_metric_bytes_total', 'sum(rate(...[$__rate_interval]))', 'Bps'], // bytes/s + ['my_metric_bytes_sum', 'avg(rate(...[$__rate_interval]))', 'Bps'], + // Bucket + ['my_metric_bucket', 'histogram_quantile(0.50, sum by(le) (rate(...[$__rate_interval])))', 'short'], + ['my_metric_seconds_bucket', 'histogram_quantile(0.50, sum by(le) (rate(...[$__rate_interval])))', 's'], + ])('Given metric %p expect %p with unit %p', (metric, expr, unit) => { + const result = getAutoQueriesForMetric(metric); + + const queryDef = result.preview; + + const queryCount = 1; + + const expected = { expr: expandExpr(expr), unit, queryCount }; + const actual = { expr: queryDef.queries[0].expr, unit: queryDef.unit, queryCount: queryDef.queries.length }; + + expect(actual).toStrictEqual(expected); + }); + }); + + describe('Consider result.breakdown query (only first)', () => { + it.each([ + // no rate + ['my_metric_general', 'avg(...) by(${groupby})', 'short'], + ['my_metric_bytes', 'avg(...) by(${groupby})', 'bytes'], + ['my_metric_seconds', 'avg(...) by(${groupby})', 's'], + // rate with counts per second + ['my_metric_count', 'sum(rate(...[$__rate_interval])) by(${groupby})', 'cps'], // cps = counts per second + ['my_metric_total', 'sum(rate(...[$__rate_interval])) by(${groupby})', 'cps'], + ['my_metric_seconds_count', 'sum(rate(...[$__rate_interval])) by(${groupby})', 'cps'], + // rate with seconds per second + ['my_metric_seconds_total', 'sum(rate(...[$__rate_interval])) by(${groupby})', 'short'], // s/s + ['my_metric_seconds_sum', 'avg(rate(...[$__rate_interval])) by(${groupby})', 'short'], + // rate with bytes per second + ['my_metric_bytes_total', 'sum(rate(...[$__rate_interval])) by(${groupby})', 'Bps'], // bytes/s + ['my_metric_bytes_sum', 'avg(rate(...[$__rate_interval])) by(${groupby})', 'Bps'], + // Bucket + ['my_metric_bucket', 'histogram_quantile(0.50, sum by(le, ${groupby}) (rate(...[$__rate_interval])))', 'short'], + [ + 'my_metric_seconds_bucket', + 'histogram_quantile(0.50, sum by(le, ${groupby}) (rate(...[$__rate_interval])))', + 's', + ], + ])('Given metric %p expect %p with unit %p', (metric, expr, unit) => { + const result = getAutoQueriesForMetric(metric); + + const queryDef = result.breakdown; + + const queryCount = 1; + + const expected = { expr: expandExpr(expr), unit, queryCount }; + const actual = { expr: queryDef.queries[0].expr, unit: queryDef.unit, queryCount: queryDef.queries.length }; + + expect(actual).toStrictEqual(expected); + }); + }); + + describe('Consider result.variant', () => { + it.each([ + // No variants + ['my_metric_count', []], + ['my_metric_seconds_count', []], + ['my_metric_bytes', []], + ['my_metric_seconds', []], + ['my_metric_general', []], + ['my_metric_seconds_total', []], + ['my_metric_seconds_sum', []], + // Bucket variants + [ + 'my_metric_bucket', + [ + { + variant: 'percentiles', + unit: 'short', + exprs: [ + 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))', + 'histogram_quantile(0.90, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))', + 'histogram_quantile(0.50, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))', + ], + }, + { + variant: 'heatmap', + unit: 'short', + exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval]))'], + }, + ], + ], + [ + 'my_metric_seconds_bucket', + [ + { + variant: 'percentiles', + unit: 's', + exprs: [ + 'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))', + 'histogram_quantile(0.90, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))', + 'histogram_quantile(0.50, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))', + ], + }, + { + variant: 'heatmap', + unit: 's', + exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval]))'], + }, + ], + ], + ])('Given metric %p should generate expected variants', (metric, expectedVariants) => { + const defs = getAutoQueriesForMetric(metric); + + const received = defs.variants.map((variant) => ({ + variant: variant.variant, + unit: variant.unit, + exprs: variant.queries.map((query) => query.expr), + })); + + expect(received).toStrictEqual(expectedVariants); + }); + }); +}); diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts b/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts index fc192b6803b..822c388c925 100644 --- a/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts +++ b/public/app/features/trails/AutomaticMetricQueries/AutoQueryEngine.ts @@ -1,178 +1,16 @@ -import { PanelBuilders, VizPanelBuilder } from '@grafana/scenes'; -import { PromQuery } from 'app/plugins/datasource/prometheus/types'; -import { HeatmapColorMode } from 'app/plugins/panel/heatmap/types'; - -import { VAR_FILTERS_EXPR, VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../shared'; - -export interface AutoQueryDef { - variant: string; - title: string; - unit: string; - queries: PromQuery[]; - vizBuilder: (def: AutoQueryDef) => VizPanelBuilder<{}, {}>; -} - -export interface AutoQueryInfo { - preview: AutoQueryDef; - main: AutoQueryDef; - variants: AutoQueryDef[]; - breakdown: AutoQueryDef; -} +import { getQueryGeneratorFor } from './query-generators'; +import { AutoQueryInfo } from './types'; export function getAutoQueriesForMetric(metric: string): AutoQueryInfo { - let unit = 'short'; - let agg = 'avg'; - let rate = false; - let title = metric; + const metricParts = metric.split('_'); - if (metric.endsWith('seconds_sum')) { - unit = 's'; - agg = 'avg'; - rate = true; - } else if (metric.endsWith('seconds')) { - unit = 's'; - agg = 'avg'; - rate = false; - } else if (metric.endsWith('bytes')) { - unit = 'bytes'; - agg = 'avg'; - rate = false; - } else if (metric.endsWith('seconds_count') || metric.endsWith('seconds_total')) { - agg = 'sum'; - rate = true; - } else if (metric.endsWith('bucket')) { - return getQueriesForBucketMetric(metric); - } else if (metric.endsWith('count') || metric.endsWith('total')) { - agg = 'sum'; - rate = true; + 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}"`); } - let query = `${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}`; - if (rate) { - query = `rate(${query}[$__rate_interval])`; - } - - const main: AutoQueryDef = { - title: `${title}`, - variant: 'graph', - unit, - queries: [{ refId: 'A', expr: `${agg}(${query})` }], - vizBuilder: simpleGraphBuilder, - }; - - const breakdown: AutoQueryDef = { - title: `${title}`, - variant: 'graph', - unit, - queries: [ - { - refId: 'A', - expr: `${agg}(${query}) by(${VAR_GROUP_BY_EXP})`, - legendFormat: `{{${VAR_GROUP_BY_EXP}}}`, - }, - ], - vizBuilder: simpleGraphBuilder, - }; - - return { preview: main, main: main, breakdown: breakdown, variants: [] }; -} - -function getQueriesForBucketMetric(metric: string): AutoQueryInfo { - let unit = 'short'; - - if (metric.endsWith('seconds_bucket')) { - unit = 's'; - } - - const p50: AutoQueryDef = { - title: metric, - variant: 'p50', - unit, - queries: [ - { - refId: 'A', - expr: `histogram_quantile(0.50, sum by(le) (rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])))`, - }, - ], - vizBuilder: simpleGraphBuilder, - }; - - const breakdown: AutoQueryDef = { - title: metric, - variant: 'p50', - unit, - queries: [ - { - refId: 'A', - expr: `histogram_quantile(0.50, sum by(le, ${VAR_GROUP_BY_EXP}) (rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])))`, - }, - ], - vizBuilder: simpleGraphBuilder, - }; - - const percentiles: AutoQueryDef = { - title: metric, - variant: 'percentiles', - unit, - queries: [ - { - refId: 'A', - expr: `histogram_quantile(0.99, sum by(le) (rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])))`, - legendFormat: '99th Percentile', - }, - { - refId: 'B', - expr: `histogram_quantile(0.90, sum by(le) (rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])))`, - legendFormat: '90th Percentile', - }, - { - refId: 'C', - expr: `histogram_quantile(0.50, sum by(le) (rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])))`, - legendFormat: '50th Percentile', - }, - ], - vizBuilder: percentilesGraphBuilder, - }; - - const heatmap: AutoQueryDef = { - title: metric, - variant: 'heatmap', - unit, - queries: [ - { - refId: 'A', - expr: `sum by(le) (rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval]))`, - format: 'heatmap', - }, - ], - vizBuilder: heatmapGraphBuilder, - }; - - return { preview: p50, main: percentiles, variants: [percentiles, heatmap], breakdown: breakdown }; -} - -function simpleGraphBuilder(def: AutoQueryDef) { - return PanelBuilders.timeseries() - .setTitle(def.title) - .setUnit(def.unit) - .setOption('legend', { showLegend: false }) - .setCustomFieldConfig('fillOpacity', 9); -} - -function percentilesGraphBuilder(def: AutoQueryDef) { - return PanelBuilders.timeseries().setTitle(def.title).setUnit(def.unit).setCustomFieldConfig('fillOpacity', 9); -} - -function heatmapGraphBuilder(def: AutoQueryDef) { - return PanelBuilders.heatmap() - .setTitle(def.title) - .setUnit(def.unit) - .setOption('calculate', false) - .setOption('color', { - mode: HeatmapColorMode.Scheme, - exponent: 0.5, - scheme: 'Spectral', - steps: 32, - reverse: false, - }); + return generator(metricParts); } diff --git a/public/app/features/trails/AutomaticMetricQueries/AutoVizPanel.tsx b/public/app/features/trails/AutomaticMetricQueries/AutoVizPanel.tsx index d2cc5a682d7..be432a3de0d 100644 --- a/public/app/features/trails/AutomaticMetricQueries/AutoVizPanel.tsx +++ b/public/app/features/trails/AutomaticMetricQueries/AutoVizPanel.tsx @@ -8,7 +8,7 @@ import { Field, RadioButtonGroup, useStyles2, Stack } from '@grafana/ui'; import { trailDS } from '../shared'; import { getTrailSettings } from '../utils'; -import { AutoQueryDef, AutoQueryInfo } from './AutoQueryEngine'; +import { AutoQueryInfo, AutoQueryDef } from './types'; export interface AutoVizPanelState extends SceneObjectState { panel?: VizPanel; diff --git a/public/app/features/trails/AutomaticMetricQueries/graph-builders/heatmap.ts b/public/app/features/trails/AutomaticMetricQueries/graph-builders/heatmap.ts new file mode 100644 index 00000000000..db561d78f96 --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/graph-builders/heatmap.ts @@ -0,0 +1,18 @@ +import { PanelBuilders } from '@grafana/scenes'; +import { HeatmapColorMode } from 'app/plugins/panel/heatmap/types'; + +import { AutoQueryDef } from '../types'; + +export function heatmapGraphBuilder(def: AutoQueryDef) { + return PanelBuilders.heatmap() + .setTitle(def.title) + .setUnit(def.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 new file mode 100644 index 00000000000..d85f7e23da5 --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/graph-builders/percentiles.ts @@ -0,0 +1,7 @@ +import { PanelBuilders } from '@grafana/scenes'; + +import { AutoQueryDef } from '../types'; + +export function percentilesGraphBuilder(def: AutoQueryDef) { + return PanelBuilders.timeseries().setTitle(def.title).setUnit(def.unit).setCustomFieldConfig('fillOpacity', 9); +} diff --git a/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts b/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts new file mode 100644 index 00000000000..538281830d7 --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/graph-builders/simple.ts @@ -0,0 +1,11 @@ +import { PanelBuilders } from '@grafana/scenes'; + +import { AutoQueryDef } from '../types'; + +export function simpleGraphBuilder(def: AutoQueryDef) { + return PanelBuilders.timeseries() + .setTitle(def.title) + .setUnit(def.unit) + .setOption('legend', { showLegend: false }) + .setCustomFieldConfig('fillOpacity', 9); +} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/bucket/index.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/bucket/index.ts new file mode 100644 index 00000000000..5e2f9d3ea00 --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/bucket/index.ts @@ -0,0 +1,86 @@ +import { VAR_FILTERS_EXPR, VAR_GROUP_BY_EXP, VAR_METRIC_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'; + +function generator(metricParts: string[]) { + let unit = 'short'; + + const title = `${VAR_METRIC_EXPR}`; + + const unitSuffix = metricParts.at(-2); + + if (unitSuffix === 'seconds') { + // TODO Map to other units + unit = 's'; + } + + const p50: AutoQueryDef = { + title, + variant: 'p50', + unit, + queries: [ + { + refId: 'A', + expr: `histogram_quantile(0.50, sum by(le) (rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])))`, + }, + ], + vizBuilder: simpleGraphBuilder, + }; + + const breakdown: AutoQueryDef = { + title, + variant: 'p50', + unit, + queries: [ + { + refId: 'A', + expr: `histogram_quantile(0.50, sum by(le, ${VAR_GROUP_BY_EXP}) (rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])))`, + }, + ], + vizBuilder: simpleGraphBuilder, + }; + + const percentiles: AutoQueryDef = { + title, + variant: 'percentiles', + unit, + queries: [ + { + refId: 'A', + expr: `histogram_quantile(0.99, sum by(le) (rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])))`, + legendFormat: '99th Percentile', + }, + { + refId: 'B', + expr: `histogram_quantile(0.90, sum by(le) (rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])))`, + legendFormat: '90th Percentile', + }, + { + refId: 'C', + expr: `histogram_quantile(0.50, sum by(le) (rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])))`, + legendFormat: '50th Percentile', + }, + ], + vizBuilder: percentilesGraphBuilder, + }; + + const heatmap: AutoQueryDef = { + title, + variant: 'heatmap', + unit, + queries: [ + { + refId: 'A', + expr: `sum by(le) (rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval]))`, + format: 'heatmap', + }, + ], + vizBuilder: heatmapGraphBuilder, + }; + + return { preview: p50, main: percentiles, variants: [percentiles, heatmap], breakdown: breakdown }; +} + +export default { generator }; diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/index.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/index.ts new file mode 100644 index 00000000000..4257e212ec7 --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/index.ts @@ -0,0 +1,9 @@ +import { generateQueries } from './queries'; +import { getGeneratorParameters } from './rules'; + +function generator(metricParts: string[]) { + const params = getGeneratorParameters(metricParts); + return generateQueries(params); +} + +export default { generator }; diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.test.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.test.ts new file mode 100644 index 00000000000..76710728093 --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.test.ts @@ -0,0 +1,62 @@ +import { VAR_GROUP_BY_EXP } from '../../../shared'; +import { AutoQueryDef, AutoQueryInfo } from '../../types'; + +import { generateQueries, getGeneralBaseQuery } from './queries'; + +describe('generateQueries', () => { + const agg = 'mockagg'; + 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) { + 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); + + describe(`since rate is ${rate}`, () => { + test(`base query must be "${expectedBaseQuery}"`, () => expect(detectedBaseQuery).toBe(expectedBaseQuery)); + }); + } + + 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)); + continue; + } + + queryInfo[key].forEach((queryDef, index) => { + describe(`queryInfo.${key}[${index}]`, () => testRateIndependentAssertions(queryDef, key)); + describe(`queryInfo.${key}[${index}]`, () => testRateSpecificAssertions(queryDef, rate)); + }); + } + }); + } +}); diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.ts new file mode 100644 index 00000000000..6979358e58a --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/queries.ts @@ -0,0 +1,48 @@ +import { VAR_FILTERS_EXPR, VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../../shared'; +import { simpleGraphBuilder } from '../../graph-builders/simple'; +import { AutoQueryDef, AutoQueryInfo } from '../../types'; + +import { AutoQueryParameters } from './types'; + +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 : GENERAL_BASE_QUERY; +} + +export function generateQueries({ agg, rate, unit }: AutoQueryParameters): AutoQueryInfo { + const baseQuery = getGeneralBaseQuery(rate); + + const main = createMainQuery(baseQuery, agg, unit); + + const breakdown = createBreakdownQuery(baseQuery, agg, unit); + + return { preview: main, main: main, breakdown: breakdown, variants: [] }; +} + +function createMainQuery(baseQuery: string, agg: string, unit: string): AutoQueryDef { + return { + title: `${VAR_METRIC_EXPR}`, + variant: 'graph', + unit, + queries: [{ refId: 'A', expr: `${agg}(${baseQuery})` }], + vizBuilder: simpleGraphBuilder, + }; +} + +function createBreakdownQuery(baseQuery: string, agg: string, unit: string): AutoQueryDef { + return { + title: `${VAR_METRIC_EXPR}`, + variant: 'graph', + unit, + queries: [ + { + refId: 'A', + expr: `${agg}(${baseQuery}) by(${VAR_GROUP_BY_EXP})`, + legendFormat: `{{${VAR_GROUP_BY_EXP}}}`, + }, + ], + vizBuilder: simpleGraphBuilder, + }; +} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/rules.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/rules.ts new file mode 100644 index 00000000000..01a42bdaaaa --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/rules.ts @@ -0,0 +1,39 @@ +import { getUnit, getPerSecondRateUnit } from '../../units'; + +import { AutoQueryParameters } from './types'; + +/** These suffixes will set rate to true */ +const RATE_SUFFIXES = new Set(['count', 'total', 'sum']); + +/** Non-default aggregattion keyed by suffix */ +const SPECIFIC_AGGREGATIONS_FOR_SUFFIX: Record = { + count: 'sum', + total: 'sum', + sum: 'avg', +}; + +function checkPreviousForUnit(suffix: string) { + return suffix === 'total' || suffix === 'sum'; +} + +export function getGeneratorParameters(metricParts: string[]): AutoQueryParameters { + const suffix = metricParts.at(-1); + + if (suffix == null) { + throw new Error('Invalid metric parameter'); + } + + const rate = RATE_SUFFIXES.has(suffix); + + const unitSuffix = checkPreviousForUnit(suffix) ? metricParts.at(-2) : suffix; + + const unit = rate ? getPerSecondRateUnit(unitSuffix) : getUnit(unitSuffix); + + const agg = SPECIFIC_AGGREGATIONS_FOR_SUFFIX[suffix] || 'avg'; + + return { + agg, + unit, + rate, + }; +} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/common/types.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/types.ts new file mode 100644 index 00000000000..cf49ce8e526 --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/common/types.ts @@ -0,0 +1,5 @@ +export type AutoQueryParameters = { + agg: string; + unit: string; + rate: boolean; +}; diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/index.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/index.ts new file mode 100644 index 00000000000..938f5a42613 --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/index.ts @@ -0,0 +1,14 @@ +import bucket from './bucket'; +import general from './common'; +import { MetricQueriesGenerator } from './types'; + +const SUFFIX_TO_ALTERNATIVE_GENERATOR: Record = { + bucket: bucket.generator, +}; + +export function getQueryGeneratorFor(suffix?: string) { + if (!suffix || suffix === '') { + return null; + } + return SUFFIX_TO_ALTERNATIVE_GENERATOR[suffix] || general.generator; +} diff --git a/public/app/features/trails/AutomaticMetricQueries/query-generators/types.ts b/public/app/features/trails/AutomaticMetricQueries/query-generators/types.ts new file mode 100644 index 00000000000..ddf30ccc27c --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/query-generators/types.ts @@ -0,0 +1,3 @@ +import { AutoQueryInfo } from '../types'; + +export type MetricQueriesGenerator = (metricParts: string[]) => AutoQueryInfo; diff --git a/public/app/features/trails/AutomaticMetricQueries/types.ts b/public/app/features/trails/AutomaticMetricQueries/types.ts new file mode 100644 index 00000000000..1ddd6babf3a --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/types.ts @@ -0,0 +1,17 @@ +import { VizPanelBuilder } from '@grafana/scenes'; +import { PromQuery } from 'app/plugins/datasource/prometheus/types'; + +export interface AutoQueryDef { + variant: string; + title: string; + unit: string; + queries: PromQuery[]; + vizBuilder: (def: AutoQueryDef) => VizPanelBuilder<{}, {}>; +} + +export interface AutoQueryInfo { + preview: AutoQueryDef; + main: AutoQueryDef; + variants: AutoQueryDef[]; + breakdown: AutoQueryDef; +} diff --git a/public/app/features/trails/AutomaticMetricQueries/units.ts b/public/app/features/trails/AutomaticMetricQueries/units.ts new file mode 100644 index 00000000000..5a150809b1e --- /dev/null +++ b/public/app/features/trails/AutomaticMetricQueries/units.ts @@ -0,0 +1,21 @@ +const DEFAULT_UNIT = 'short'; + +export function getUnit(metricPart: string | undefined) { + return (metricPart && UNIT_MAP[metricPart]) || DEFAULT_UNIT; +} + +const UNIT_MAP: Record = { + bytes: 'bytes', + seconds: 's', +}; + +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/BreakdownScene.tsx b/public/app/features/trails/BreakdownScene.tsx index 7e63de042c7..e202db66427 100644 --- a/public/app/features/trails/BreakdownScene.tsx +++ b/public/app/features/trails/BreakdownScene.tsx @@ -24,7 +24,8 @@ import { Button, Field, RadioButtonGroup, useStyles2 } from '@grafana/ui'; import { ALL_VARIABLE_VALUE } from 'app/features/variables/constants'; import { AddToFiltersGraphAction } from './AddToFiltersGraphAction'; -import { AutoQueryDef, getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine'; +import { getAutoQueriesForMetric } from './AutomaticMetricQueries/AutoQueryEngine'; +import { AutoQueryDef } from './AutomaticMetricQueries/types'; import { ByFrameRepeater } from './ByFrameRepeater'; import { LayoutSwitcher } from './LayoutSwitcher'; import { MetricScene } from './MetricScene';