mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
* Loki autocomplete: add IN_LOGFMT situation for log queries * Loki autocomplete: add IN_LOGFMT situation for metric queries * Loki autocomplete: improve handling of trailing pipes and spaces * Loki autocomplete: add logfmt arguments completion * Loki autocomplete: add flags support to IN_LOGFMT * Loki autocomplete: extend IN_LOGFMT situation with labels and flag * Loki autocomplete: return logQuery in IN_LOGFMT situation * Loki autocomplete: offer label completions when IN_LOGFMT * Query utils: update parser detection method * Validation: update test * Loki autocomplete: improve IN_LOGFMT detection when in metric query * Loki autocomplete: improve logfmt suggestions * Loki autocomplete: improve logfmt suggestions in different scenarios * Loki autocomplete situation: refactor resolvers to support multiple paths * Situation: add test case * Loki autocomplete: allow user to use 2 flags * Situation: change flag to flags * Remove console log * Validation: import test parser * Completions: better handling of trailing comma scenario * Upgrade lezer-logql * Revert temporary imports * Loki Query Builder: Add support for new logfmt features (#74858) * Query builder: add params to logfmt definition * Logfmt operation: add default params * Query builder: update deprecated JsonExpression * Operation utils: update logfmt renderer * Query builder: parse LogfmtParser * Query builder: parse LogfmtExpressionParser * Remove console log * Remove unused variable * Remove extra character from render * Update unit tests * Fix unit tests * Operations: remove restParams from logfmt booleans * Parsing: group cases * Formatting * Formatting * Update modifyQuery * LogContextProvider: update with parser changes * LogContextProvider: remove unnecessary type castings It takes more energy to write `as unknow as LokiQuery` than to write a refId. * Formatting * Situation: use charAt instead of substring with endsWith * Situation: explain logfmt suggestions * Logfmt: improve flag suggestions * Remove console log * Completions: update test
306 lines
10 KiB
TypeScript
306 lines
10 KiB
TypeScript
import { LabelParamEditor } from '../../prometheus/querybuilder/components/LabelParamEditor';
|
|
import {
|
|
getAggregationExplainer,
|
|
getLastLabelRemovedHandler,
|
|
getOnLabelAddedHandler,
|
|
getPromAndLokiOperationDisplayName,
|
|
} from '../../prometheus/querybuilder/shared/operationUtils';
|
|
import {
|
|
QueryBuilderOperation,
|
|
QueryBuilderOperationDef,
|
|
QueryBuilderOperationParamDef,
|
|
VisualQueryModeller,
|
|
} from '../../prometheus/querybuilder/shared/types';
|
|
import { FUNCTIONS } from '../syntax';
|
|
|
|
import { LokiOperationId, LokiOperationOrder, LokiVisualQuery, LokiVisualQueryOperationCategory } from './types';
|
|
|
|
export function createRangeOperation(name: string, isRangeOperationWithGrouping?: boolean): QueryBuilderOperationDef {
|
|
const params = [getRangeVectorParamDef()];
|
|
const defaultParams = ['$__auto'];
|
|
let paramChangedHandler = undefined;
|
|
|
|
if (name === LokiOperationId.QuantileOverTime) {
|
|
defaultParams.push('0.95');
|
|
params.push({
|
|
name: 'Quantile',
|
|
type: 'number',
|
|
});
|
|
}
|
|
|
|
if (isRangeOperationWithGrouping) {
|
|
params.push({
|
|
name: 'By label',
|
|
type: 'string',
|
|
restParam: true,
|
|
optional: true,
|
|
});
|
|
|
|
paramChangedHandler = getOnLabelAddedHandler(`__${name}_by`);
|
|
}
|
|
|
|
return {
|
|
id: name,
|
|
name: getPromAndLokiOperationDisplayName(name),
|
|
params: params,
|
|
defaultParams,
|
|
alternativesKey: 'range function',
|
|
category: LokiVisualQueryOperationCategory.RangeFunctions,
|
|
orderRank: LokiOperationOrder.RangeVectorFunction,
|
|
renderer: operationWithRangeVectorRenderer,
|
|
addOperationHandler: addLokiOperation,
|
|
paramChangedHandler,
|
|
explainHandler: (op, def) => {
|
|
let opDocs = FUNCTIONS.find((x) => x.insertText === op.id)?.documentation ?? '';
|
|
|
|
if (op.params[0] === '$__auto') {
|
|
return `${opDocs} \`$__auto\` is a variable that will be replaced with the [value of step](https://grafana.com/docs/grafana/next/datasources/loki/query-editor/#options) for range queries and with the value of the selected time range (calculated to - from) for instant queries.`;
|
|
} else {
|
|
return `${opDocs} The [range vector](https://grafana.com/docs/loki/latest/logql/metric_queries/#range-vector-aggregation) is set to \`${op.params[0]}\`.`;
|
|
}
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createRangeOperationWithGrouping(name: string): QueryBuilderOperationDef[] {
|
|
const rangeOperation = createRangeOperation(name, true);
|
|
// Copy range operation params without the last param
|
|
const params = rangeOperation.params.slice(0, -1);
|
|
const operations: QueryBuilderOperationDef[] = [
|
|
rangeOperation,
|
|
{
|
|
id: `__${name}_by`,
|
|
name: `${getPromAndLokiOperationDisplayName(name)} by`,
|
|
params: [
|
|
...params,
|
|
{
|
|
name: 'Label',
|
|
type: 'string',
|
|
restParam: true,
|
|
optional: true,
|
|
editor: LabelParamEditor,
|
|
},
|
|
],
|
|
defaultParams: [...rangeOperation.defaultParams, ''],
|
|
alternativesKey: 'range function with grouping',
|
|
category: LokiVisualQueryOperationCategory.RangeFunctions,
|
|
renderer: getRangeAggregationWithGroupingRenderer(name, 'by'),
|
|
paramChangedHandler: getLastLabelRemovedHandler(name),
|
|
explainHandler: getAggregationExplainer(name, 'by'),
|
|
addOperationHandler: addLokiOperation,
|
|
hideFromList: true,
|
|
},
|
|
{
|
|
id: `__${name}_without`,
|
|
name: `${getPromAndLokiOperationDisplayName(name)} without`,
|
|
params: [
|
|
...params,
|
|
{
|
|
name: 'Label',
|
|
type: 'string',
|
|
restParam: true,
|
|
optional: true,
|
|
editor: LabelParamEditor,
|
|
},
|
|
],
|
|
defaultParams: [...rangeOperation.defaultParams, ''],
|
|
alternativesKey: 'range function with grouping',
|
|
category: LokiVisualQueryOperationCategory.RangeFunctions,
|
|
renderer: getRangeAggregationWithGroupingRenderer(name, 'without'),
|
|
paramChangedHandler: getLastLabelRemovedHandler(name),
|
|
explainHandler: getAggregationExplainer(name, 'without'),
|
|
addOperationHandler: addLokiOperation,
|
|
hideFromList: true,
|
|
},
|
|
];
|
|
|
|
return operations;
|
|
}
|
|
|
|
export function getRangeAggregationWithGroupingRenderer(aggregation: string, grouping: 'by' | 'without') {
|
|
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);
|
|
|
|
if (params.length === 2 && aggregation === LokiOperationId.QuantileOverTime) {
|
|
return `${aggregation}(${params[1]}, ${innerExpr} [${params[0]}]) ${grouping} (${restParams.join(', ')})`;
|
|
}
|
|
|
|
return `${aggregation}(${innerExpr} [${params[0]}]) ${grouping} (${restParams.join(', ')})`;
|
|
};
|
|
}
|
|
|
|
function operationWithRangeVectorRenderer(
|
|
model: QueryBuilderOperation,
|
|
def: QueryBuilderOperationDef,
|
|
innerExpr: string
|
|
) {
|
|
const params = model.params ?? [];
|
|
const rangeVector = params[0] ?? '$__auto';
|
|
// QuantileOverTime is only range vector with more than one param
|
|
if (params.length === 2 && model.id === LokiOperationId.QuantileOverTime) {
|
|
const quantile = params[1];
|
|
return `${model.id}(${quantile}, ${innerExpr} [${rangeVector}])`;
|
|
}
|
|
|
|
return `${model.id}(${innerExpr} [${params[0] ?? '$__auto'}])`;
|
|
}
|
|
|
|
export function labelFilterRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
|
|
const integerOperators = ['<', '<=', '>', '>='];
|
|
|
|
if (integerOperators.includes(String(model.params[1]))) {
|
|
return `${innerExpr} | ${model.params[0]} ${model.params[1]} ${model.params[2]}`;
|
|
}
|
|
|
|
return `${innerExpr} | ${model.params[0]} ${model.params[1]} \`${model.params[2]}\``;
|
|
}
|
|
|
|
export function isConflictingFilter(
|
|
operation: QueryBuilderOperation,
|
|
queryOperations: QueryBuilderOperation[]
|
|
): boolean {
|
|
const operationIsNegative = operation.params[1].toString().startsWith('!');
|
|
|
|
const candidates = queryOperations.filter(
|
|
(queryOperation) =>
|
|
queryOperation.id === LokiOperationId.LabelFilter &&
|
|
queryOperation.params[0] === operation.params[0] &&
|
|
queryOperation.params[2] === operation.params[2]
|
|
);
|
|
|
|
const conflict = candidates.some((candidate) => {
|
|
if (operationIsNegative && candidate.params[1].toString().startsWith('!') === false) {
|
|
return true;
|
|
}
|
|
if (operationIsNegative === false && candidate.params[1].toString().startsWith('!')) {
|
|
return true;
|
|
}
|
|
return false;
|
|
});
|
|
|
|
return conflict;
|
|
}
|
|
|
|
export function pipelineRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
|
|
switch (model.id) {
|
|
case LokiOperationId.Logfmt:
|
|
const [strict = false, keepEmpty = false, ...labels] = model.params;
|
|
return `${innerExpr} | logfmt${strict ? ' --strict' : ''}${keepEmpty ? ' --keep-empty' : ''} ${labels.join(
|
|
', '
|
|
)}`.trim();
|
|
default:
|
|
return `${innerExpr} | ${model.id}`;
|
|
}
|
|
}
|
|
|
|
function isRangeVectorFunction(def: QueryBuilderOperationDef) {
|
|
return def.category === LokiVisualQueryOperationCategory.RangeFunctions;
|
|
}
|
|
|
|
function getIndexOfOrLast(
|
|
operations: QueryBuilderOperation[],
|
|
queryModeller: VisualQueryModeller,
|
|
condition: (def: QueryBuilderOperationDef) => boolean
|
|
) {
|
|
const index = operations.findIndex((x) => {
|
|
const opDef = queryModeller.getOperationDef(x.id);
|
|
if (!opDef) {
|
|
return false;
|
|
}
|
|
return condition(opDef);
|
|
});
|
|
|
|
return index === -1 ? operations.length : index;
|
|
}
|
|
|
|
export function addLokiOperation(
|
|
def: QueryBuilderOperationDef,
|
|
query: LokiVisualQuery,
|
|
modeller: VisualQueryModeller
|
|
): LokiVisualQuery {
|
|
const newOperation: QueryBuilderOperation = {
|
|
id: def.id,
|
|
params: def.defaultParams,
|
|
};
|
|
|
|
const operations = [...query.operations];
|
|
|
|
const existingRangeVectorFunction = operations.find((x) => {
|
|
const opDef = modeller.getOperationDef(x.id);
|
|
if (!opDef) {
|
|
return false;
|
|
}
|
|
return isRangeVectorFunction(opDef);
|
|
});
|
|
|
|
switch (def.category) {
|
|
case LokiVisualQueryOperationCategory.Aggregations:
|
|
case LokiVisualQueryOperationCategory.Functions:
|
|
// If we are adding a function but we have not range vector function yet add one
|
|
if (!existingRangeVectorFunction) {
|
|
const placeToInsert = getIndexOfOrLast(
|
|
operations,
|
|
modeller,
|
|
(def) => def.category === LokiVisualQueryOperationCategory.Functions
|
|
);
|
|
operations.splice(placeToInsert, 0, { id: LokiOperationId.Rate, params: ['$__auto'] });
|
|
}
|
|
operations.push(newOperation);
|
|
break;
|
|
case LokiVisualQueryOperationCategory.RangeFunctions:
|
|
// If adding a range function and range function is already added replace it
|
|
if (existingRangeVectorFunction) {
|
|
const index = operations.indexOf(existingRangeVectorFunction);
|
|
operations[index] = newOperation;
|
|
break;
|
|
}
|
|
|
|
// Add range functions after any formats, line filters and label filters
|
|
default:
|
|
const placeToInsert = getIndexOfOrLast(
|
|
operations,
|
|
modeller,
|
|
(x) => (def.orderRank ?? 100) < (x.orderRank ?? 100)
|
|
);
|
|
operations.splice(placeToInsert, 0, newOperation);
|
|
break;
|
|
}
|
|
|
|
return {
|
|
...query,
|
|
operations,
|
|
};
|
|
}
|
|
|
|
export function addNestedQueryHandler(def: QueryBuilderOperationDef, query: LokiVisualQuery): LokiVisualQuery {
|
|
return {
|
|
...query,
|
|
binaryQueries: [
|
|
...(query.binaryQueries ?? []),
|
|
{
|
|
operator: '/',
|
|
query,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
|
|
export function getLineFilterRenderer(operation: string, caseInsensitive?: boolean) {
|
|
return function lineFilterRenderer(model: QueryBuilderOperation, def: QueryBuilderOperationDef, innerExpr: string) {
|
|
if (caseInsensitive) {
|
|
return `${innerExpr} ${operation} \`(?i)${model.params[0]}\``;
|
|
}
|
|
return `${innerExpr} ${operation} \`${model.params[0]}\``;
|
|
};
|
|
}
|
|
|
|
function getRangeVectorParamDef(): QueryBuilderOperationParamDef {
|
|
return {
|
|
name: 'Range',
|
|
type: 'string',
|
|
options: ['$__auto', '1m', '5m', '10m', '1h', '24h'],
|
|
};
|
|
}
|