mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
f42c830b3c
commit
abe32b9521
@ -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")');
|
||||
});
|
||||
});
|
||||
|
@ -22,6 +22,8 @@ export class PromQueryModeller extends LokiAndPromQueryModellerBase<PromVisualQu
|
||||
PromVisualQueryOperationCategory.RangeFunctions,
|
||||
PromVisualQueryOperationCategory.Functions,
|
||||
PromVisualQueryOperationCategory.BinaryOps,
|
||||
PromVisualQueryOperationCategory.Trigonometric,
|
||||
PromVisualQueryOperationCategory.Time,
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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],
|
||||
};
|
||||
}
|
||||
|
@ -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];
|
||||
|
@ -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 {
|
||||
|
Loading…
Reference in New Issue
Block a user