From 5fd01d913e15c70f274016de25ed02e54bd59fb8 Mon Sep 17 00:00:00 2001 From: Nathan Reese Date: Wed, 29 Aug 2018 12:31:36 -0600 Subject: [PATCH] Implement inspector for Saved Searches (#22376) * Implement inspector for Saved Searches * add inspect top nav to discover app * add functional test, add support for empty results * clean up functional test names * create inspector request before processing response --- .../public/discover/controllers/discover.js | 49 +++++++++++++ .../discover/embeddable/search_embeddable.js | 9 +++ .../discover/embeddable/search_template.html | 1 + .../segmented_search_request.js | 2 +- src/ui/public/doc_table/doc_table.js | 21 ++++++ test/functional/apps/discover/_inspector.js | 70 +++++++++++++++++++ test/functional/apps/discover/index.js | 1 + 7 files changed, 152 insertions(+), 1 deletion(-) create mode 100644 test/functional/apps/discover/_inspector.js diff --git a/src/core_plugins/kibana/public/discover/controllers/discover.js b/src/core_plugins/kibana/public/discover/controllers/discover.js index 67f6b22b7ed1..d449bdcfef83 100644 --- a/src/core_plugins/kibana/public/discover/controllers/discover.js +++ b/src/core_plugins/kibana/public/discover/controllers/discover.js @@ -55,6 +55,9 @@ import '../components/fetch_error'; import { getPainlessError } from './get_painless_error'; import { showShareContextMenu } from 'ui/share'; import { getUnhashableStatesProvider } from 'ui/state_management/state_hashing'; +import { Inspector } from 'ui/inspector'; +import { RequestAdapter } from 'ui/inspector/adapters'; +import { getRequestInspectorStats, getResponseInspectorStats } from 'ui/courier/utils/courier_inspector_utils'; const app = uiModules.get('apps/discover', [ 'kibana/notify', @@ -159,6 +162,9 @@ function discoverController( location: 'Discover' }); const getUnhashableStates = Private(getUnhashableStatesProvider); + const inspectorAdapters = { + requests: new RequestAdapter() + }; $scope.getDocLink = getDocLink; $scope.intervalOptions = intervalOptions; @@ -201,6 +207,15 @@ function discoverController( objectType: 'search', }); } + }, { + key: 'inspect', + description: 'Open Inspector for search', + testId: 'openInspectorButton', + run() { + Inspector.open(inspectorAdapters, { + title: savedSearch.title + }); + } }]; // the actual courier.SearchSource @@ -554,9 +569,38 @@ function discoverController( segmented.setSortFn(sortFn); segmented.setSize($scope.opts.sampleSize); + let inspectorRequests = []; + function logResponseInInspector(resp) { + if (inspectorRequests.length > 0) { + const inspectorRequest = inspectorRequests.shift(); + inspectorRequest + .stats(getResponseInspectorStats($scope.searchSource, resp)) + .ok({ json: resp }); + } + } + // triggered when the status updated segmented.on('status', function (status) { $scope.fetchStatus = status; + if (status.complete === 0) { + // starting new segmented search request + inspectorAdapters.requests.reset(); + inspectorRequests = []; + } + + if (status.remaining > 0) { + const inspectorRequest = inspectorAdapters.requests.start( + `Segment ${$scope.fetchStatus.complete}`, + { + description: `This request queries Elasticsearch to fetch the data for the search.`, + }); + inspectorRequest.stats(getRequestInspectorStats($scope.searchSource)); + $scope.searchSource.getSearchRequestBody().then(body => { + inspectorRequest.json(body); + }); + inspectorRequests.push(inspectorRequest); + } + }); segmented.on('first', function () { @@ -564,6 +608,7 @@ function discoverController( }); segmented.on('segment', (resp) => { + logResponseInInspector(resp); if (resp._shards.failed > 0) { $scope.failures = _.union($scope.failures, resp._shards.failures); $scope.failures = _.uniq($scope.failures, false, function (failure) { @@ -572,6 +617,10 @@ function discoverController( } }); + segmented.on('emptySegment', function (resp) { + logResponseInInspector(resp); + }); + segmented.on('mergedSegment', function (merged) { $scope.mergedEsResp = merged; diff --git a/src/core_plugins/kibana/public/discover/embeddable/search_embeddable.js b/src/core_plugins/kibana/public/discover/embeddable/search_embeddable.js index 8b076ce825e2..5a138f0897b8 100644 --- a/src/core_plugins/kibana/public/discover/embeddable/search_embeddable.js +++ b/src/core_plugins/kibana/public/discover/embeddable/search_embeddable.js @@ -22,6 +22,7 @@ import { Embeddable } from 'ui/embeddable'; import searchTemplate from './search_template.html'; import * as columnActions from 'ui/doc_table/actions/columns'; import { getTime } from 'ui/timefilter/get_time'; +import { RequestAdapter } from 'ui/inspector/adapters'; export class SearchEmbeddable extends Embeddable { constructor({ onEmbeddableStateChanged, savedSearch, editUrl, loader, $rootScope, $compile }) { @@ -38,6 +39,13 @@ export class SearchEmbeddable extends Embeddable { this.$rootScope = $rootScope; this.$compile = $compile; this.customization = {}; + this.inspectorAdaptors = { + requests: new RequestAdapter() + }; + } + + getInspectorAdapters() { + return this.inspectorAdaptors; } emitEmbeddableStateChange(embeddableState) { @@ -84,6 +92,7 @@ export class SearchEmbeddable extends Embeddable { this.searchScope.description = this.savedSearch.description; this.searchScope.searchSource = this.savedSearch.searchSource; + this.searchScope.inspectorAdapters = this.inspectorAdaptors; const timeRangeSearchSource = this.searchScope.searchSource.create(); timeRangeSearchSource.setField('filter', () => { diff --git a/src/core_plugins/kibana/public/discover/embeddable/search_template.html b/src/core_plugins/kibana/public/discover/embeddable/search_template.html index a6b763f9b4ca..3fbcb16ea17d 100644 --- a/src/core_plugins/kibana/public/discover/embeddable/search_template.html +++ b/src/core_plugins/kibana/public/discover/embeddable/search_template.html @@ -13,5 +13,6 @@ on-move-column="moveColumn" on-remove-column="removeColumn" data-test-subj="embeddedSavedSearchDocTable" + inspector-adapters="inspectorAdapters" > diff --git a/src/ui/public/courier/fetch/request/segmented_search_request/segmented_search_request.js b/src/ui/public/courier/fetch/request/segmented_search_request/segmented_search_request.js index 3245a0f03dee..6ab49eee4fe6 100644 --- a/src/ui/public/courier/fetch/request/segmented_search_request/segmented_search_request.js +++ b/src/ui/public/courier/fetch/request/segmented_search_request/segmented_search_request.js @@ -233,7 +233,7 @@ export function SegmentedSearchRequestProvider(Private, config) { this.resp = _.omit(this._mergedResp, '_bucketIndex'); if (firstHits) this._handle.emit('first', seg); - if (gotHits) this._handle.emit('segment', seg); + gotHits ? this._handle.emit('segment', seg) : this._handle.emit('emptySegment', seg); if (haveHits) this._handle.emit('mergedSegment', this.resp); } diff --git a/src/ui/public/doc_table/doc_table.js b/src/ui/public/doc_table/doc_table.js index b7e47a9c9d62..479f59beea9c 100644 --- a/src/ui/public/doc_table/doc_table.js +++ b/src/ui/public/doc_table/doc_table.js @@ -27,6 +27,7 @@ import './components/table_header'; import './components/table_row'; import { dispatchRenderComplete } from '../render_complete'; import { uiModules } from '../modules'; +import { getRequestInspectorStats, getResponseInspectorStats } from '../courier/utils/courier_inspector_utils'; import { getLimitedSearchResultsMessage } from './doc_table_strings'; @@ -49,6 +50,7 @@ uiModules.get('kibana') onChangeSortOrder: '=?', onMoveColumn: '=?', onRemoveColumn: '=?', + inspectorAdapters: '=?', }, link: function ($scope, $el) { const notify = new Notifier(); @@ -132,7 +134,26 @@ uiModules.get('kibana') } function startSearching() { + let inspectorRequest = undefined; + if (_.has($scope, 'inspectorAdapters.requests')) { + $scope.inspectorAdapters.requests.reset(); + inspectorRequest = $scope.inspectorAdapters.requests.start('Data', { + description: `This request queries Elasticsearch to fetch the data for the search.`, + }); + inspectorRequest.stats(getRequestInspectorStats($scope.searchSource)); + $scope.searchSource.getSearchRequestBody().then(body => { + inspectorRequest.json(body); + }); + } $scope.searchSource.onResults() + .then(resp => { + if (inspectorRequest) { + inspectorRequest + .stats(getResponseInspectorStats($scope.searchSource, resp)) + .ok({ json: resp }); + } + return resp; + }) .then(onResults) .catch(error => { notify.error(error); diff --git a/test/functional/apps/discover/_inspector.js b/test/functional/apps/discover/_inspector.js new file mode 100644 index 000000000000..5ca2497ef84e --- /dev/null +++ b/test/functional/apps/discover/_inspector.js @@ -0,0 +1,70 @@ +/* + * 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 expect from 'expect.js'; + +export default function ({ getService, getPageObjects }) { + const PageObjects = getPageObjects(['common', 'header', 'visualize']); + const esArchiver = getService('esArchiver'); + const kibanaServer = getService('kibanaServer'); + + const STATS_ROW_NAME_INDEX = 0; + const STATS_ROW_VALUE_INDEX = 1; + function getHitCount(requestStats) { + const hitsCountStatsRow = requestStats.find((statsRow) => { + return statsRow[STATS_ROW_NAME_INDEX] === 'Hits'; + }); + return hitsCountStatsRow[STATS_ROW_VALUE_INDEX]; + } + + describe('inspect', () => { + before(async () => { + await esArchiver.loadIfNeeded('logstash_functional'); + await esArchiver.load('discover'); + // delete .kibana index and update configDoc + await kibanaServer.uiSettings.replace({ + 'dateFormat:tz': 'UTC', + 'defaultIndex': 'logstash-*' + }); + + await PageObjects.common.navigateToApp('discover'); + }); + + afterEach(async () => { + await PageObjects.visualize.closeInspector(); + }); + + it('should display request stats with no results', async () => { + await PageObjects.visualize.openInspector(); + const requestStats = await PageObjects.visualize.getInspectorTableData(); + + expect(getHitCount(requestStats)).to.be('0'); + }); + + it('should display request stats with results', async () => { + await PageObjects.header.setAbsoluteRange('2015-09-19 06:31:44.000', '2015-09-23 18:31:44.000'); + + await PageObjects.visualize.openInspector(); + const requestStats = await PageObjects.visualize.getInspectorTableData(); + + expect(getHitCount(requestStats)).to.be('14004'); + }); + + }); +} diff --git a/test/functional/apps/discover/index.js b/test/functional/apps/discover/index.js index 2aff236694c9..4c8441d5e40d 100644 --- a/test/functional/apps/discover/index.js +++ b/test/functional/apps/discover/index.js @@ -37,5 +37,6 @@ export default function ({ getService, loadTestFile }) { loadTestFile(require.resolve('./_sidebar')); loadTestFile(require.resolve('./_source_filters')); loadTestFile(require.resolve('./_large_string')); + loadTestFile(require.resolve('./_inspector')); }); }