Add configs for terminate_after (#37643)

This commit is contained in:
Joe Reuter 2019-06-24 15:27:18 +02:00 committed by GitHub
parent 3184be10e0
commit 4d88aaa274
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 166 additions and 60 deletions

View file

@ -128,6 +128,14 @@ store saved searches, visualizations and dashboards. Kibana creates a new index
if the index doesnt already exist. If you configure a custom index, the name must
be lowercase, and conform to {es} {ref}/indices-create-index.html[index name limitations].
`kibana.autocompleteTimeout:`:: *Default: "1000"* Time in milliseconds to wait
for autocomplete suggestions from Elasticsearch. This value must be a whole number
greater than zero.
`kibana.autocompleteTerminateAfter:`:: *Default: "100000"* Maximum number of
documents loaded by each shard to generate autocomplete suggestions. This value
must be a whole number greater than zero.
`logging.dest:`:: *Default: `stdout`* Enables you specify a file where Kibana
stores log output.

View file

@ -1,5 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`renders control with warning 1`] = `
<EuiFormRow
data-test-subj="inputControl0"
describedByIds={Array []}
fullWidth={false}
hasEmptyLabelSpace={false}
id="controlId"
label={
<React.Fragment>
<EuiToolTip
content="This is a warning"
delay="regular"
position="top"
>
<EuiIcon
type="alert"
/>
</EuiToolTip>
test control
</React.Fragment>
}
labelType="label"
>
<div>
My Control
</div>
</EuiFormRow>
`;
exports[`renders disabled control with tooltip 1`] = `
<EuiFormRow
data-test-subj="inputControl0"

View file

@ -20,10 +20,7 @@
import PropTypes from 'prop-types';
import React from 'react';
import {
EuiFormRow,
EuiToolTip,
} from '@elastic/eui';
import { EuiFormRow, EuiToolTip, EuiIcon } from '@elastic/eui';
export function FormRow(props) {
let control = props.children;
@ -35,9 +32,20 @@ export function FormRow(props) {
);
}
const label = props.warningMsg ? (
<>
<EuiToolTip position="top" content={props.warningMsg}>
<EuiIcon type="alert" />
</EuiToolTip>
{props.label}
</>
) : (
props.label
);
return (
<EuiFormRow
label={props.label}
label={label}
id={props.id}
data-test-subj={'inputControl' + props.controlIndex}
>
@ -48,6 +56,7 @@ export function FormRow(props) {
FormRow.propTypes = {
label: PropTypes.string.isRequired,
warningMsg: PropTypes.string,
id: PropTypes.string.isRequired,
children: PropTypes.node.isRequired,
controlIndex: PropTypes.number.isRequired,

View file

@ -37,6 +37,20 @@ test('renders enabled control', () => {
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('renders control with warning', () => {
const component = shallow(
<FormRow
label="test control"
id="controlId"
controlIndex={0}
warningMsg="This is a warning"
>
<div>My Control</div>
</FormRow>
);
expect(component).toMatchSnapshot(); // eslint-disable-line
});
test('renders disabled control with tooltip', () => {
const component = shallow(
<FormRow

View file

@ -66,6 +66,7 @@ export class InputControlVis extends Component {
formatOptionLabel={control.format}
disableMsg={control.isEnabled() ? null : control.disabledReason}
multiselect={control.options.multiselect}
partialResults={control.partialResults}
dynamicOptions={control.options.dynamicOptions}
controlIndex={index}
stageFilter={this.props.stageFilter}

View file

@ -23,33 +23,30 @@ import _ from 'lodash';
import { FormRow } from './form_row';
import { injectI18n } from '@kbn/i18n/react';
import {
EuiFieldText,
EuiComboBox,
} from '@elastic/eui';
import { EuiFieldText, EuiComboBox } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
class ListControlUi extends Component {
state = {
isLoading: false
}
isLoading: false,
};
componentDidMount = () => {
this._isMounted = true;
}
};
componentWillUnmount = () => {
this._isMounted = false;
}
};
handleOnChange = (selectedOptions) => {
handleOnChange = selectedOptions => {
const selectedValues = selectedOptions.map(({ value }) => {
return value;
});
this.props.stageFilter(this.props.controlIndex, selectedValues);
}
};
debouncedFetch = _.debounce(async (searchValue) => {
debouncedFetch = _.debounce(async searchValue => {
await this.props.fetchOptions(searchValue);
if (this._isMounted) {
@ -59,11 +56,14 @@ class ListControlUi extends Component {
}
}, 300);
onSearchChange = (searchValue) => {
this.setState({
isLoading: true,
}, this.debouncedFetch.bind(null, searchValue));
}
onSearchChange = searchValue => {
this.setState(
{
isLoading: true,
},
this.debouncedFetch.bind(null, searchValue)
);
};
renderControl() {
const { intl } = this.props;
@ -73,7 +73,7 @@ class ListControlUi extends Component {
<EuiFieldText
placeholder={intl.formatMessage({
id: 'inputControl.vis.listControl.selectTextPlaceholder',
defaultMessage: 'Select...'
defaultMessage: 'Select...',
})}
disabled={true}
/>
@ -85,7 +85,7 @@ class ListControlUi extends Component {
return {
label: this.props.formatOptionLabel(option).toString(),
value: option,
['data-test-subj']: `option_${option.toString().replace(' ', '_')}`
['data-test-subj']: `option_${option.toString().replace(' ', '_')}`,
};
})
.sort((a, b) => {
@ -103,7 +103,7 @@ class ListControlUi extends Component {
<EuiComboBox
placeholder={intl.formatMessage({
id: 'inputControl.vis.listControl.selectPlaceholder',
defaultMessage: 'Select...'
defaultMessage: 'Select...',
})}
options={options}
isLoading={this.state.isLoading}
@ -118,10 +118,18 @@ class ListControlUi extends Component {
}
render() {
const partialResultsWarningMessage = i18n.translate(
'inputControl.vis.listControl.partialResultsWarningMessage',
{
defaultMessage: `Terms list is incomplete. Adjust the autocomplete settings in kibana.yml for more results.`,
}
);
return (
<FormRow
id={this.props.id}
label={this.props.label}
warningMsg={this.props.partialResults ? partialResultsWarningMessage : undefined}
controlIndex={this.props.controlIndex}
disableMsg={this.props.disableMsg}
>
@ -140,6 +148,7 @@ ListControlUi.propTypes = {
disableMsg: PropTypes.string,
multiselect: PropTypes.bool,
dynamicOptions: PropTypes.bool,
partialResults: PropTypes.bool,
controlIndex: PropTypes.number.isRequired,
stageFilter: PropTypes.func.isRequired,
fetchOptions: PropTypes.func,

View file

@ -26,6 +26,7 @@ import {
import { PhraseFilterManager } from './filter_manager/phrase_filter_manager';
import { createSearchSource } from './create_search_source';
import { i18n } from '@kbn/i18n';
import chrome from 'ui/chrome';
function getEscapedQuery(query = '') {
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators
@ -98,8 +99,8 @@ class ListControl extends Control {
const fieldName = this.filterManager.fieldName;
const initialSearchSourceState = {
timeout: '1s',
terminate_after: 100000
timeout: `${chrome.getInjected('autocompleteTimeout')}ms`,
terminate_after: chrome.getInjected('autocompleteTerminateAfter')
};
const aggs = termsAgg({
field: indexPattern.fields.byName[fieldName],
@ -141,6 +142,7 @@ class ListControl extends Control {
return;
}
this.partialResults = resp.terminated_early || resp.timed_out;
this.selectOptions = selectOptions;
this.enable = true;
this.disabledReason = '';

View file

@ -17,8 +17,16 @@
* under the License.
*/
import chrome from 'ui/chrome';
import { listControlFactory } from './list_control_factory';
chrome.getInjected.mockImplementation((key) => {
switch(key) {
case 'autocompleteTimeout': return 1000;
case 'autocompleteTerminateAfter': return 100000;
}
});
const mockField = {
name: 'myField',
format: {
@ -59,7 +67,7 @@ function MockSearchSource() {
};
}
const mockKbnApi = {
const getMockKbnApi = () => ({
indexPatterns: {
get: async () => {
return mockIndexPattern;
@ -73,8 +81,8 @@ const mockKbnApi = {
return [];
}
},
SearchSource: MockSearchSource,
};
SearchSource: jest.fn(MockSearchSource),
});
describe('hasValue', () => {
const controlParams = {
@ -86,7 +94,7 @@ describe('hasValue', () => {
let listControl;
beforeEach(async () => {
listControl = await listControlFactory(controlParams, mockKbnApi, useTimeFilter);
listControl = await listControlFactory(controlParams, getMockKbnApi(), useTimeFilter);
});
test('should be false when control has no value', () => {
@ -111,12 +119,22 @@ describe('fetch', () => {
options: {}
};
const useTimeFilter = false;
let mockKbnApi;
let listControl;
beforeEach(async () => {
mockKbnApi = getMockKbnApi();
listControl = await listControlFactory(controlParams, mockKbnApi, useTimeFilter);
});
test('should pass in timeout parameters from injected vars', async () => {
await listControl.fetch();
expect(mockKbnApi.SearchSource).toHaveBeenCalledWith({
timeout: `1000ms`,
terminate_after: 100000
});
});
test('should set selectOptions to results of terms aggregation', async () => {
await listControl.fetch();
expect(listControl.selectOptions).toEqual(['Zurich Airport', 'Xi an Xianyang International Airport']);
@ -135,6 +153,7 @@ describe('fetch with ancestors', () => {
let listControl;
let parentControl;
beforeEach(async () => {
const mockKbnApi = getMockKbnApi();
listControl = await listControlFactory(controlParams, mockKbnApi, useTimeFilter);
const parentControlParams = {

View file

@ -54,6 +54,9 @@ export default function (kibana) {
defaultAppId: Joi.string().default('home'),
index: Joi.string().default('.kibana'),
disableWelcomeScreen: Joi.boolean().default(false),
autocompleteTerminateAfter: Joi.number().integer().min(1).default(100000),
// TODO Also allow units here like in elasticsearch config once this is moved to the new platform
autocompleteTimeout: Joi.number().integer().min(1).default(1000),
}).default();
},
@ -76,25 +79,27 @@ export default function (kibana) {
{
id: 'kibana:discover',
title: i18n.translate('kbn.discoverTitle', {
defaultMessage: 'Discover'
defaultMessage: 'Discover',
}),
order: -1003,
url: `${kbnBaseUrl}#/discover`,
icon: 'plugins/kibana/assets/discover.svg',
euiIconType: 'discoverApp',
}, {
},
{
id: 'kibana:visualize',
title: i18n.translate('kbn.visualizeTitle', {
defaultMessage: 'Visualize'
defaultMessage: 'Visualize',
}),
order: -1002,
url: `${kbnBaseUrl}#/visualize`,
icon: 'plugins/kibana/assets/visualize.svg',
euiIconType: 'visualizeApp',
}, {
},
{
id: 'kibana:dashboard',
title: i18n.translate('kbn.dashboardTitle', {
defaultMessage: 'Dashboard'
defaultMessage: 'Dashboard',
}),
order: -1001,
url: `${kbnBaseUrl}#/dashboards`,
@ -106,25 +111,27 @@ export default function (kibana) {
subUrlBase: `${kbnBaseUrl}#/dashboard`,
icon: 'plugins/kibana/assets/dashboard.svg',
euiIconType: 'dashboardApp',
}, {
},
{
id: 'kibana:dev_tools',
title: i18n.translate('kbn.devToolsTitle', {
defaultMessage: 'Dev Tools'
defaultMessage: 'Dev Tools',
}),
order: 9001,
url: '/app/kibana#/dev_tools',
icon: 'plugins/kibana/assets/wrench.svg',
euiIconType: 'devToolsApp',
}, {
},
{
id: 'kibana:management',
title: i18n.translate('kbn.managementTitle', {
defaultMessage: 'Management'
defaultMessage: 'Management',
}),
order: 9003,
url: `${kbnBaseUrl}#/management`,
icon: 'plugins/kibana/assets/settings.svg',
euiIconType: 'managementApp',
linkToLastSubUrl: false
linkToLastSubUrl: false,
},
],
@ -268,7 +275,7 @@ export default function (kibana) {
},
advancedSettings: {
show: true,
save: true
save: true,
},
indexPatterns: {
save: true,
@ -288,7 +295,7 @@ export default function (kibana) {
index_patterns: true,
objects: true,
},
}
},
};
},
@ -323,6 +330,6 @@ export default function (kibana) {
server.expose('systemApi', systemApi);
server.expose('handleEsError', handleEsError);
server.injectUiAppVars('kibana', () => injectVars(server));
}
},
});
}

View file

@ -29,7 +29,9 @@ export function injectVars(server) {
// Get types that are import and exportable, by default yes unless isImportableAndExportable is set to false
const { types: allTypes } = server.savedObjects;
const savedObjectsManagement = server.getSavedObjectsManagement();
const importAndExportableTypes = allTypes.filter(type => savedObjectsManagement.isImportAndExportable(type));
const importAndExportableTypes = allTypes.filter(type =>
savedObjectsManagement.isImportAndExportable(type)
);
return {
kbnDefaultAppId: serverConfig.get('kibana.defaultAppId'),
@ -37,6 +39,8 @@ export function injectVars(server) {
regionmapsConfig: regionmap,
mapConfig: mapConfig,
importAndExportableTypes,
autocompleteTerminateAfter: serverConfig.get('kibana.autocompleteTerminateAfter'),
autocompleteTimeout: serverConfig.get('kibana.autocompleteTimeout'),
tilemapsConfig: {
deprecated: {
isOverridden: isOverridden,

View file

@ -21,6 +21,9 @@ import { get, map } from 'lodash';
import handleESError from '../../../lib/handle_es_error';
export function registerValueSuggestions(server) {
const serverConfig = server.config();
const autocompleteTerminateAfter = serverConfig.get('kibana.autocompleteTerminateAfter');
const autocompleteTimeout = serverConfig.get('kibana.autocompleteTimeout');
server.route({
path: '/api/kibana/suggestions/values/{index}',
method: ['POST'],
@ -28,7 +31,11 @@ export function registerValueSuggestions(server) {
const { index } = req.params;
const { field, query, boolFilter } = req.payload;
const { callWithRequest } = server.plugins.elasticsearch.getCluster('data');
const body = getBody({ field, query, boolFilter });
const body = getBody(
{ field, query, boolFilter },
autocompleteTerminateAfter,
autocompleteTimeout
);
try {
const response = await callWithRequest(req, 'search', { index, body });
const buckets = get(response, 'aggregations.suggestions.buckets') || [];
@ -37,30 +44,26 @@ export function registerValueSuggestions(server) {
} catch (error) {
throw handleESError(error);
}
}
},
});
}
function getBody({ field, query, boolFilter = [] }) {
function getBody({ field, query, boolFilter = [] }, terminateAfter, timeout) {
// Helps ensure that the regex is not evaluated eagerly against the terms dictionary
const executionHint = 'map';
// Helps keep the number of buckets that need to be tracked at the shard level contained in case
// this is a high cardinality field
const terminateAfter = 100000;
// We don't care about the accuracy of the counts, just the content of the terms, so this reduces
// the amount of information that needs to be transmitted to the coordinating node
const shardSize = 10;
return {
size: 0,
timeout: '1s',
timeout: `${timeout}ms`,
terminate_after: terminateAfter,
query: {
bool: {
filter: boolFilter,
}
},
},
aggs: {
suggestions: {
@ -68,14 +71,14 @@ function getBody({ field, query, boolFilter = [] }) {
field,
include: `${getEscapedQuery(query)}.*`,
execution_hint: executionHint,
shard_size: shardSize
}
}
}
shard_size: shardSize,
},
},
},
};
}
function getEscapedQuery(query = '') {
// https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html#_standard_operators
return query.replace(/[.?+*|{}[\]()"\\#@&<>~]/g, (match) => `\\${match}`);
return query.replace(/[.?+*|{}[\]()"\\#@&<>~]/g, match => `\\${match}`);
}

View file

@ -36,7 +36,8 @@ export function serializeFetchParams(
const body = {
...fetchParams.body || {},
};
if (esShardTimeout > 0) {
if (!('timeout' in body) && esShardTimeout > 0) {
body.timeout = `${esShardTimeout}ms`;
}