Implement PromQL Functions in new query editor (#45431)

* adding functions for prometheus queries

* WIP: functions added. Just categorizing them and testing each function out

* WIP: testing functions

* WIP verifying other functions

* Functions added and classified

* added tests

* moved bottomk to aggregations module

* added tests to the PromQueryModeller

* code review comments. Made range renderer common with code

* removed duplicate functions

* updated comments
This commit is contained in:
Stephanie Closson 2022-02-17 12:39:41 -04:00 committed by GitHub
parent f42c830b3c
commit abe32b9521
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 407 additions and 26 deletions

View File

@ -278,4 +278,40 @@ describe('PromQueryModeller', () => {
})
).toBe('metric_a / on(le) metric_b');
});
it('Can render functions that require a range as a parameter', () => {
expect(
modeller.renderQuery({
metric: 'metric_a',
labels: [],
operations: [{ id: 'holt_winters', params: ['auto', 0.5, 0.5] }],
})
).toBe('holt_winters(metric_a[$__rate_interval], 0.5, 0.5)');
});
it('Can render functions that require parameters left of a range', () => {
expect(
modeller.renderQuery({
metric: 'metric_a',
labels: [],
operations: [{ id: 'quantile_over_time', params: ['auto', 1] }],
})
).toBe('quantile_over_time(1, metric_a[$__rate_interval])');
});
it('Can render the label_join function', () => {
expect(
modeller.renderQuery({
metric: 'metric_a',
labels: [],
operations: [{ id: 'label_join', params: ['label_1', ',', 'label_2'] }],
})
).toBe('label_join(metric_a, "label_1", ",", "label_2")');
});
it('Can render label_join with extra parameters', () => {
expect(
modeller.renderQuery({
metric: 'metric_a',
labels: [],
operations: [{ id: 'label_join', params: ['label_1', ', ', 'label_2', 'label_3', 'label_4', 'label_5'] }],
})
).toBe('label_join(metric_a, "label_1", ", ", "label_2", "label_3", "label_4", "label_5")');
});
});

View File

@ -22,6 +22,8 @@ export class PromQueryModeller extends LokiAndPromQueryModellerBase<PromVisualQu
PromVisualQueryOperationCategory.RangeFunctions,
PromVisualQueryOperationCategory.Functions,
PromVisualQueryOperationCategory.BinaryOps,
PromVisualQueryOperationCategory.Trigonometric,
PromVisualQueryOperationCategory.Time,
]);
}

View File

@ -17,6 +17,7 @@ export function getAggregationOperations(): QueryBuilderOperationDef[] {
...createAggregationOperation(PromOperationId.Max),
...createAggregationOperation(PromOperationId.Count),
...createAggregationOperation(PromOperationId.Topk),
...createAggregationOperation(PromOperationId.BottomK),
createAggregationOverTime(PromOperationId.SumOverTime),
createAggregationOverTime(PromOperationId.AvgOverTime),
createAggregationOverTime(PromOperationId.MinOverTime),
@ -25,7 +26,6 @@ export function getAggregationOperations(): QueryBuilderOperationDef[] {
createAggregationOverTime(PromOperationId.LastOverTime),
createAggregationOverTime(PromOperationId.PresentOverTime),
createAggregationOverTime(PromOperationId.StddevOverTime),
createAggregationOverTime(PromOperationId.StdvarOverTime),
];
}
@ -94,7 +94,7 @@ function createAggregationOperation(name: string): QueryBuilderOperationDef[] {
];
// Handle some special aggregations that have parameters
if (name === 'topk') {
if (name === 'topk' || name === 'bottomk') {
const param: QueryBuilderOperationParamDef = {
name: 'K-value',
type: 'number',

View File

@ -1,13 +1,17 @@
import { LabelParamEditor } from './components/LabelParamEditor';
import {
defaultAddOperationHandler,
functionRendererLeft,
functionRendererRight,
getPromAndLokiOperationDisplayName,
rangeRendererLeftWithParams,
rangeRendererRightWithParams,
} from './shared/operationUtils';
import {
QueryBuilderOperation,
QueryBuilderOperationDef,
QueryBuilderOperationParamDef,
QueryWithOperations,
VisualQueryModeller,
} from './shared/types';
import { PromOperationId, PromVisualQuery, PromVisualQueryOperationCategory } from './types';
@ -51,7 +55,7 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
createRangeFunction(PromOperationId.Irate),
createRangeFunction(PromOperationId.Increase),
createRangeFunction(PromOperationId.Delta),
// Not sure about this one. It could also be a more generic "Simple math operation" where user specifies
// Not sure about this one. It could also be a more generic 'Simple math operation' where user specifies
// both the operator and the operand in a single input
{
id: PromOperationId.MultiplyBy,
@ -80,12 +84,211 @@ export function getOperationDefinitions(): QueryBuilderOperationDef[] {
renderer: (model, def, innerExpr) => innerExpr,
addOperationHandler: addNestedQueryHandler,
},
createFunction({ id: PromOperationId.Absent }),
createRangeFunction(PromOperationId.AbsentOverTime),
createFunction({
id: PromOperationId.Acos,
category: PromVisualQueryOperationCategory.Trigonometric,
}),
createFunction({
id: PromOperationId.Acosh,
category: PromVisualQueryOperationCategory.Trigonometric,
}),
createFunction({
id: PromOperationId.Asin,
category: PromVisualQueryOperationCategory.Trigonometric,
}),
createFunction({
id: PromOperationId.Asinh,
category: PromVisualQueryOperationCategory.Trigonometric,
}),
createFunction({
id: PromOperationId.Atan,
category: PromVisualQueryOperationCategory.Trigonometric,
}),
createFunction({
id: PromOperationId.Atanh,
category: PromVisualQueryOperationCategory.Trigonometric,
}),
createFunction({ id: PromOperationId.Ceil }),
createFunction({
id: PromOperationId.Clamp,
name: 'Clamp',
params: [
{ name: 'Minimum Scalar', type: 'number' },
{ name: 'Maximum Scalar', type: 'number' },
],
defaultParams: [1, 1],
}),
createFunction({
id: PromOperationId.ClampMax,
params: [{ name: 'Maximum Scalar', type: 'number' }],
defaultParams: [1],
}),
createFunction({
id: PromOperationId.ClampMin,
params: [{ name: 'Minimum Scalar', type: 'number' }],
defaultParams: [1],
}),
createFunction({
id: PromOperationId.Cos,
category: PromVisualQueryOperationCategory.Trigonometric,
}),
createFunction({
id: PromOperationId.Cosh,
category: PromVisualQueryOperationCategory.Trigonometric,
}),
createFunction({
id: PromOperationId.CountValues,
params: [{ name: 'Identifier', type: 'string' }],
defaultParams: ['count'],
renderer: functionRendererLeft,
}),
createFunction({
id: PromOperationId.DayOfMonth,
category: PromVisualQueryOperationCategory.Time,
}),
createFunction({
id: PromOperationId.DayOfWeek,
category: PromVisualQueryOperationCategory.Time,
}),
createFunction({
id: PromOperationId.DaysInMonth,
category: PromVisualQueryOperationCategory.Time,
}),
createFunction({ id: PromOperationId.Deg }),
createRangeFunction(PromOperationId.Deriv),
//
createFunction({ id: PromOperationId.Exp }),
createFunction({ id: PromOperationId.Floor }),
createFunction({ id: PromOperationId.Group }),
createFunction({
id: PromOperationId.HoltWinters,
params: [
getRangeVectorParamDef(),
{ name: 'Smoothing Factor', type: 'number' },
{ name: 'Trend Factor', type: 'number' },
],
defaultParams: ['auto', 0.5, 0.5],
alternativesKey: 'range function',
category: PromVisualQueryOperationCategory.RangeFunctions,
renderer: rangeRendererRightWithParams,
}),
createFunction({ id: PromOperationId.Hour }),
createRangeFunction(PromOperationId.Idelta),
createFunction({
id: PromOperationId.LabelJoin,
params: [
{
name: 'Destination Label',
type: 'string',
editor: LabelParamEditor,
},
{
name: 'Separator',
type: 'string',
},
{
name: 'Source Label',
type: 'string',
restParam: true,
optional: true,
editor: LabelParamEditor,
},
],
defaultParams: ['', ',', ''],
renderer: labelJoinRenderer,
addOperationHandler: labelJoinAddOperationHandler,
}),
createFunction({ id: PromOperationId.Log10 }),
createFunction({ id: PromOperationId.Log2 }),
createFunction({ id: PromOperationId.Minute }),
createFunction({ id: PromOperationId.Month }),
createFunction({
id: PromOperationId.Pi,
renderer: (model) => `${model.id}()`,
}),
createFunction({
id: PromOperationId.PredictLinear,
params: [getRangeVectorParamDef(), { name: 'Seconds from now', type: 'number' }],
defaultParams: ['auto', 60],
alternativesKey: 'range function',
category: PromVisualQueryOperationCategory.RangeFunctions,
renderer: rangeRendererRightWithParams,
}),
createFunction({
id: PromOperationId.Quantile,
params: [{ name: 'Value', type: 'number' }],
defaultParams: [1],
renderer: functionRendererLeft,
}),
createFunction({
id: PromOperationId.QuantileOverTime,
params: [getRangeVectorParamDef(), { name: 'Quantile', type: 'number' }],
defaultParams: ['auto', 0.5],
alternativesKey: 'range function',
category: PromVisualQueryOperationCategory.RangeFunctions,
renderer: rangeRendererLeftWithParams,
}),
createFunction({ id: PromOperationId.Rad }),
createRangeFunction(PromOperationId.Resets),
createFunction({
id: PromOperationId.Round,
category: PromVisualQueryOperationCategory.Functions,
params: [{ name: 'To Nearest', type: 'number' }],
defaultParams: [1],
}),
createFunction({ id: PromOperationId.Scalar }),
createFunction({ id: PromOperationId.Sgn }),
createFunction({ id: PromOperationId.Sin, category: PromVisualQueryOperationCategory.Trigonometric }),
createFunction({
id: PromOperationId.Sinh,
category: PromVisualQueryOperationCategory.Trigonometric,
}),
createFunction({ id: PromOperationId.Sort }),
createFunction({ id: PromOperationId.SortDesc }),
createFunction({ id: PromOperationId.Sqrt }),
createFunction({ id: PromOperationId.Stddev }),
createFunction({
id: PromOperationId.Tan,
category: PromVisualQueryOperationCategory.Trigonometric,
}),
createFunction({
id: PromOperationId.Tanh,
category: PromVisualQueryOperationCategory.Trigonometric,
}),
createFunction({
id: PromOperationId.Time,
renderer: (model) => `${model.id}()`,
}),
createFunction({ id: PromOperationId.Timestamp }),
createFunction({
id: PromOperationId.Vector,
params: [{ name: 'Value', type: 'number' }],
defaultParams: [1],
renderer: (model) => `${model.id}(${model.params[0]})`,
}),
createFunction({ id: PromOperationId.Year }),
];
return list;
}
function createRangeFunction(name: string): QueryBuilderOperationDef {
export function createFunction(definition: Partial<QueryBuilderOperationDef>): QueryBuilderOperationDef {
return {
...definition,
id: definition.id!,
name: definition.name ?? getPromAndLokiOperationDisplayName(definition.id!),
params: definition.params ?? [],
defaultParams: definition.defaultParams ?? [],
category: definition.category ?? PromVisualQueryOperationCategory.Functions,
renderer: definition.renderer ?? (definition.params ? functionRendererRight : functionRendererLeft),
addOperationHandler: definition.addOperationHandler ?? defaultAddOperationHandler,
};
}
export function createRangeFunction(name: string): QueryBuilderOperationDef {
return {
id: name,
name: getPromAndLokiOperationDisplayName(name),
@ -98,7 +301,7 @@ function createRangeFunction(name: string): QueryBuilderOperationDef {
};
}
function operationWithRangeVectorRenderer(
export function operationWithRangeVectorRenderer(
model: QueryBuilderOperation,
def: QueryBuilderOperationDef,
innerExpr: string
@ -174,3 +377,23 @@ function addNestedQueryHandler(def: QueryBuilderOperationDef, query: PromVisualQ
],
};
}
function labelJoinRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
if (typeof model.params[1] !== 'string') {
throw 'The separator must be a string';
}
const separator = `"${model.params[1]}"`;
return `${model.id}(${innerExpr}, "${model.params[0]}", ${separator}, "${model.params.slice(2).join(separator)}")`;
}
function labelJoinAddOperationHandler<T extends QueryWithOperations>(def: QueryBuilderOperationDef, query: T) {
const newOperation: QueryBuilderOperation = {
id: def.id,
params: def.defaultParams,
};
return {
...query,
operations: [...query.operations, newOperation],
};
}

View File

@ -23,6 +23,67 @@ export function functionRendererRight(model: QueryBuilderOperation, def: QueryBu
return str + params.join(', ') + ')';
}
function rangeRendererWithParams(
model: QueryBuilderOperation,
def: QueryBuilderOperationDef,
innerExpr: string,
renderLeft: boolean
) {
if (def.params.length < 2) {
throw `Cannot render a function with params of length [${def.params.length}]`;
}
// First, make sure the first parameter (that is the range vector) is translated if the user selected 'auto'
let rangeVector = (model.params ?? [])[0] ?? 'auto';
if (rangeVector === 'auto') {
rangeVector = '$__rate_interval';
}
// Next frame the remaining parameters, but get rid of the first one because it's used to move the
// instant vector into a range vector.
const params = renderParams(
{
...model,
params: model.params.slice(1),
},
{
...def,
params: def.params.slice(1),
defaultParams: def.defaultParams.slice(1),
},
innerExpr
);
const str = model.id + '(';
// Depending on the renderLeft variable, render parameters to the left or right
// renderLeft === true (renderLeft) => (param1, param2, rangeVector[...])
// renderLeft === false (renderRight) => (rangeVector[...], param1, param2)
if (innerExpr) {
renderLeft ? params.push(`${innerExpr}[${rangeVector}]`) : params.unshift(`${innerExpr}[${rangeVector}]`);
}
// stick everything together
return str + params.join(', ') + ')';
}
export function rangeRendererRightWithParams(
model: QueryBuilderOperation,
def: QueryBuilderOperationDef,
innerExpr: string
) {
return rangeRendererWithParams(model, def, innerExpr, false);
}
export function rangeRendererLeftWithParams(
model: QueryBuilderOperation,
def: QueryBuilderOperationDef,
innerExpr: string
) {
return rangeRendererWithParams(model, def, innerExpr, true);
}
function renderParams(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
return (model.params ?? []).map((value, index) => {
const paramDef = def.params[index];

View File

@ -20,35 +20,94 @@ export enum PromVisualQueryOperationCategory {
RangeFunctions = 'Range functions',
Functions = 'Functions',
BinaryOps = 'Binary operations',
Trigonometric = 'Trigonometric',
Time = 'Time Functions',
}
export enum PromOperationId {
HistogramQuantile = 'histogram_quantile',
LabelReplace = 'label_replace',
Ln = 'ln',
Abs = 'abs',
Absent = 'absent',
AbsentOverTime = 'absent_over_time',
Acos = 'acos',
Acosh = 'acosh',
Asin = 'asin',
Asinh = 'asinh',
Atan = 'atan',
Atanh = 'atanh',
Avg = 'avg',
AvgOverTime = 'avg_over_time',
BottomK = 'bottomk',
Ceil = 'ceil',
Changes = 'changes',
Rate = 'rate',
Irate = 'irate',
Increase = 'increase',
Clamp = 'clamp',
ClampMax = 'clamp_max',
ClampMin = 'clamp_min',
Cos = 'cos',
Cosh = 'cosh',
Count = 'count',
CountOverTime = 'count_over_time',
CountScalar = 'count_scalar',
CountValues = 'count_values',
DayOfMonth = 'day_of_month',
DayOfWeek = 'day_of_week',
DaysInMonth = 'days_in_month',
Deg = 'deg',
Delta = 'delta',
Deriv = 'deriv',
DropCommonLabels = 'drop_common_labels',
Exp = 'exp',
Floor = 'floor',
Group = 'group',
HistogramQuantile = 'histogram_quantile',
HoltWinters = 'holt_winters',
Hour = 'hour',
Idelta = 'idelta',
Increase = 'increase',
Irate = 'irate',
LabelJoin = 'label_join',
LabelReplace = 'label_replace',
Last = 'last',
LastOverTime = 'last_over_time',
Ln = 'ln',
Log10 = 'log10',
Log2 = 'log2',
Max = 'max',
MaxOverTime = 'max_over_time',
Min = 'min',
MinOverTime = 'min_over_time',
Minute = 'minute',
Month = 'month',
Pi = 'pi',
PredictLinear = 'predict_linear',
Present = 'present',
PresentOverTime = 'present_over_time',
Quantile = 'quantile',
QuantileOverTime = 'quantile_over_time',
Rad = 'rad',
Rate = 'rate',
Resets = 'resets',
Round = 'round',
Scalar = 'scalar',
Sgn = 'sgn',
Sin = 'sin',
Sinh = 'sinh',
Sort = 'sort',
SortDesc = 'sort_desc',
Sqrt = 'sqrt',
Stddev = 'stddev',
StddevOverTime = 'stddev_over_time',
Sum = 'sum',
SumOverTime = 'sum_over_time',
Tan = 'tan',
Tanh = 'tanh',
Time = 'time',
Timestamp = 'timestamp',
Topk = 'topk',
Vector = 'vector',
Year = 'year',
MultiplyBy = '__multiply_by',
DivideBy = '__divide_by',
NestedQuery = '__nested_query',
Sum = 'sum',
Avg = 'avg',
Min = 'min',
Max = 'max',
Count = 'count',
Topk = 'topk',
SumOverTime = 'sum_over_time',
AvgOverTime = 'avg_over_time',
MinOverTime = 'min_over_time',
MaxOverTime = 'max_over_time',
CountOverTime = 'count_over_time',
LastOverTime = 'last_over_time',
PresentOverTime = 'present_over_time',
StddevOverTime = 'stddev_over_time',
StdvarOverTime = 'stdvar_over_time',
}
export interface PromQueryPattern {