mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore Metrics: Refactor auto query generators (#97815)
* move graph builders in one file * rename the folder as auto-query * have a components folder * more refactoring * rename * remove getQueryGeneratorFor * why not renaming again * add tests for baseQuery * remove the need of default query generator * refactor getAutoQueriesForMetric * split the logic * better structure * better structure 2 * clean up * summary tests * unite tests for default.ts * update unit tests * update unit tests * cleaning * one variable for filters
This commit is contained in:
parent
3266d62d6d
commit
3345f860dc
@ -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';
|
||||
|
@ -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);
|
||||
}
|
@ -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,
|
||||
});
|
||||
}
|
@ -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 });
|
||||
}
|
@ -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);
|
||||
}
|
@ -1,4 +0,0 @@
|
||||
export type CommonVizParams = {
|
||||
title: string;
|
||||
unit: string;
|
||||
};
|
@ -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}`;
|
||||
}
|
@ -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));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -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<string, string> = {
|
||||
count: 'sum',
|
||||
total: 'sum',
|
||||
};
|
||||
|
||||
function shouldCheckPreviousSuffixForUnit(suffix: string) {
|
||||
return suffix === 'total';
|
||||
}
|
||||
|
||||
const aggLabels: Record<string, string> = {
|
||||
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);
|
||||
}
|
@ -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;
|
||||
}
|
@ -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,
|
||||
};
|
||||
}
|
@ -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,
|
||||
});
|
||||
}
|
@ -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);
|
||||
});
|
||||
});
|
@ -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<string, string> = {
|
||||
bytes: 'bytes',
|
||||
seconds: 's',
|
||||
};
|
||||
|
||||
const UNIT_LIST = ['bytes', 'seconds'];
|
||||
|
||||
const RATE_UNIT_MAP: Record<string, string> = {
|
||||
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;
|
||||
}
|
@ -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';
|
||||
|
@ -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;
|
||||
|
@ -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,
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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;
|
@ -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;
|
@ -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})`
|
||||
);
|
||||
});
|
||||
});
|
@ -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);
|
||||
}
|
41
public/app/features/trails/autoQuery/graphBuilders.ts
Normal file
41
public/app/features/trails/autoQuery/graphBuilders.ts
Normal file
@ -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 });
|
||||
}
|
@ -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})');
|
||||
});
|
||||
});
|
@ -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}`;
|
||||
}
|
@ -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([]);
|
||||
});
|
||||
});
|
@ -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',
|
@ -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');
|
||||
});
|
||||
});
|
@ -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<string, string> = {
|
||||
count: 'sum',
|
||||
total: 'sum',
|
||||
};
|
||||
const aggLabels: Record<string, string> = {
|
||||
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,
|
||||
});
|
||||
}
|
@ -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');
|
||||
});
|
||||
});
|
@ -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,
|
||||
};
|
||||
}
|
@ -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');
|
||||
});
|
||||
});
|
@ -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}`;
|
||||
}
|
@ -17,3 +17,11 @@ export interface AutoQueryInfo {
|
||||
}
|
||||
|
||||
export type VizBuilder = () => VizPanelBuilder<{}, {}>;
|
||||
|
||||
export type AutoQueryContext = {
|
||||
metricParts: string[];
|
||||
isUtf8Metric: boolean;
|
||||
unit: string;
|
||||
suffix: string;
|
||||
unitSuffix?: string;
|
||||
};
|
94
public/app/features/trails/autoQuery/units.test.ts
Normal file
94
public/app/features/trails/autoQuery/units.test.ts
Normal file
@ -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();
|
||||
});
|
||||
});
|
35
public/app/features/trails/autoQuery/units.ts
Normal file
35
public/app/features/trails/autoQuery/units.ts
Normal file
@ -0,0 +1,35 @@
|
||||
export const DEFAULT_UNIT = 'short';
|
||||
export const DEFAULT_RATE_UNIT = 'cps'; // Count per second
|
||||
|
||||
const UNIT_MAP: Record<string, string> = { bytes: 'bytes', seconds: 's' };
|
||||
const UNIT_LIST = Object.keys(UNIT_MAP);
|
||||
const RATE_UNIT_MAP: Record<string, string> = {
|
||||
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;
|
||||
}
|
@ -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';
|
||||
|
Loading…
Reference in New Issue
Block a user