Loki: Decouple from Prometheus operationUtils (#78830)

* Loki: Decouple from Prometheus operationUtils

* Update comments
This commit is contained in:
Ivana Huckova 2023-12-12 11:44:35 +01:00 committed by GitHub
parent 4d375aa5d4
commit ed86583107
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 415 additions and 45 deletions

View File

@ -5861,6 +5861,9 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "17"],
[0, 0, 0, "Styles should be written using objects.", "18"]
],
"public/app/plugins/datasource/prometheus/querybuilder/operationUtils.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/prometheus/querybuilder/shared/LabelFilterItem.tsx:5381": [
[0, 0, 0, "Use data-testid for E2E selectors instead of aria-label", "0"],
[0, 0, 0, "Do not use any type assertions.", "1"],
@ -5889,9 +5892,6 @@ exports[`better eslint`] = {
[0, 0, 0, "Styles should be written using objects.", "0"],
[0, 0, 0, "Styles should be written using objects.", "1"]
],
"public/app/plugins/datasource/prometheus/querybuilder/shared/operationUtils.ts:5381": [
[0, 0, 0, "Do not use any type assertions.", "0"]
],
"public/app/plugins/datasource/prometheus/querybuilder/shared/parsingUtils.ts:5381": [
[0, 0, 0, "Unexpected any. Specify a different type.", "0"]
],

View File

@ -1,10 +1,10 @@
import { defaultAddOperationHandler } from '../../prometheus/querybuilder/shared/operationUtils';
import {
QueryBuilderOperation,
QueryBuilderOperationDef,
QueryBuilderOperationParamDef,
} from '../../prometheus/querybuilder/shared/types';
import { defaultAddOperationHandler } from './operationUtils';
import { LokiOperationId, LokiVisualQueryOperationCategory } from './types';
export const binaryScalarDefs = [

View File

@ -3,13 +3,13 @@ import React, { useState } from 'react';
import { SelectableValue, getDefaultTimeRange, toOption } from '@grafana/data';
import { Select } from '@grafana/ui';
import { getOperationParamId } from '../../../prometheus/querybuilder/shared/operationUtils';
import { QueryBuilderOperationParamEditorProps } from '../../../prometheus/querybuilder/shared/types';
import { placeHolderScopedVars } from '../../components/monaco-query-field/monaco-completion-provider/validation';
import { LokiDatasource } from '../../datasource';
import { getLogQueryFromMetricsQuery, isQueryWithError } from '../../queryUtils';
import { extractUnwrapLabelKeysFromDataFrame } from '../../responseUtils';
import { lokiQueryModeller } from '../LokiQueryModeller';
import { getOperationParamId } from '../operationUtils';
import { LokiVisualQuery } from '../types';
export function UnwrapParamEditor({

View File

@ -1,6 +1,8 @@
import { QueryBuilderOperation, QueryBuilderOperationDef } from '../../prometheus/querybuilder/shared/types';
import {
createAggregationOperation,
createAggregationOperationWithParam,
createRangeOperation,
createRangeOperationWithGrouping,
getLineFilterRenderer,
@ -322,3 +324,168 @@ describe('pipelineRenderer', () => {
expect(pipelineRenderer(model, definition!, '{}')).toBe('{} | drop foo, bar, baz');
});
});
describe('createAggregationOperation', () => {
it('returns correct aggregation definitions with overrides', () => {
expect(createAggregationOperation('test_aggregation', { category: 'test_category' })).toMatchObject([
{
addOperationHandler: {},
alternativesKey: 'plain aggregations',
category: 'test_category',
defaultParams: [],
explainHandler: {},
id: 'test_aggregation',
name: 'Test aggregation',
paramChangedHandler: {},
params: [
{
name: 'By label',
optional: true,
restParam: true,
type: 'string',
},
],
renderer: {},
},
{
alternativesKey: 'aggregations by',
category: 'test_category',
defaultParams: [''],
explainHandler: {},
hideFromList: true,
id: '__test_aggregation_by',
name: 'Test aggregation by',
paramChangedHandler: {},
params: [
{
editor: {},
name: 'Label',
optional: true,
restParam: true,
type: 'string',
},
],
renderer: {},
},
{
alternativesKey: 'aggregations by',
category: 'test_category',
defaultParams: [''],
explainHandler: {},
hideFromList: true,
id: '__test_aggregation_without',
name: 'Test aggregation without',
paramChangedHandler: {},
params: [
{
name: 'Label',
optional: true,
restParam: true,
type: 'string',
},
],
renderer: {},
},
]);
});
});
describe('createAggregationOperationWithParams', () => {
it('returns correct aggregation definitions with overrides and params', () => {
expect(
createAggregationOperationWithParam(
'test_aggregation',
{
params: [{ name: 'K-value', type: 'number' }],
defaultParams: [5],
},
{ category: 'test_category' }
)
).toMatchObject([
{
addOperationHandler: {},
alternativesKey: 'plain aggregations',
category: 'test_category',
defaultParams: [5],
explainHandler: {},
id: 'test_aggregation',
name: 'Test aggregation',
paramChangedHandler: {},
params: [
{ name: 'K-value', type: 'number' },
{ name: 'By label', optional: true, restParam: true, type: 'string' },
],
renderer: {},
},
{
alternativesKey: 'aggregations by',
category: 'test_category',
defaultParams: [5, ''],
explainHandler: {},
hideFromList: true,
id: '__test_aggregation_by',
name: 'Test aggregation by',
paramChangedHandler: {},
params: [
{ name: 'K-value', type: 'number' },
{ editor: {}, name: 'Label', optional: true, restParam: true, type: 'string' },
],
renderer: {},
},
{
alternativesKey: 'aggregations by',
category: 'test_category',
defaultParams: [5, ''],
explainHandler: {},
hideFromList: true,
id: '__test_aggregation_without',
name: 'Test aggregation without',
paramChangedHandler: {},
params: [
{ name: 'K-value', type: 'number' },
{ name: 'Label', optional: true, restParam: true, type: 'string' },
],
renderer: {},
},
]);
});
it('returns correct query string using aggregation definitions with overrides and number type param', () => {
const def = createAggregationOperationWithParam(
'test_aggregation',
{
params: [{ name: 'K-value', type: 'number' }],
defaultParams: [5],
},
{ category: 'test_category' }
);
const topKByDefinition = def[1];
expect(
topKByDefinition.renderer(
{ id: '__topk_by', params: ['5', 'source', 'place'] },
def[1],
'rate({place="luna"} |= `` [5m])'
)
).toBe('test_aggregation by(source, place) (5, rate({place="luna"} |= `` [5m]))');
});
it('returns correct query string using aggregation definitions with overrides and string type param', () => {
const def = createAggregationOperationWithParam(
'test_aggregation',
{
params: [{ name: 'Identifier', type: 'string' }],
defaultParams: ['count'],
},
{ category: 'test_category' }
);
const countValueDefinition = def[1];
expect(
countValueDefinition.renderer(
{ id: 'count_values', params: ['5', 'source', 'place'] },
def[1],
'rate({place="luna"} |= `` [5m])'
)
).toBe('test_aggregation by(source, place) ("5", rate({place="luna"} |= `` [5m]))');
});
});

View File

@ -1,14 +1,13 @@
import { capitalize } from 'lodash';
import pluralize from 'pluralize';
import { LabelParamEditor } from '../../prometheus/querybuilder/components/LabelParamEditor';
import {
getAggregationExplainer,
getLastLabelRemovedHandler,
getOnLabelAddedHandler,
getPromAndLokiOperationDisplayName,
} from '../../prometheus/querybuilder/shared/operationUtils';
import {
QueryBuilderOperation,
QueryBuilderOperationDef,
QueryBuilderOperationParamDef,
QueryBuilderOperationParamValue,
QueryWithOperations,
VisualQueryModeller,
} from '../../prometheus/querybuilder/shared/types';
import { FUNCTIONS } from '../syntax';
@ -41,7 +40,7 @@ export function createRangeOperation(name: string, isRangeOperationWithGrouping?
return {
id: name,
name: getPromAndLokiOperationDisplayName(name),
name: getLokiOperationDisplayName(name),
params: params,
defaultParams,
alternativesKey: 'range function',
@ -70,7 +69,7 @@ export function createRangeOperationWithGrouping(name: string): QueryBuilderOper
rangeOperation,
{
id: `__${name}_by`,
name: `${getPromAndLokiOperationDisplayName(name)} by`,
name: `${getLokiOperationDisplayName(name)} by`,
params: [
...params,
{
@ -92,7 +91,7 @@ export function createRangeOperationWithGrouping(name: string): QueryBuilderOper
},
{
id: `__${name}_without`,
name: `${getPromAndLokiOperationDisplayName(name)} without`,
name: `${getLokiOperationDisplayName(name)} without`,
params: [
...params,
{
@ -309,3 +308,212 @@ function getRangeVectorParamDef(): QueryBuilderOperationParamDef {
options: ['$__auto', '1m', '5m', '10m', '1h', '24h'],
};
}
export function getOperationParamId(operationId: string, paramIndex: number) {
return `operations.${operationId}.param.${paramIndex}`;
}
export function getOnLabelAddedHandler(changeToOperationId: string) {
return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDef) {
// Check if we actually have the label param. As it's optional the aggregation can have one less, which is the
// case of just simple aggregation without label. When user adds the label it now has the same number of params
// as its definition, and now we can change it to its `_by` variant.
if (op.params.length === def.params.length) {
return {
...op,
id: changeToOperationId,
};
}
return op;
};
}
/**
* Very simple poc implementation, needs to be modified to support all aggregation operators
*/
export function getAggregationExplainer(aggregationName: string, mode: 'by' | 'without' | '') {
return function aggregationExplainer(model: QueryBuilderOperation) {
const labels = model.params.map((label) => `\`${label}\``).join(' and ');
const labelWord = pluralize('label', model.params.length);
switch (mode) {
case 'by':
return `Calculates ${aggregationName} over dimensions while preserving ${labelWord} ${labels}.`;
case 'without':
return `Calculates ${aggregationName} over the dimensions ${labels}. All other labels are preserved.`;
default:
return `Calculates ${aggregationName} over the dimensions.`;
}
};
}
/**
* This function will transform operations without labels to their plan aggregation operation
*/
export function getLastLabelRemovedHandler(changeToOperationId: string) {
return function onParamChanged(index: number, op: QueryBuilderOperation, def: QueryBuilderOperationDef) {
// If definition has more params then is defined there are no optional rest params anymore.
// We then transform this operation into a different one
if (op.params.length < def.params.length) {
return {
...op,
id: changeToOperationId,
};
}
return op;
};
}
export function getLokiOperationDisplayName(funcName: string) {
return capitalize(funcName.replace(/_/g, ' '));
}
export function defaultAddOperationHandler<T extends QueryWithOperations>(def: QueryBuilderOperationDef, query: T) {
const newOperation: QueryBuilderOperation = {
id: def.id,
params: def.defaultParams,
};
return {
...query,
operations: [...query.operations, newOperation],
};
}
export function createAggregationOperation(
name: string,
overrides: Partial<QueryBuilderOperationDef> = {}
): QueryBuilderOperationDef[] {
const operations: QueryBuilderOperationDef[] = [
{
id: name,
name: getLokiOperationDisplayName(name),
params: [
{
name: 'By label',
type: 'string',
restParam: true,
optional: true,
},
],
defaultParams: [],
alternativesKey: 'plain aggregations',
category: LokiVisualQueryOperationCategory.Aggregations,
renderer: functionRendererLeft,
paramChangedHandler: getOnLabelAddedHandler(`__${name}_by`),
explainHandler: getAggregationExplainer(name, ''),
addOperationHandler: defaultAddOperationHandler,
...overrides,
},
{
id: `__${name}_by`,
name: `${getLokiOperationDisplayName(name)} by`,
params: [
{
name: 'Label',
type: 'string',
restParam: true,
optional: true,
editor: LabelParamEditor,
},
],
defaultParams: [''],
alternativesKey: 'aggregations by',
category: LokiVisualQueryOperationCategory.Aggregations,
renderer: getAggregationByRenderer(name),
paramChangedHandler: getLastLabelRemovedHandler(name),
explainHandler: getAggregationExplainer(name, 'by'),
addOperationHandler: defaultAddOperationHandler,
hideFromList: true,
...overrides,
},
{
id: `__${name}_without`,
name: `${getLokiOperationDisplayName(name)} without`,
params: [
{
name: 'Label',
type: 'string',
restParam: true,
optional: true,
editor: LabelParamEditor,
},
],
defaultParams: [''],
alternativesKey: 'aggregations by',
category: LokiVisualQueryOperationCategory.Aggregations,
renderer: getAggregationWithoutRenderer(name),
paramChangedHandler: getLastLabelRemovedHandler(name),
explainHandler: getAggregationExplainer(name, 'without'),
addOperationHandler: defaultAddOperationHandler,
hideFromList: true,
...overrides,
},
];
return operations;
}
function getAggregationWithoutRenderer(aggregation: string) {
return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
return `${aggregation} without(${model.params.join(', ')}) (${innerExpr})`;
};
}
export function functionRendererLeft(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
const params = renderParams(model, def, innerExpr);
const str = model.id + '(';
if (innerExpr) {
params.push(innerExpr);
}
return str + params.join(', ') + ')';
}
function renderParams(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
return (model.params ?? []).map((value, index) => {
const paramDef = def.params[index];
if (paramDef.type === 'string') {
return '"' + value + '"';
}
return value;
});
}
function getAggregationByRenderer(aggregation: string) {
return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
return `${aggregation} by(${model.params.join(', ')}) (${innerExpr})`;
};
}
export function createAggregationOperationWithParam(
name: string,
paramsDef: { params: QueryBuilderOperationParamDef[]; defaultParams: QueryBuilderOperationParamValue[] },
overrides: Partial<QueryBuilderOperationDef> = {}
): QueryBuilderOperationDef[] {
const operations = createAggregationOperation(name, overrides);
operations[0].params.unshift(...paramsDef.params);
operations[1].params.unshift(...paramsDef.params);
operations[2].params.unshift(...paramsDef.params);
operations[0].defaultParams = paramsDef.defaultParams;
operations[1].defaultParams = [...paramsDef.defaultParams, ''];
operations[2].defaultParams = [...paramsDef.defaultParams, ''];
operations[1].renderer = getAggregationByRendererWithParameter(name);
operations[2].renderer = getAggregationByRendererWithParameter(name);
return operations;
}
function getAggregationByRendererWithParameter(aggregation: string) {
return function aggregationRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
const restParamIndex = def.params.findIndex((param) => param.restParam);
const params = model.params.slice(0, restParamIndex);
const restParams = model.params.slice(restParamIndex);
return `${aggregation} by(${restParams.join(', ')}) (${params
.map((param, idx) => (def.params[idx].type === 'string' ? `\"${param}\"` : param))
.join(', ')}, ${innerExpr})`;
};
}

View File

@ -1,7 +1,3 @@
import {
createAggregationOperation,
createAggregationOperationWithParam,
} from '../../prometheus/querybuilder/shared/operationUtils';
import { QueryBuilderOperationDef, QueryBuilderOperationParamValue } from '../../prometheus/querybuilder/shared/types';
import { binaryScalarOperations } from './binaryScalarOperations';
@ -9,6 +5,8 @@ import { UnwrapParamEditor } from './components/UnwrapParamEditor';
import {
addLokiOperation,
addNestedQueryHandler,
createAggregationOperation,
createAggregationOperationWithParam,
createRangeOperation,
createRangeOperationWithGrouping,
getLineFilterRenderer,

View File

@ -1,10 +1,10 @@
import { addOperationWithRangeVector } from './operations';
import {
createAggregationOperation,
createAggregationOperationWithParam,
getPromAndLokiOperationDisplayName,
getPromOperationDisplayName,
getRangeVectorParamDef,
} from './shared/operationUtils';
} from './operationUtils';
import { addOperationWithRangeVector } from './operations';
import { QueryBuilderOperation, QueryBuilderOperationDef } from './shared/types';
import { PromVisualQueryOperationCategory, PromOperationId } from './types';
@ -42,7 +42,7 @@ export function getAggregationOperations(): QueryBuilderOperationDef[] {
function createAggregationOverTime(name: string): QueryBuilderOperationDef {
return {
id: name,
name: getPromAndLokiOperationDisplayName(name),
name: getPromOperationDisplayName(name),
params: [getRangeVectorParamDef()],
defaultParams: ['$__interval'],
alternativesKey: 'overtime function',

View File

@ -1,4 +1,4 @@
import { defaultAddOperationHandler } from './shared/operationUtils';
import { defaultAddOperationHandler } from './operationUtils';
import { QueryBuilderOperation, QueryBuilderOperationDef, QueryBuilderOperationParamDef } from './shared/types';
import { PromOperationId, PromVisualQueryOperationCategory } from './types';

View File

@ -4,7 +4,7 @@ import { DataSourceApi, SelectableValue, toOption } from '@grafana/data';
import { Select } from '@grafana/ui';
import { promQueryModeller } from '../PromQueryModeller';
import { getOperationParamId } from '../shared/operationUtils';
import { getOperationParamId } from '../operationUtils';
import { QueryBuilderLabelFilter, QueryBuilderOperationParamEditorProps } from '../shared/types';
import { PromVisualQuery } from '../types';

View File

@ -8,8 +8,8 @@ import { PrometheusDatasource } from '../../datasource';
import PromQlLanguageProvider from '../../language_provider';
import { EmptyLanguageProviderMock } from '../../language_provider.mock';
import { PromQuery } from '../../types';
import { getOperationParamId } from '../operationUtils';
import { addOperation } from '../shared/OperationList.testUtils';
import { getOperationParamId } from '../shared/operationUtils';
import { PromQueryBuilderContainer } from './PromQueryBuilderContainer';

View File

@ -3,9 +3,7 @@ import pluralize from 'pluralize';
import { SelectableValue } from '@grafana/data/src';
import { LabelParamEditor } from '../components/LabelParamEditor';
import { PromVisualQueryOperationCategory } from '../types';
import { LabelParamEditor } from './components/LabelParamEditor';
import {
QueryBuilderLabelFilter,
QueryBuilderOperation,
@ -13,7 +11,8 @@ import {
QueryBuilderOperationParamDef,
QueryBuilderOperationParamValue,
QueryWithOperations,
} from './types';
} from './shared/types';
import { PromVisualQueryOperationCategory } from './types';
export function functionRendererLeft(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
const params = renderParams(model, def, innerExpr);
@ -116,7 +115,7 @@ export function defaultAddOperationHandler<T extends QueryWithOperations>(def: Q
};
}
export function getPromAndLokiOperationDisplayName(funcName: string) {
export function getPromOperationDisplayName(funcName: string) {
return capitalize(funcName.replace(/_/g, ' '));
}
@ -153,17 +152,14 @@ export function getRangeVectorParamDef(withRateInterval = false): QueryBuilderOp
return param;
}
/**
* This function is shared between Prometheus and Loki variants
*/
export function createAggregationOperation<T extends QueryWithOperations>(
export function createAggregationOperation(
name: string,
overrides: Partial<QueryBuilderOperationDef> = {}
): QueryBuilderOperationDef[] {
const operations: QueryBuilderOperationDef[] = [
{
id: name,
name: getPromAndLokiOperationDisplayName(name),
name: getPromOperationDisplayName(name),
params: [
{
name: 'By label',
@ -183,7 +179,7 @@ export function createAggregationOperation<T extends QueryWithOperations>(
},
{
id: `__${name}_by`,
name: `${getPromAndLokiOperationDisplayName(name)} by`,
name: `${getPromOperationDisplayName(name)} by`,
params: [
{
name: 'Label',
@ -205,7 +201,7 @@ export function createAggregationOperation<T extends QueryWithOperations>(
},
{
id: `__${name}_without`,
name: `${getPromAndLokiOperationDisplayName(name)} without`,
name: `${getPromOperationDisplayName(name)} without`,
params: [
{
name: 'Label',

View File

@ -4,11 +4,11 @@ import {
defaultAddOperationHandler,
functionRendererLeft,
functionRendererRight,
getPromAndLokiOperationDisplayName,
getPromOperationDisplayName,
getRangeVectorParamDef,
rangeRendererLeftWithParams,
rangeRendererRightWithParams,
} from './shared/operationUtils';
} from './operationUtils';
import {
QueryBuilderOperation,
QueryBuilderOperationDef,
@ -265,7 +265,7 @@ export function createFunction(definition: Partial<QueryBuilderOperationDef>): Q
return {
...definition,
id: definition.id!,
name: definition.name ?? getPromAndLokiOperationDisplayName(definition.id!),
name: definition.name ?? getPromOperationDisplayName(definition.id!),
params: definition.params ?? [],
defaultParams: definition.defaultParams ?? [],
category: definition.category ?? PromVisualQueryOperationCategory.Functions,
@ -277,7 +277,7 @@ export function createFunction(definition: Partial<QueryBuilderOperationDef>): Q
export function createRangeFunction(name: string, withRateInterval = false): QueryBuilderOperationDef {
return {
id: name,
name: getPromAndLokiOperationDisplayName(name),
name: getPromOperationDisplayName(name),
params: [getRangeVectorParamDef(withRateInterval)],
defaultParams: [withRateInterval ? '$__rate_interval' : '$__interval'],
alternativesKey: 'range function',

View File

@ -7,7 +7,8 @@ import { AccessoryButton, InputGroup } from '@grafana/experimental';
import { InlineField, Select } from '@grafana/ui';
import { lokiOperators } from 'app/plugins/datasource/loki/querybuilder/types';
import { isConflictingSelector } from './operationUtils';
import { isConflictingSelector } from '../operationUtils';
import { QueryBuilderLabelFilter } from './types';
export interface Props {

View File

@ -7,9 +7,10 @@ import { Button, Icon, InlineField, Tooltip, useTheme2, Stack } from '@grafana/u
import { isConflictingFilter } from 'app/plugins/datasource/loki/querybuilder/operationUtils';
import { LokiOperationId } from 'app/plugins/datasource/loki/querybuilder/types';
import { getOperationParamId } from '../operationUtils';
import { OperationHeader } from './OperationHeader';
import { getOperationParamEditor } from './OperationParamEditor';
import { getOperationParamId } from './operationUtils';
import {
QueryBuilderOperation,
QueryBuilderOperationDef,

View File

@ -4,10 +4,9 @@ import React, { ComponentType } from 'react';
import { GrafanaTheme2, SelectableValue, toOption } from '@grafana/data';
import { AutoSizeInput, Button, Checkbox, Select, useStyles2, Stack } from '@grafana/ui';
import { getOperationParamId } from '../operationUtils';
import { QueryBuilderOperationParamDef, QueryBuilderOperationParamEditorProps } from '../shared/types';
import { getOperationParamId } from './operationUtils';
export function getOperationParamEditor(
paramDef: QueryBuilderOperationParamDef
): ComponentType<QueryBuilderOperationParamEditorProps> {