Skip to content

Commit

Permalink
[KQL] Add util for getting field names from KQL expression (elastic#1…
Browse files Browse the repository at this point in the history
…83573)

## Summary

Resolves elastic#180555.

Adds a utility to kbn-es-query for getting the field names associated
with a KQL expression.

This utility already (mostly) existed in
x-pack/plugins/observability_solution/apm/common/utils/get_kuery_fields.ts
but didn't have test coverage for things like wildcards and nested
fields. This also updates the utility to be a little more robust in
checking the KQL node types.

### Checklist

- [ ]
[Documentation](https://www.elastic.co/guide/en/kibana/master/development-documentation.html)
was added for features that require explanation or tutorials
- [x] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: Matthew Kime <matt@mattki.me>
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
  • Loading branch information
3 people authored May 21, 2024
1 parent 53435ea commit 564dec5
Show file tree
Hide file tree
Showing 11 changed files with 159 additions and 117 deletions.
2 changes: 2 additions & 0 deletions packages/kbn-es-query/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ export {
toElasticsearchQuery,
escapeKuery,
escapeQuotes,
getKqlFieldNames,
getKqlFieldNamesFromExpression,
} from './src/kuery';

export {
Expand Down
7 changes: 6 additions & 1 deletion packages/kbn-es-query/src/kuery/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,10 @@ export const toElasticsearchQuery = (...params: Parameters<typeof astToElasticse
export { KQLSyntaxError } from './kuery_syntax_error';
export { nodeTypes, nodeBuilder } from './node_types';
export { fromKueryExpression, toKqlExpression } from './ast';
export { escapeKuery, escapeQuotes } from './utils';
export {
escapeKuery,
escapeQuotes,
getKqlFieldNames,
getKqlFieldNamesFromExpression,
} from './utils';
export type { DslQuery, KueryNode, KueryQueryOptions, KueryParseOptions } from './types';
84 changes: 84 additions & 0 deletions packages/kbn-es-query/src/kuery/utils/get_kql_fields.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { getKqlFieldNamesFromExpression } from './get_kql_fields';

describe('getKqlFieldNames', () => {
it('returns single kuery field', () => {
const expression = 'service.name: my-service';
expect(getKqlFieldNamesFromExpression(expression)).toEqual(['service.name']);
});

it('returns kuery fields with wildcard', () => {
const expression = 'service.name: *';
expect(getKqlFieldNamesFromExpression(expression)).toEqual(['service.name']);
});

it('returns multiple fields used AND operator', () => {
const expression = 'service.name: my-service AND service.environment: production';
expect(getKqlFieldNamesFromExpression(expression)).toEqual([
'service.name',
'service.environment',
]);
});

it('returns multiple kuery fields with OR operator', () => {
const expression = 'network.carrier.mcc: test or child.id: 33';
expect(getKqlFieldNamesFromExpression(expression)).toEqual(['network.carrier.mcc', 'child.id']);
});

it('returns multiple kuery fields with wildcard', () => {
const expression = 'network.carrier.mcc:* or child.id: *';
expect(getKqlFieldNamesFromExpression(expression)).toEqual(['network.carrier.mcc', 'child.id']);
});

it('returns single kuery fields with gt operator', () => {
const expression = 'transaction.duration.aggregate > 10';
expect(getKqlFieldNamesFromExpression(expression)).toEqual(['transaction.duration.aggregate']);
});

it('returns duplicate fields', () => {
const expression = 'service.name: my-service and service.name: my-service and trace.id: trace';
expect(getKqlFieldNamesFromExpression(expression)).toEqual([
'service.name',
'service.name',
'trace.id',
]);
});

it('returns multiple fields with multiple logical operators', () => {
const expression =
'(service.name:opbeans-* OR service.name:kibana) and (service.environment:production)';
expect(getKqlFieldNamesFromExpression(expression)).toEqual([
'service.name',
'service.name',
'service.environment',
]);
});

it('returns nested fields', () => {
const expression = 'user.names:{ first: "Alice" and last: "White" }';
expect(getKqlFieldNamesFromExpression(expression)).toEqual(['user.names']);
});

it('returns wildcard fields', () => {
const expression = 'server.*: kibana';
expect(getKqlFieldNamesFromExpression(expression)).toEqual(['server.*']);
});

// _field_caps doesn't allow escaped wildcards, so for now this behavior is what we want
it('returns escaped fields', () => {
const expression = 'server.\\*: kibana';
expect(getKqlFieldNamesFromExpression(expression)).toEqual(['server.*']);
});

it('do not return if kuery field is null', () => {
const expression = 'opbean';
expect(getKqlFieldNamesFromExpression(expression)).toEqual([]);
});
});
45 changes: 45 additions & 0 deletions packages/kbn-es-query/src/kuery/utils/get_kql_fields.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/

import { fromKueryExpression, KueryNode } from '../../..';
import { nodeTypes } from '../node_types';
import { functions } from '../functions';

export function getKqlFieldNamesFromExpression(expression: string): string[] {
const node = fromKueryExpression(expression);
return getKqlFieldNames(node);
}

export function getKqlFieldNames(node: KueryNode): string[] {
if (nodeTypes.function.isNode(node)) {
if (functions.and.isNode(node) || functions.or.isNode(node)) {
return node.arguments.reduce<string[]>((result, child) => {
return result.concat(getKqlFieldNames(child));
}, []);
} else if (
functions.not.isNode(node) ||
functions.exists.isNode(node) ||
functions.is.isNode(node) ||
functions.nested.isNode(node) ||
functions.range.isNode(node)
) {
// For each of these field types, we only need to look at the first argument to determine the fields
const [fieldNode] = node.arguments;
return getKqlFieldNames(fieldNode);
} else {
throw new Error(`KQL function ${node.function} not supported in getKqlFieldNames`);
}
} else if (nodeTypes.literal.isNode(node)) {
if (node.value === null) return [];
return [`${nodeTypes.literal.toElasticsearchQuery(node)}`];
} else if (nodeTypes.wildcard.isNode(node)) {
return [nodeTypes.wildcard.toElasticsearchQuery(node)];
} else {
throw new Error(`KQL node type ${node.type} not supported in getKqlFieldNames`);
}
}
1 change: 1 addition & 0 deletions packages/kbn-es-query/src/kuery/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
*/

export { escapeKuery, escapeQuotes } from './escape_kuery';
export { getKqlFieldNames, getKqlFieldNamesFromExpression } from './get_kql_fields';
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,8 @@
* 2.0.
*/

import { fromKueryExpression } from '@kbn/es-query';
import { getKqlFieldNamesFromExpression } from '@kbn/es-query';
import { i18n } from '@kbn/i18n';
import { getKueryFields } from './utils/get_kuery_fields';
import {
AGENT_NAME,
SERVICE_NAME,
Expand Down Expand Up @@ -51,7 +50,7 @@ export function validateServiceGroupKuery(kuery: string): {
message?: string;
} {
try {
const kueryFields = getKueryFields([fromKueryExpression(kuery)]);
const kueryFields = getKqlFieldNamesFromExpression(kuery);
const unsupportedKueryFields = kueryFields.filter((fieldName) => !isSupportedField(fieldName));
if (unsupportedKueryFields.length === 0) {
return { isValidFields: true, isValidSyntax: true };
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@
*/
import React, { useCallback, useEffect } from 'react';
import { i18n } from '@kbn/i18n';
import { Filter, fromKueryExpression, Query, TimeRange, toElasticsearchQuery } from '@kbn/es-query';
import {
Filter,
fromKueryExpression,
getKqlFieldNamesFromExpression,
Query,
TimeRange,
toElasticsearchQuery,
} from '@kbn/es-query';
import { useHistory, useLocation } from 'react-router-dom';
import deepEqual from 'fast-deep-equal';
import { useKibana } from '@kbn/kibana-react-plugin/public';
Expand All @@ -27,7 +34,6 @@ import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_
import { clearCache } from '../../../services/rest/call_api';
import { useTimeRangeId } from '../../../context/time_range_id/use_time_range_id';
import { toBoolean, toNumber } from '../../../context/url_params_context/helpers';
import { getKueryFields } from '../../../../common/utils/get_kuery_fields';
import { SearchQueryActions } from '../../../services/telemetry';

export const DEFAULT_REFRESH_INTERVAL = 60000;
Expand Down Expand Up @@ -228,7 +234,7 @@ export function UnifiedSearchBar({
if (!res) {
return;
}
const kueryFields = getKueryFields([fromKueryExpression(query?.query as string)]);
const kueryFields = getKqlFieldNamesFromExpression(query?.query as string);

const existingQueryParams = toQuery(location.search);
const updatedQueryWithTime = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
import { fromKueryExpression } from '@kbn/es-query';
import { getKqlFieldNamesFromExpression } from '@kbn/es-query';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import { createHash } from 'crypto';
import { flatten, merge, pickBy, sortBy, sum, uniq } from 'lodash';
Expand Down Expand Up @@ -54,7 +54,6 @@ import {
SavedServiceGroup,
} from '../../../../common/service_groups';
import { asMutableArray } from '../../../../common/utils/as_mutable_array';
import { getKueryFields } from '../../../../common/utils/get_kuery_fields';
import { APMError } from '../../../../typings/es_schemas/ui/apm_error';
import { AgentName } from '../../../../typings/es_schemas/ui/fields/agent';
import { Span } from '../../../../typings/es_schemas/ui/span';
Expand Down Expand Up @@ -1409,11 +1408,10 @@ export const tasks: TelemetryTask[] = [
namespaces: ['*'],
});

const kueryNodes = response.saved_objects.map(({ attributes: { kuery } }) =>
fromKueryExpression(kuery)
);

const kueryFields = getKueryFields(kueryNodes);
const kueryExpressions = response.saved_objects.map(({ attributes: { kuery } }) => kuery);
const kueryFields = kueryExpressions
.map(getKqlFieldNamesFromExpression)
.reduce((a, b) => a.concat(b), []);

return {
service_groups: {
Expand All @@ -1435,11 +1433,12 @@ export const tasks: TelemetryTask[] = [
namespaces: ['*'],
});

const kueryNodes = response.saved_objects.map(({ attributes: { kuery } }) =>
fromKueryExpression(kuery ?? '')
const kueryExpressions = response.saved_objects.map(
({ attributes: { kuery } }) => kuery ?? ''
);

const kueryFields = getKueryFields(kueryNodes);
const kueryFields = kueryExpressions
.map(getKqlFieldNamesFromExpression)
.reduce((a, b) => a.concat(b), []);

return {
custom_dashboards: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,7 @@ import { EQUATION_REGEX, validateCustomThreshold } from './validation';
const errorReason = 'this should appear as error reason';

jest.mock('@kbn/es-query', () => {
const actual = jest.requireActual('@kbn/es-query');
return {
...actual,
buildEsQuery: jest.fn(() => {
// eslint-disable-next-line no-throw-literal
throw { shortMessage: errorReason };
Expand Down

0 comments on commit 564dec5

Please sign in to comment.