mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
Improve KQL error messages (#34900)
Attempts to make KQL syntax errors more sensical to the average user. I initially tried to use a similar solution to the one we used for detecting usage of old lucene syntax. In other words, I tried to create rules in the grammar that would match strings containing common mistakes the user might make and throw custom error messages for each situation. This proved to be more difficult for detecting errors in the regular language. While the Lucene rules could be completely separated from the main grammar, the KQL error rules had to be mixed into the main grammar which made it much more complex and had a lot of unintended side effects. So instead I decided to lean more heavily on PEG's built in error reporting. Giving certain rules human readable names allows the parser to use those names in the error reporting instead of auto generating a long list of possible characters that might be expected based on the matching rules. Since the PEG errors contain location information I was also able to add ascii art that points the user to exactly where the error occurred in their query string. While this approach is not quite as nice as bespoke error messages that tell the user exactly what is wrong in plain English, it's much more maintainable and I think it still results in much better error messages compared to what we have today. I've also removed the old original kuery grammar (for queries like is(response, 200)). We were only using it to display an error if I user was still using the old syntax. This version of kuery hasn't existed since 6.3 and we've had error messages telling users this since then. I think it's safe to remove the legacy parser at this point, which greatly reduces the complexity of our error reporting.
This commit is contained in:
parent
40fad359c0
commit
539bc6f3a2
21 changed files with 600 additions and 2250 deletions
|
@ -20,7 +20,6 @@ bower_components
|
|||
/packages/*/target
|
||||
/packages/eslint-config-kibana
|
||||
/packages/kbn-es-query/src/kuery/ast/kuery.js
|
||||
/packages/kbn-es-query/src/kuery/ast/legacy_kuery.js
|
||||
/packages/kbn-pm/dist
|
||||
/packages/kbn-plugin-generator/sao_template/template
|
||||
/packages/kbn-ui-framework/dist
|
||||
|
|
|
@ -20,6 +20,7 @@
|
|||
"timelion": "src/legacy/core_plugins/timelion",
|
||||
"tagCloud": "src/legacy/core_plugins/tagcloud",
|
||||
"tsvb": "src/legacy/core_plugins/metrics",
|
||||
"kbnESQuery": "packages/kbn-es-query",
|
||||
"xpack.apm": "x-pack/plugins/apm",
|
||||
"xpack.beatsManagement": "x-pack/plugins/beats_management",
|
||||
"xpack.crossClusterReplication": "x-pack/plugins/cross_cluster_replication",
|
||||
|
|
|
@ -76,8 +76,6 @@ Creates a filter (`RangeFilter`) where the value for the given field is in the g
|
|||
|
||||
This folder contains the code corresponding to generating Elasticsearch queries using the Kibana query language.
|
||||
|
||||
It also contains code corresponding to the original implementation of Kuery (released in 6.0) which should be removed at some point (see legacy_kuery.js, legacy_kuery.peg).
|
||||
|
||||
In general, you will only need to worry about the following functions from the `ast` folder:
|
||||
|
||||
```javascript
|
||||
|
|
|
@ -12,7 +12,8 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"lodash": "npm:@elastic/lodash@3.10.1-kibana1",
|
||||
"moment-timezone": "^0.5.14"
|
||||
"moment-timezone": "^0.5.14",
|
||||
"@kbn/i18n": "1.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.2.3",
|
||||
|
|
|
@ -52,15 +52,6 @@ describe('build query', function () {
|
|||
expect(result.filter).to.eql(expectedESQueries);
|
||||
});
|
||||
|
||||
it('should throw a useful error if it looks like query is using an old, unsupported syntax', function () {
|
||||
const oldQuery = { query: 'is(foo, bar)', language: 'kuery' };
|
||||
|
||||
expect(buildQueryFromKuery).withArgs(indexPattern, [oldQuery], true).to.throwError(
|
||||
/OutdatedKuerySyntaxError/
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it('should accept a specific date format for a kuery query into an ES query in the bool\'s filter clause', function () {
|
||||
const queries = [{ query: '@timestamp:"2018-04-03T19:04:17"', language: 'kuery' }];
|
||||
|
||||
|
|
|
@ -17,20 +17,15 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
import { fromLegacyKueryExpression, fromKueryExpression, toElasticsearchQuery, nodeTypes } from '../kuery';
|
||||
import {
|
||||
fromKueryExpression,
|
||||
toElasticsearchQuery,
|
||||
nodeTypes,
|
||||
} from '../kuery';
|
||||
|
||||
export function buildQueryFromKuery(indexPattern, queries = [], allowLeadingWildcards, dateFormatTZ = null) {
|
||||
const queryASTs = queries.map(query => {
|
||||
try {
|
||||
return fromKueryExpression(query.query, { allowLeadingWildcards });
|
||||
} catch (parseError) {
|
||||
try {
|
||||
fromLegacyKueryExpression(query.query);
|
||||
} catch (legacyParseError) {
|
||||
throw parseError;
|
||||
}
|
||||
throw Error('OutdatedKuerySyntaxError');
|
||||
}
|
||||
return fromKueryExpression(query.query, { allowLeadingWildcards });
|
||||
});
|
||||
return buildQuery(indexPattern, queryASTs, { dateFormatTZ });
|
||||
}
|
||||
|
|
|
@ -22,13 +22,6 @@ import expect from '@kbn/expect';
|
|||
import { nodeTypes } from '../../node_types/index';
|
||||
import indexPatternResponse from '../../../__fixtures__/index_pattern_response.json';
|
||||
|
||||
// Helpful utility allowing us to test the PEG parser by simply checking for deep equality between
|
||||
// the nodes the parser generates and the nodes our constructor functions generate.
|
||||
|
||||
function fromLegacyKueryExpressionNoMeta(text) {
|
||||
return ast.fromLegacyKueryExpression(text, { includeMetadata: false });
|
||||
}
|
||||
|
||||
let indexPattern;
|
||||
|
||||
describe('kuery AST API', function () {
|
||||
|
@ -38,153 +31,6 @@ describe('kuery AST API', function () {
|
|||
indexPattern = indexPatternResponse;
|
||||
});
|
||||
|
||||
describe('fromLegacyKueryExpression', function () {
|
||||
|
||||
it('should return location and text metadata for each AST node', function () {
|
||||
const notNode = ast.fromLegacyKueryExpression('!foo:bar');
|
||||
expect(notNode).to.have.property('text', '!foo:bar');
|
||||
expect(notNode.location).to.eql({ min: 0, max: 8 });
|
||||
|
||||
const isNode = notNode.arguments[0];
|
||||
expect(isNode).to.have.property('text', 'foo:bar');
|
||||
expect(isNode.location).to.eql({ min: 1, max: 8 });
|
||||
|
||||
const { arguments: [ argNode1, argNode2 ] } = isNode;
|
||||
expect(argNode1).to.have.property('text', 'foo');
|
||||
expect(argNode1.location).to.eql({ min: 1, max: 4 });
|
||||
|
||||
expect(argNode2).to.have.property('text', 'bar');
|
||||
expect(argNode2.location).to.eql({ min: 5, max: 8 });
|
||||
});
|
||||
|
||||
it('should return a match all "is" function for whitespace', function () {
|
||||
const expected = nodeTypes.function.buildNode('is', '*', '*');
|
||||
const actual = fromLegacyKueryExpressionNoMeta(' ');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should return an "and" function for single literals', function () {
|
||||
const expected = nodeTypes.function.buildNode('and', [nodeTypes.literal.buildNode('foo')]);
|
||||
const actual = fromLegacyKueryExpressionNoMeta('foo');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should ignore extraneous whitespace at the beginning and end of the query', function () {
|
||||
const expected = nodeTypes.function.buildNode('and', [nodeTypes.literal.buildNode('foo')]);
|
||||
const actual = fromLegacyKueryExpressionNoMeta(' foo ');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('literals and queries separated by whitespace should be joined by an implicit "and"', function () {
|
||||
const expected = nodeTypes.function.buildNode('and', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
]);
|
||||
const actual = fromLegacyKueryExpressionNoMeta('foo bar');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should also support explicit "and"s as a binary operator', function () {
|
||||
const expected = nodeTypes.function.buildNode('and', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
]);
|
||||
const actual = fromLegacyKueryExpressionNoMeta('foo and bar');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should also support "and" as a function', function () {
|
||||
const expected = nodeTypes.function.buildNode('and', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
], 'function');
|
||||
const actual = fromLegacyKueryExpressionNoMeta('and(foo, bar)');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should support "or" as a binary operator', function () {
|
||||
const expected = nodeTypes.function.buildNode('or', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
]);
|
||||
const actual = fromLegacyKueryExpressionNoMeta('foo or bar');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should support "or" as a function', function () {
|
||||
const expected = nodeTypes.function.buildNode('or', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
]);
|
||||
const actual = fromLegacyKueryExpressionNoMeta('or(foo, bar)');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should support negation of queries with a "!" prefix', function () {
|
||||
const expected = nodeTypes.function.buildNode('not',
|
||||
nodeTypes.function.buildNode('or', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
]));
|
||||
const actual = fromLegacyKueryExpressionNoMeta('!or(foo, bar)');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('"and" should have a higher precedence than "or"', function () {
|
||||
const expected = nodeTypes.function.buildNode('or', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.function.buildNode('or', [
|
||||
nodeTypes.function.buildNode('and', [
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
nodeTypes.literal.buildNode('baz'),
|
||||
]),
|
||||
nodeTypes.literal.buildNode('qux'),
|
||||
])
|
||||
]);
|
||||
const actual = fromLegacyKueryExpressionNoMeta('foo or bar and baz or qux');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should support grouping to override default precedence', function () {
|
||||
const expected = nodeTypes.function.buildNode('and', [
|
||||
nodeTypes.function.buildNode('or', [
|
||||
nodeTypes.literal.buildNode('foo'),
|
||||
nodeTypes.literal.buildNode('bar'),
|
||||
]),
|
||||
nodeTypes.literal.buildNode('baz'),
|
||||
]);
|
||||
const actual = fromLegacyKueryExpressionNoMeta('(foo or bar) and baz');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should support a shorthand operator syntax for "is" functions', function () {
|
||||
const expected = nodeTypes.function.buildNode('is', 'foo', 'bar', true);
|
||||
const actual = fromLegacyKueryExpressionNoMeta('foo:bar');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should support a shorthand operator syntax for inclusive "range" functions', function () {
|
||||
const argumentNodes = [
|
||||
nodeTypes.literal.buildNode('bytes'),
|
||||
nodeTypes.literal.buildNode(1000),
|
||||
nodeTypes.literal.buildNode(8000),
|
||||
];
|
||||
const expected = nodeTypes.function.buildNodeWithArgumentNodes('range', argumentNodes);
|
||||
const actual = fromLegacyKueryExpressionNoMeta('bytes:[1000 to 8000]');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should support functions with named arguments', function () {
|
||||
const expected = nodeTypes.function.buildNode('range', 'bytes', { gt: 1000, lt: 8000 });
|
||||
const actual = fromLegacyKueryExpressionNoMeta('range(bytes, gt=1000, lt=8000)');
|
||||
expect(actual).to.eql(expected);
|
||||
});
|
||||
|
||||
it('should throw an error for unknown functions', function () {
|
||||
expect(ast.fromLegacyKueryExpression).withArgs('foo(bar)').to.throwException(/Unknown function "foo"/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fromKueryExpression', function () {
|
||||
|
||||
it('should return a match all "is" function for whitespace', function () {
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
import _ from 'lodash';
|
||||
import { nodeTypes } from '../node_types/index';
|
||||
import { parse as parseKuery } from './kuery';
|
||||
import { parse as parseLegacyKuery } from './legacy_kuery';
|
||||
import { KQLSyntaxError } from '../errors';
|
||||
|
||||
export function fromLiteralExpression(expression, parseOptions) {
|
||||
parseOptions = {
|
||||
|
@ -31,12 +31,16 @@ export function fromLiteralExpression(expression, parseOptions) {
|
|||
return fromExpression(expression, parseOptions, parseKuery);
|
||||
}
|
||||
|
||||
export function fromLegacyKueryExpression(expression, parseOptions) {
|
||||
return fromExpression(expression, parseOptions, parseLegacyKuery);
|
||||
}
|
||||
|
||||
export function fromKueryExpression(expression, parseOptions) {
|
||||
return fromExpression(expression, parseOptions, parseKuery);
|
||||
try {
|
||||
return fromExpression(expression, parseOptions, parseKuery);
|
||||
} catch (error) {
|
||||
if (error.name === 'SyntaxError') {
|
||||
throw new KQLSyntaxError(error, expression);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function fromExpression(expression, parseOptions = {}, parse = parseKuery) {
|
||||
|
@ -46,11 +50,12 @@ function fromExpression(expression, parseOptions = {}, parse = parseKuery) {
|
|||
|
||||
parseOptions = {
|
||||
...parseOptions,
|
||||
helpers: { nodeTypes }
|
||||
helpers: { nodeTypes },
|
||||
};
|
||||
|
||||
return parse(expression, parseOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* @params {String} indexPattern
|
||||
* @params {Object} config - contains the dateFormatTZ
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -66,8 +66,11 @@ Expression
|
|||
/ FieldValueExpression
|
||||
/ ValueExpression
|
||||
|
||||
Field "fieldName"
|
||||
= Literal
|
||||
|
||||
FieldRangeExpression
|
||||
= field:Literal Space* operator:RangeOperator Space* value:Literal {
|
||||
= field:Field Space* operator:RangeOperator Space* value:Literal {
|
||||
if (value.type === 'cursor') {
|
||||
return {
|
||||
...value,
|
||||
|
@ -79,7 +82,7 @@ FieldRangeExpression
|
|||
}
|
||||
|
||||
FieldValueExpression
|
||||
= field:Literal Space* ':' Space* partial:ListOfValues {
|
||||
= field:Field Space* ':' Space* partial:ListOfValues {
|
||||
if (partial.type === 'cursor') {
|
||||
return {
|
||||
...partial,
|
||||
|
@ -154,7 +157,7 @@ NotListOfValues
|
|||
}
|
||||
/ ListOfValues
|
||||
|
||||
Value
|
||||
Value "value"
|
||||
= value:QuotedString {
|
||||
if (value.type === 'cursor') return value;
|
||||
const isPhrase = buildLiteralNode(true);
|
||||
|
@ -171,19 +174,19 @@ Value
|
|||
return (field) => buildFunctionNode('is', [field, value, isPhrase]);
|
||||
}
|
||||
|
||||
Or
|
||||
Or "OR"
|
||||
= Space+ 'or'i Space+
|
||||
/ &{ return errorOnLuceneSyntax; } LuceneOr
|
||||
|
||||
And
|
||||
And "AND"
|
||||
= Space+ 'and'i Space+
|
||||
/ &{ return errorOnLuceneSyntax; } LuceneAnd
|
||||
|
||||
Not
|
||||
Not "NOT"
|
||||
= 'not'i Space+
|
||||
/ &{ return errorOnLuceneSyntax; } LuceneNot
|
||||
|
||||
Literal
|
||||
Literal "literal"
|
||||
= QuotedString / UnquotedLiteral
|
||||
|
||||
QuotedString
|
||||
|
@ -277,7 +280,7 @@ RangeOperator
|
|||
/ '<' { return 'lt'; }
|
||||
/ '>' { return 'gt'; }
|
||||
|
||||
Space
|
||||
Space "whitespace"
|
||||
= [\ \t\r\n]
|
||||
|
||||
Cursor
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,153 +0,0 @@
|
|||
/*
|
||||
* Kuery parser
|
||||
*
|
||||
* To generate the parsing module (legacy_kuery.js), run `grunt peg`
|
||||
* To watch changes and generate on file change, run `grunt watch:peg`
|
||||
*/
|
||||
|
||||
/*
|
||||
* Initialization block
|
||||
*/
|
||||
{
|
||||
var nodeTypes = options.helpers.nodeTypes;
|
||||
|
||||
if (options.includeMetadata === undefined) {
|
||||
options.includeMetadata = true;
|
||||
}
|
||||
|
||||
function addMeta(source, text, location) {
|
||||
if (options.includeMetadata) {
|
||||
return Object.assign(
|
||||
{},
|
||||
source,
|
||||
{
|
||||
text: text,
|
||||
location: simpleLocation(location),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return source;
|
||||
}
|
||||
|
||||
function simpleLocation(location) {
|
||||
// Returns an object representing the position of the function within the expression,
|
||||
// demarcated by the position of its first character and last character. We calculate these values
|
||||
// using the offset because the expression could span multiple lines, and we don't want to deal
|
||||
// with column and line values.
|
||||
return {
|
||||
min: location.start.offset,
|
||||
max: location.end.offset
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
start
|
||||
= space? query:OrQuery space? {
|
||||
if (query.type === 'literal') {
|
||||
return addMeta(nodeTypes.function.buildNode('and', [query]), text(), location());
|
||||
}
|
||||
return query;
|
||||
}
|
||||
/ whitespace:[\ \t\r\n]* {
|
||||
return addMeta(nodeTypes.function.buildNode('is', '*', '*', false), text(), location());
|
||||
}
|
||||
|
||||
OrQuery
|
||||
= left:AndQuery space 'or'i space right:OrQuery {
|
||||
return addMeta(nodeTypes.function.buildNode('or', [left, right]), text(), location());
|
||||
}
|
||||
/ AndQuery
|
||||
|
||||
AndQuery
|
||||
= left:NegatedClause space 'and'i space right:AndQuery {
|
||||
return addMeta(nodeTypes.function.buildNode('and', [left, right]), text(), location());
|
||||
}
|
||||
/ left:NegatedClause space !'or'i right:AndQuery {
|
||||
return addMeta(nodeTypes.function.buildNode('and', [left, right]), text(), location());
|
||||
}
|
||||
/ NegatedClause
|
||||
|
||||
NegatedClause
|
||||
= [!] clause:Clause {
|
||||
return addMeta(nodeTypes.function.buildNode('not', clause), text(), location());
|
||||
}
|
||||
/ Clause
|
||||
|
||||
Clause
|
||||
= '(' subQuery:start ')' {
|
||||
return subQuery;
|
||||
}
|
||||
/ Term
|
||||
|
||||
Term
|
||||
= field:literal_arg_type ':' value:literal_arg_type {
|
||||
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes('is', [field, value, nodeTypes.literal.buildNode(true)]), text(), location());
|
||||
}
|
||||
/ field:literal_arg_type ':[' space? gt:literal_arg_type space 'to'i space lt:literal_arg_type space? ']' {
|
||||
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes('range', [field, gt, lt]), text(), location());
|
||||
}
|
||||
/ function
|
||||
/ !Keywords literal:literal_arg_type { return literal; }
|
||||
|
||||
function_name
|
||||
= first:[a-zA-Z]+ rest:[.a-zA-Z0-9_-]* { return first.join('') + rest.join('') }
|
||||
|
||||
function "function"
|
||||
= name:function_name space? '(' space? arg_list:arg_list? space? ')' {
|
||||
return addMeta(nodeTypes.function.buildNodeWithArgumentNodes(name, arg_list || []), text(), location());
|
||||
}
|
||||
|
||||
arg_list
|
||||
= first:argument rest:(space? ',' space? arg:argument {return arg})* space? ','? {
|
||||
return [first].concat(rest);
|
||||
}
|
||||
|
||||
argument
|
||||
= name:function_name space? '=' space? value:arg_type {
|
||||
return addMeta(nodeTypes.namedArg.buildNode(name, value), text(), location());
|
||||
}
|
||||
/ element:arg_type {return element}
|
||||
|
||||
arg_type
|
||||
= OrQuery
|
||||
/ literal_arg_type
|
||||
|
||||
literal_arg_type
|
||||
= literal:literal {
|
||||
var result = addMeta(nodeTypes.literal.buildNode(literal), text(), location());
|
||||
return result;
|
||||
}
|
||||
|
||||
Keywords
|
||||
= 'and'i / 'or'i
|
||||
|
||||
/* ----- Core types ----- */
|
||||
|
||||
literal "literal"
|
||||
= '"' chars:dq_char* '"' { return chars.join(''); } // double quoted string
|
||||
/ "'" chars:sq_char* "'" { return chars.join(''); } // single quoted string
|
||||
/ 'true' { return true; } // unquoted literals from here down
|
||||
/ 'false' { return false; }
|
||||
/ 'null' { return null; }
|
||||
/ string:[^\[\]()"',:=\ \t]+ { // this also matches numbers via Number()
|
||||
var result = string.join('');
|
||||
// Sort of hacky, but PEG doesn't have backtracking so
|
||||
// a number rule is hard to read, and performs worse
|
||||
if (isNaN(Number(result))) return result;
|
||||
return Number(result)
|
||||
}
|
||||
|
||||
space
|
||||
= [\ \t\r\n]+
|
||||
|
||||
dq_char
|
||||
= "\\" sequence:('"' / "\\") { return sequence; }
|
||||
/ [^"] // everything except "
|
||||
|
||||
sq_char
|
||||
= "\\" sequence:("'" / "\\") { return sequence; }
|
||||
/ [^'] // everything except '
|
||||
|
||||
integer
|
||||
= digits:[0-9]+ {return parseInt(digits.join(''))}
|
69
packages/kbn-es-query/src/kuery/errors/index.js
Normal file
69
packages/kbn-es-query/src/kuery/errors/index.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { repeat } from 'lodash';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
const endOfInputText = i18n.translate('kbnESQuery.kql.errors.endOfInputText', {
|
||||
defaultMessage: 'end of input',
|
||||
});
|
||||
|
||||
export class KQLSyntaxError extends Error {
|
||||
|
||||
constructor(error, expression) {
|
||||
const grammarRuleTranslations = {
|
||||
fieldName: i18n.translate('kbnESQuery.kql.errors.fieldNameText', {
|
||||
defaultMessage: 'field name',
|
||||
}),
|
||||
value: i18n.translate('kbnESQuery.kql.errors.valueText', {
|
||||
defaultMessage: 'value',
|
||||
}),
|
||||
literal: i18n.translate('kbnESQuery.kql.errors.literalText', {
|
||||
defaultMessage: 'literal',
|
||||
}),
|
||||
whitespace: i18n.translate('kbnESQuery.kql.errors.whitespaceText', {
|
||||
defaultMessage: 'whitespace',
|
||||
}),
|
||||
};
|
||||
|
||||
const translatedExpectations = error.expected.map((expected) => {
|
||||
return grammarRuleTranslations[expected.description] || expected.description;
|
||||
});
|
||||
|
||||
const translatedExpectationText = translatedExpectations.join(', ');
|
||||
|
||||
const message = i18n.translate('kbnESQuery.kql.errors.syntaxError', {
|
||||
defaultMessage: 'Expected {expectedList} but {foundInput} found.',
|
||||
values: {
|
||||
expectedList: translatedExpectationText,
|
||||
foundInput: error.found ? `"${error.found}"` : endOfInputText,
|
||||
},
|
||||
});
|
||||
|
||||
const fullMessage = [
|
||||
message,
|
||||
expression,
|
||||
repeat('-', error.location.start.offset) + '^',
|
||||
].join('\n');
|
||||
|
||||
super(fullMessage);
|
||||
this.name = 'KQLSyntaxError';
|
||||
this.shortMessage = message;
|
||||
}
|
||||
}
|
105
packages/kbn-es-query/src/kuery/errors/index.test.js
Normal file
105
packages/kbn-es-query/src/kuery/errors/index.test.js
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* Licensed to Elasticsearch B.V. under one or more contributor
|
||||
* license agreements. See the NOTICE file distributed with
|
||||
* this work for additional information regarding copyright
|
||||
* ownership. Elasticsearch B.V. licenses this file to you under
|
||||
* the Apache License, Version 2.0 (the "License"); you may
|
||||
* not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { fromKueryExpression } from '../ast';
|
||||
|
||||
|
||||
describe('kql syntax errors', () => {
|
||||
|
||||
it('should throw an error for a field query missing a value', () => {
|
||||
expect(() => {
|
||||
fromKueryExpression('response:');
|
||||
}).toThrow('Expected "(", value, whitespace but end of input found.\n' +
|
||||
'response:\n' +
|
||||
'---------^');
|
||||
});
|
||||
|
||||
it('should throw an error for an OR query missing a right side sub-query', () => {
|
||||
expect(() => {
|
||||
fromKueryExpression('response:200 or ');
|
||||
}).toThrow('Expected "(", NOT, field name, value but end of input found.\n' +
|
||||
'response:200 or \n' +
|
||||
'----------------^');
|
||||
});
|
||||
|
||||
it('should throw an error for an OR list of values missing a right side sub-query', () => {
|
||||
expect(() => {
|
||||
fromKueryExpression('response:(200 or )');
|
||||
}).toThrow('Expected "(", NOT, value but ")" found.\n' +
|
||||
'response:(200 or )\n' +
|
||||
'-----------------^');
|
||||
});
|
||||
|
||||
it('should throw an error for a NOT query missing a sub-query', () => {
|
||||
expect(() => {
|
||||
fromKueryExpression('response:200 and not ');
|
||||
}).toThrow('Expected "(", field name, value but end of input found.\n' +
|
||||
'response:200 and not \n' +
|
||||
'---------------------^');
|
||||
});
|
||||
|
||||
it('should throw an error for a NOT list missing a sub-query', () => {
|
||||
expect(() => {
|
||||
fromKueryExpression('response:(200 and not )');
|
||||
}).toThrow('Expected "(", value but ")" found.\n' +
|
||||
'response:(200 and not )\n' +
|
||||
'----------------------^');
|
||||
});
|
||||
|
||||
it('should throw an error for unbalanced quotes', () => {
|
||||
expect(() => {
|
||||
fromKueryExpression('foo:"ba ');
|
||||
}).toThrow('Expected "(", value, whitespace but "\"" found.\n' +
|
||||
'foo:"ba \n' +
|
||||
'----^');
|
||||
});
|
||||
|
||||
it('should throw an error for unescaped quotes in a quoted string', () => {
|
||||
expect(() => {
|
||||
fromKueryExpression('foo:"ba "r"');
|
||||
}).toThrow('Expected AND, OR, end of input, whitespace but "r" found.\n' +
|
||||
'foo:"ba "r"\n' +
|
||||
'---------^');
|
||||
});
|
||||
|
||||
it('should throw an error for unescaped special characters in literals', () => {
|
||||
expect(() => {
|
||||
fromKueryExpression('foo:ba:r');
|
||||
}).toThrow('Expected AND, OR, end of input, whitespace but ":" found.\n' +
|
||||
'foo:ba:r\n' +
|
||||
'------^');
|
||||
});
|
||||
|
||||
it('should throw an error for range queries missing a value', () => {
|
||||
expect(() => {
|
||||
fromKueryExpression('foo > ');
|
||||
}).toThrow('Expected literal, whitespace but end of input found.\n' +
|
||||
'foo > \n' +
|
||||
'------^');
|
||||
});
|
||||
|
||||
it('should throw an error for range queries missing a field', () => {
|
||||
expect(() => {
|
||||
fromKueryExpression('< 1000');
|
||||
}).toThrow('Expected "(", NOT, end of input, field name, value, whitespace but "<" found.\n' +
|
||||
'< 1000\n' +
|
||||
'^');
|
||||
});
|
||||
|
||||
});
|
|
@ -20,3 +20,4 @@
|
|||
export * from './ast';
|
||||
export * from './filter_migration';
|
||||
export * from './node_types';
|
||||
export * from './errors';
|
||||
|
|
|
@ -81,7 +81,6 @@ import { searchRequestQueue } from '../search_request_queue';
|
|||
import { FetchSoonProvider } from '../fetch';
|
||||
import { FieldWildcardProvider } from '../../field_wildcard';
|
||||
import { getHighlightRequest } from '../../../../core_plugins/kibana/common/highlight';
|
||||
import { KbnError, OutdatedKuerySyntaxError } from '../../errors';
|
||||
|
||||
const FIELDS = [
|
||||
'type',
|
||||
|
@ -580,15 +579,8 @@ export function SearchSourceProvider(Promise, Private, config) {
|
|||
_.set(flatData.body, '_source.includes', remainingFields);
|
||||
}
|
||||
|
||||
try {
|
||||
const esQueryConfigs = getEsQueryConfig(config);
|
||||
flatData.body.query = buildEsQuery(flatData.index, flatData.query, flatData.filters, esQueryConfigs);
|
||||
} catch (e) {
|
||||
if (e.message === 'OutdatedKuerySyntaxError') {
|
||||
throw new OutdatedKuerySyntaxError();
|
||||
}
|
||||
throw new KbnError(e.message, KbnError);
|
||||
}
|
||||
const esQueryConfigs = getEsQueryConfig(config);
|
||||
flatData.body.query = buildEsQuery(flatData.index, flatData.query, flatData.filters, esQueryConfigs);
|
||||
|
||||
if (flatData.highlightAll != null) {
|
||||
if (flatData.highlightAll && flatData.body.query) {
|
||||
|
|
|
@ -19,7 +19,6 @@
|
|||
|
||||
import angular from 'angular';
|
||||
import { createLegacyClass } from './utils/legacy_class';
|
||||
import { documentationLinks } from './documentation_links';
|
||||
|
||||
const canStack = (function () {
|
||||
const err = new Error();
|
||||
|
@ -290,10 +289,3 @@ export class NoResults extends VislibError {
|
|||
super('No results found');
|
||||
}
|
||||
}
|
||||
|
||||
export class OutdatedKuerySyntaxError extends KbnError {
|
||||
constructor() {
|
||||
const link = `[docs](${documentationLinks.query.kueryQuerySyntax})`;
|
||||
super(`It looks like you're using an outdated Kuery syntax. See what changed in the ${link}!`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -69,6 +69,7 @@ export function formatMsg(err, source) {
|
|||
|
||||
formatMsg.describeError = function (err) {
|
||||
if (!err) return undefined;
|
||||
if (err.shortMessage) return err.shortMessage;
|
||||
if (err.body && err.body.message) return err.body.message;
|
||||
if (err.message) return err.message;
|
||||
return '' + err;
|
||||
|
|
|
@ -38,14 +38,14 @@ export function fromUser(userInput: object | string) {
|
|||
|
||||
userInput = userInput || '';
|
||||
if (typeof userInput === 'string') {
|
||||
userInput = userInput.trim();
|
||||
if (userInput.length === 0) {
|
||||
const trimmedUserInput = userInput.trim();
|
||||
if (trimmedUserInput.length === 0) {
|
||||
return matchAll;
|
||||
}
|
||||
|
||||
if (userInput[0] === '{') {
|
||||
if (trimmedUserInput[0] === '{') {
|
||||
try {
|
||||
return JSON.parse(userInput);
|
||||
return JSON.parse(trimmedUserInput);
|
||||
} catch (e) {
|
||||
return userInput;
|
||||
}
|
||||
|
|
|
@ -18,10 +18,6 @@
|
|||
*/
|
||||
|
||||
module.exports = {
|
||||
legacyKuery: {
|
||||
src: 'packages/kbn-es-query/src/kuery/ast/legacy_kuery.peg',
|
||||
dest: 'packages/kbn-es-query/src/kuery/ast/legacy_kuery.js'
|
||||
},
|
||||
kuery: {
|
||||
src: 'packages/kbn-es-query/src/kuery/ast/kuery.peg',
|
||||
dest: 'packages/kbn-es-query/src/kuery/ast/kuery.js',
|
||||
|
|
|
@ -93,8 +93,8 @@ export default function ({ getService, getPageObjects }) {
|
|||
});
|
||||
|
||||
it('a bad syntax query should show an error message', async function () {
|
||||
const expectedError = 'Discover: Expected "*", ":", "<", "<=", ">", ">=", "\\", "\\n", ' +
|
||||
'"\\r", "\\t", [\\ \\t\\r\\n] or end of input but "(" found.';
|
||||
const expectedError = 'Discover: Expected ":", "<", "<=", ">", ">=", AND, OR, end of input, ' +
|
||||
'whitespace but "(" found.';
|
||||
await queryBar.setQuery('xxx(yyy))');
|
||||
await queryBar.submitQuery();
|
||||
const toastMessage = await PageObjects.header.getToastMessage();
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue