Prometheus: QueryEditor fix error when switching from code to builder for undefined aggregation operations (#110179)

* convert stddev to function instead of aggregation

* adds missing stdvar, limitK and limit_ratio agg functions

* add tests
This commit is contained in:
jcolladokuri
2025-08-27 12:04:13 -07:00
committed by GitHub
parent be4dc6fdb6
commit 541e378891
5 changed files with 136 additions and 1 deletions
@@ -0,0 +1,55 @@
import { getAggregationOperations } from './aggregations';
import { QueryBuilderOperation } from './shared/types';
import { PromOperationId } from './types';
describe('getAggregationOperations', () => {
it('returns a list containing all aggregation IDs', () => {
const ops = getAggregationOperations();
const ids = ops.map((o) => o.id);
expect(ids).toContain(PromOperationId.Sum);
expect(ids).toContain(PromOperationId.Avg);
expect(ids).toContain(PromOperationId.Min);
expect(ids).toContain(PromOperationId.Max);
expect(ids).toContain(PromOperationId.Count);
expect(ids).toContain(PromOperationId.Group);
expect(ids).toContain(PromOperationId.Stddev);
expect(ids).toContain(PromOperationId.Stdvar);
// aggregations with params
expect(ids).toContain(PromOperationId.TopK);
expect(ids).toContain(PromOperationId.BottomK);
expect(ids).toContain(PromOperationId.CountValues);
expect(ids).toContain(PromOperationId.Quantile);
expect(ids).toContain(PromOperationId.LimitK);
expect(ids).toContain(PromOperationId.LimitRatio);
// over-time range aggregations
expect(ids).toContain(PromOperationId.SumOverTime);
expect(ids).toContain(PromOperationId.AvgOverTime);
expect(ids).toContain(PromOperationId.MinOverTime);
expect(ids).toContain(PromOperationId.MaxOverTime);
expect(ids).toContain(PromOperationId.CountOverTime);
expect(ids).toContain(PromOperationId.LastOverTime);
expect(ids).toContain(PromOperationId.PresentOverTime);
expect(ids).toContain(PromOperationId.AbsentOverTime);
expect(ids).toContain(PromOperationId.StddevOverTime);
});
it('includes over-time range functions with correct renderer behavior', () => {
const ops = getAggregationOperations();
const sumOverTime = ops.find((o) => o.id === PromOperationId.SumOverTime)!;
expect(sumOverTime).toBeDefined();
expect(sumOverTime.params.length).toBeGreaterThan(0);
// call renderer with default param
const model: QueryBuilderOperation = { id: sumOverTime.id, params: sumOverTime.defaultParams };
const rendered = sumOverTime.renderer(model, sumOverTime, 'metric');
expect(rendered).toBe(`${PromOperationId.SumOverTime}(metric[$__interval])`);
// call renderer with custom param
const modelCustom: QueryBuilderOperation = { id: sumOverTime.id, params: ['5m'] };
const renderedCustom = sumOverTime.renderer(modelCustom, sumOverTime, 'metric');
expect(renderedCustom).toBe(`${PromOperationId.SumOverTime}(metric[5m])`);
});
});
@@ -17,6 +17,8 @@ export function getAggregationOperations(): QueryBuilderOperationDef[] {
...createAggregationOperation(PromOperationId.Max),
...createAggregationOperation(PromOperationId.Count),
...createAggregationOperation(PromOperationId.Group),
...createAggregationOperation(PromOperationId.Stddev),
...createAggregationOperation(PromOperationId.Stdvar),
...createAggregationOperationWithParam(PromOperationId.TopK, {
params: [{ name: 'K-value', type: 'number' }],
defaultParams: [5],
@@ -33,6 +35,14 @@ export function getAggregationOperations(): QueryBuilderOperationDef[] {
params: [{ name: 'Value', type: 'number' }],
defaultParams: [1],
}),
...createAggregationOperationWithParam(PromOperationId.LimitK, {
params: [{ name: 'K-value', type: 'number' }],
defaultParams: [5],
}),
...createAggregationOperationWithParam(PromOperationId.LimitRatio, {
params: [{ name: 'Ratio', type: 'number' }],
defaultParams: [1],
}),
createAggregationOverTime(PromOperationId.SumOverTime),
createAggregationOverTime(PromOperationId.AvgOverTime),
createAggregationOverTime(PromOperationId.MinOverTime),
@@ -0,0 +1,68 @@
import { getOperationDefinitions, addOperationWithRangeVector } from './operations';
import { QueryBuilderOperation, VisualQueryModeller } from './shared/types';
import { PromOperationId, PromVisualQuery } from './types';
describe('getOperationDefinitions', () => {
it('returns a list containing operation IDs', () => {
const ops = getOperationDefinitions();
const ids = ops.map((o) => o.id);
expect(ids).toContain(PromOperationId.HistogramQuantile);
expect(ids).toContain(PromOperationId.Rate);
expect(ids).toContain(PromOperationId.PredictLinear);
expect(ids).toContain(PromOperationId.QuantileOverTime);
expect(ids).toContain(PromOperationId.LabelJoin);
expect(ids).toContain(PromOperationId.Vector);
});
it('range function renderer uses default and custom range vector params', () => {
const ops = getOperationDefinitions();
const rate = ops.find((o) => o.id === PromOperationId.Rate)!;
expect(rate).toBeDefined();
const modelDefault: QueryBuilderOperation = { id: rate.id, params: rate.defaultParams };
const renderedDefault = rate.renderer(modelDefault, rate, 'metric');
expect(renderedDefault).toBe(`${PromOperationId.Rate}(metric[$__rate_interval])`);
const modelCustom: QueryBuilderOperation = { id: rate.id, params: ['5m'] };
const renderedCustom = rate.renderer(modelCustom, rate, 'metric');
expect(renderedCustom).toBe(`${PromOperationId.Rate}(metric[5m])`);
});
it('changeTypeHandler updates operation params when rate/interval default changes', () => {
const ops = getOperationDefinitions();
const doubleExp = ops.find((o) => o.id === PromOperationId.DoubleExponentialSmoothing)!;
expect(doubleExp).toBeDefined();
// simulate an operation currently using $__rate_interval
const operation: QueryBuilderOperation = { id: doubleExp.id, params: ['$__rate_interval'] };
// create a new definition that uses $__interval instead
const newDef = { ...doubleExp, defaultParams: ['$__interval'] };
if (doubleExp.changeTypeHandler) {
const updated = doubleExp.changeTypeHandler(operation, newDef);
expect(updated.params).toEqual(newDef.defaultParams);
}
});
it('addOperationWithRangeVector inserts a range op at the front of the query', () => {
const ops = getOperationDefinitions();
const changes = ops.find((o) => o.id === PromOperationId.Changes)!;
const query: PromVisualQuery = { operations: [], metric: 'metric', labels: [] };
const modeller = {} as VisualQueryModeller;
const result = addOperationWithRangeVector(changes, query, modeller);
expect(result.operations[0].id).toBe(changes.id);
});
it('vector renderer renders parameter as value', () => {
const ops = getOperationDefinitions();
const vector = ops.find((o) => o.id === PromOperationId.Vector)!;
expect(vector).toBeDefined();
const model: QueryBuilderOperation = { id: vector.id, params: [42] };
const rendered = vector.renderer(model, vector, '');
expect(rendered).toBe(`${PromOperationId.Vector}(42)`);
});
});
@@ -258,7 +258,6 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
createFunction({ id: PromOperationId.Sort }),
createFunction({ id: PromOperationId.SortDesc }),
createFunction({ id: PromOperationId.Sqrt }),
createFunction({ id: PromOperationId.Stddev }),
createFunction({
id: PromOperationId.Tan,
category: PromVisualQueryOperationCategory.Trigonometric,
@@ -100,6 +100,8 @@ export enum PromOperationId {
LabelReplace = 'label_replace',
Last = 'last',
LastOverTime = 'last_over_time',
LimitK = 'limitk',
LimitRatio = 'limit_ratio',
Ln = 'ln',
Log10 = 'log10',
Log2 = 'log2',
@@ -128,6 +130,7 @@ export enum PromOperationId {
Sqrt = 'sqrt',
Stddev = 'stddev',
StddevOverTime = 'stddev_over_time',
Stdvar = 'stdvar',
Sum = 'sum',
SumOverTime = 'sum_over_time',
Tan = 'tan',