diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md
index 51bc46bbdccc83..7bae595e75ad08 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.plugin.setup.md
@@ -7,7 +7,7 @@
Signature:
```typescript
-setup(core: CoreSetup, { expressions, uiActions }: DataSetupDependencies): DataPublicPluginSetup;
+setup(core: CoreSetup, { expressions, uiActions, usageCollection }: DataSetupDependencies): DataPublicPluginSetup;
```
## Parameters
@@ -15,7 +15,7 @@ setup(core: CoreSetup, { expressions, uiActions }: DataSetupDependencies): DataP
| Parameter | Type | Description |
| --- | --- | --- |
| core | CoreSetup
| |
-| { expressions, uiActions } | DataSetupDependencies
| |
+| { expressions, uiActions, usageCollection } | DataSetupDependencies
| |
Returns:
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md
index abd57f3a9568bf..1291af5359887d 100644
--- a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.md
@@ -18,4 +18,5 @@ export interface SearchInterceptorDeps
| [http](./kibana-plugin-plugins-data-public.searchinterceptordeps.http.md) | CoreStart['http']
| |
| [toasts](./kibana-plugin-plugins-data-public.searchinterceptordeps.toasts.md) | ToastsStart
| |
| [uiSettings](./kibana-plugin-plugins-data-public.searchinterceptordeps.uisettings.md) | CoreStart['uiSettings']
| |
+| [usageCollector](./kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md) | SearchUsageCollector
| |
diff --git a/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md
new file mode 100644
index 00000000000000..21afce19276769
--- /dev/null
+++ b/docs/development/plugins/data/public/kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md
@@ -0,0 +1,11 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-public](./kibana-plugin-plugins-data-public.md) > [SearchInterceptorDeps](./kibana-plugin-plugins-data-public.searchinterceptordeps.md) > [usageCollector](./kibana-plugin-plugins-data-public.searchinterceptordeps.usagecollector.md)
+
+## SearchInterceptorDeps.usageCollector property
+
+Signature:
+
+```typescript
+usageCollector?: SearchUsageCollector;
+```
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md
index ca8ad8fdc06eac..3afba80064f084 100644
--- a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.md
@@ -14,5 +14,6 @@ export interface ISearchSetup
| Property | Type | Description |
| --- | --- | --- |
-| [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) | (name: string, strategy: ISearchStrategy) => void
| Extension point exposed for other plugins to register their own search strategies. |
+| [registerSearchStrategy](./kibana-plugin-plugins-data-server.isearchsetup.registersearchstrategy.md) | TRegisterSearchStrategy
| Extension point exposed for other plugins to register their own search strategies. |
+| [usage](./kibana-plugin-plugins-data-server.isearchsetup.usage.md) | SearchUsage
| Used internally for telemetry |
diff --git a/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md
new file mode 100644
index 00000000000000..85abd9d9dba980
--- /dev/null
+++ b/docs/development/plugins/data/server/kibana-plugin-plugins-data-server.isearchsetup.usage.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-plugins-data-server](./kibana-plugin-plugins-data-server.md) > [ISearchSetup](./kibana-plugin-plugins-data-server.isearchsetup.md) > [usage](./kibana-plugin-plugins-data-server.isearchsetup.usage.md)
+
+## ISearchSetup.usage property
+
+Used internally for telemetry
+
+Signature:
+
+```typescript
+usage: SearchUsage;
+```
diff --git a/src/plugins/data/kibana.json b/src/plugins/data/kibana.json
index 2ffd0688b134ee..b4f20ec6225e2c 100644
--- a/src/plugins/data/kibana.json
+++ b/src/plugins/data/kibana.json
@@ -10,6 +10,7 @@
"optionalPlugins": ["usageCollection"],
"extraPublicDirs": ["common", "common/utils/abort_utils"],
"requiredBundles": [
+ "usageCollection",
"kibanaUtils",
"kibanaReact",
"kibanaLegacy",
diff --git a/src/plugins/data/public/plugin.ts b/src/plugins/data/public/plugin.ts
index 4040781bb2f01a..323a32ea362ac7 100644
--- a/src/plugins/data/public/plugin.ts
+++ b/src/plugins/data/public/plugin.ts
@@ -111,7 +111,7 @@ export class DataPublicPlugin implements Plugin {
+ let mockCoreSetup: MockedKeys;
+ let mockUsageCollectionSetup: Setup;
+ let usageCollector: SearchUsageCollector;
+
+ beforeEach(() => {
+ mockCoreSetup = coreMock.createSetup();
+ (mockCoreSetup as any).getStartServices.mockResolvedValue([
+ {
+ application: {
+ currentAppId$: from(['foo/bar']),
+ },
+ } as jest.Mocked,
+ {} as any,
+ {} as any,
+ ]);
+ mockUsageCollectionSetup = usageCollectionPluginMock.createSetupContract();
+ usageCollector = createUsageCollector(mockCoreSetup, mockUsageCollectionSetup);
+ });
+
+ test('tracks query timeouts', async () => {
+ await usageCollector.trackQueryTimedOut();
+ expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled();
+ expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][0]).toBe('foo/bar');
+ expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED);
+ expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe(
+ SEARCH_EVENT_TYPE.QUERY_TIMED_OUT
+ );
+ });
+
+ test('tracks query cancellation', async () => {
+ await usageCollector.trackQueriesCancelled();
+ expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled();
+ expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED);
+ expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe(
+ SEARCH_EVENT_TYPE.QUERIES_CANCELLED
+ );
+ });
+
+ test('tracks long popups', async () => {
+ await usageCollector.trackLongQueryPopupShown();
+ expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled();
+ expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.LOADED);
+ expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe(
+ SEARCH_EVENT_TYPE.LONG_QUERY_POPUP_SHOWN
+ );
+ });
+
+ test('tracks long popups dismissed', async () => {
+ await usageCollector.trackLongQueryDialogDismissed();
+ expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled();
+ expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.CLICK);
+ expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe(
+ SEARCH_EVENT_TYPE.LONG_QUERY_DIALOG_DISMISSED
+ );
+ });
+
+ test('tracks run query beyond timeout', async () => {
+ await usageCollector.trackLongQueryRunBeyondTimeout();
+ expect(mockUsageCollectionSetup.reportUiStats).toHaveBeenCalled();
+ expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][1]).toBe(METRIC_TYPE.CLICK);
+ expect(mockUsageCollectionSetup.reportUiStats.mock.calls[0][2]).toBe(
+ SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT
+ );
+ });
+
+ test('tracks response errors', async () => {
+ const duration = 10;
+ await usageCollector.trackError(duration);
+ expect(mockCoreSetup.http.post).toBeCalled();
+ expect(mockCoreSetup.http.post.mock.calls[0][0]).toBe('/api/search/usage');
+ });
+
+ test('tracks response duration', async () => {
+ const duration = 5;
+ await usageCollector.trackSuccess(duration);
+ expect(mockCoreSetup.http.post).toBeCalled();
+ expect(mockCoreSetup.http.post.mock.calls[0][0]).toBe('/api/search/usage');
+ });
+});
diff --git a/src/plugins/data/public/search/collectors/create_usage_collector.ts b/src/plugins/data/public/search/collectors/create_usage_collector.ts
new file mode 100644
index 00000000000000..cb1b2b65c17c84
--- /dev/null
+++ b/src/plugins/data/public/search/collectors/create_usage_collector.ts
@@ -0,0 +1,92 @@
+/*
+ * 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 { first } from 'rxjs/operators';
+import { CoreSetup } from '../../../../../core/public';
+import { METRIC_TYPE, UsageCollectionSetup } from '../../../../usage_collection/public';
+import { SEARCH_EVENT_TYPE, SearchUsageCollector } from './types';
+
+export const createUsageCollector = (
+ core: CoreSetup,
+ usageCollection?: UsageCollectionSetup
+): SearchUsageCollector => {
+ const getCurrentApp = async () => {
+ const [{ application }] = await core.getStartServices();
+ return application.currentAppId$.pipe(first()).toPromise();
+ };
+
+ return {
+ trackQueryTimedOut: async () => {
+ const currentApp = await getCurrentApp();
+ return usageCollection?.reportUiStats(
+ currentApp!,
+ METRIC_TYPE.LOADED,
+ SEARCH_EVENT_TYPE.QUERY_TIMED_OUT
+ );
+ },
+ trackQueriesCancelled: async () => {
+ const currentApp = await getCurrentApp();
+ return usageCollection?.reportUiStats(
+ currentApp!,
+ METRIC_TYPE.LOADED,
+ SEARCH_EVENT_TYPE.QUERIES_CANCELLED
+ );
+ },
+ trackLongQueryPopupShown: async () => {
+ const currentApp = await getCurrentApp();
+ return usageCollection?.reportUiStats(
+ currentApp!,
+ METRIC_TYPE.LOADED,
+ SEARCH_EVENT_TYPE.LONG_QUERY_POPUP_SHOWN
+ );
+ },
+ trackLongQueryDialogDismissed: async () => {
+ const currentApp = await getCurrentApp();
+ return usageCollection?.reportUiStats(
+ currentApp!,
+ METRIC_TYPE.CLICK,
+ SEARCH_EVENT_TYPE.LONG_QUERY_DIALOG_DISMISSED
+ );
+ },
+ trackLongQueryRunBeyondTimeout: async () => {
+ const currentApp = await getCurrentApp();
+ return usageCollection?.reportUiStats(
+ currentApp!,
+ METRIC_TYPE.CLICK,
+ SEARCH_EVENT_TYPE.LONG_QUERY_RUN_BEYOND_TIMEOUT
+ );
+ },
+ trackError: async (duration: number) => {
+ return core.http.post('/api/search/usage', {
+ body: JSON.stringify({
+ eventType: 'error',
+ duration,
+ }),
+ });
+ },
+ trackSuccess: async (duration: number) => {
+ return core.http.post('/api/search/usage', {
+ body: JSON.stringify({
+ eventType: 'success',
+ duration,
+ }),
+ });
+ },
+ };
+};
diff --git a/src/plugins/data/public/search/collectors/index.ts b/src/plugins/data/public/search/collectors/index.ts
new file mode 100644
index 00000000000000..afe127c00b5dd5
--- /dev/null
+++ b/src/plugins/data/public/search/collectors/index.ts
@@ -0,0 +1,21 @@
+/*
+ * 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.
+ */
+
+export { createUsageCollector } from './create_usage_collector';
+export { SEARCH_EVENT_TYPE, SearchUsageCollector } from './types';
diff --git a/src/plugins/data/public/search/collectors/types.ts b/src/plugins/data/public/search/collectors/types.ts
new file mode 100644
index 00000000000000..bb85532fd3ab59
--- /dev/null
+++ b/src/plugins/data/public/search/collectors/types.ts
@@ -0,0 +1,36 @@
+/*
+ * 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.
+ */
+
+export enum SEARCH_EVENT_TYPE {
+ QUERY_TIMED_OUT = 'queryTimedOut',
+ QUERIES_CANCELLED = 'queriesCancelled',
+ LONG_QUERY_POPUP_SHOWN = 'longQueryPopupShown',
+ LONG_QUERY_DIALOG_DISMISSED = 'longQueryDialogDismissed',
+ LONG_QUERY_RUN_BEYOND_TIMEOUT = 'longQueryRunBeyondTimeout',
+}
+
+export interface SearchUsageCollector {
+ trackQueryTimedOut: () => Promise;
+ trackQueriesCancelled: () => Promise;
+ trackLongQueryPopupShown: () => Promise;
+ trackLongQueryDialogDismissed: () => Promise;
+ trackLongQueryRunBeyondTimeout: () => Promise;
+ trackError: (duration: number) => Promise;
+ trackSuccess: (duration: number) => Promise;
+}
diff --git a/src/plugins/data/public/search/search_interceptor.ts b/src/plugins/data/public/search/search_interceptor.ts
index 8edbfd94deb383..84e24114a9e6c4 100644
--- a/src/plugins/data/public/search/search_interceptor.ts
+++ b/src/plugins/data/public/search/search_interceptor.ts
@@ -18,12 +18,13 @@
*/
import { BehaviorSubject, throwError, timer, Subscription, defer, from, Observable } from 'rxjs';
-import { finalize, filter } from 'rxjs/operators';
+import { finalize, filter, tap } from 'rxjs/operators';
import { ApplicationStart, Toast, ToastsStart, CoreStart } from 'kibana/public';
import { getCombinedSignal, AbortError } from '../../common/utils';
import { IEsSearchRequest, IEsSearchResponse } from '../../common/search';
import { ISearchOptions } from './types';
import { getLongQueryNotification } from './long_query_notification';
+import { SearchUsageCollector } from './collectors';
const LONG_QUERY_NOTIFICATION_DELAY = 10000;
@@ -32,6 +33,7 @@ export interface SearchInterceptorDeps {
application: ApplicationStart;
http: CoreStart['http'];
uiSettings: CoreStart['uiSettings'];
+ usageCollector?: SearchUsageCollector;
}
export class SearchInterceptor {
@@ -121,6 +123,13 @@ export class SearchInterceptor {
this.pendingCount$.next(++this.pendingCount);
return this.runSearch(request, combinedSignal).pipe(
+ tap({
+ next: (e) => {
+ if (this.deps.usageCollector) {
+ this.deps.usageCollector.trackSuccess(e.rawResponse.took);
+ }
+ },
+ }),
finalize(() => {
this.pendingCount$.next(--this.pendingCount);
cleanup();
@@ -185,6 +194,9 @@ export class SearchInterceptor {
if (this.longRunningToast) {
this.deps.toasts.remove(this.longRunningToast);
delete this.longRunningToast;
+ if (this.deps.usageCollector) {
+ this.deps.usageCollector.trackLongQueryDialogDismissed();
+ }
}
};
}
diff --git a/src/plugins/data/public/search/search_service.ts b/src/plugins/data/public/search/search_service.ts
index a27eba21714bbe..064e16014cb70a 100644
--- a/src/plugins/data/public/search/search_service.ts
+++ b/src/plugins/data/public/search/search_service.ts
@@ -37,9 +37,12 @@ import {
getCalculateAutoTimeExpression,
} from './aggs';
import { ISearchGeneric } from './types';
+import { SearchUsageCollector, createUsageCollector } from './collectors';
+import { UsageCollectionSetup } from '../../../usage_collection/public';
interface SearchServiceSetupDependencies {
expressions: ExpressionsSetup;
+ usageCollection?: UsageCollectionSetup;
getInternalStartServices: GetInternalStartServicesFn;
packageInfo: PackageInfo;
}
@@ -52,6 +55,7 @@ export class SearchService implements Plugin {
private esClient?: LegacyApiCaller;
private readonly aggTypesRegistry = new AggTypesRegistry();
private searchInterceptor!: SearchInterceptor;
+ private usageCollector?: SearchUsageCollector;
/**
* getForceNow uses window.location, so we must have a separate implementation
@@ -62,8 +66,14 @@ export class SearchService implements Plugin {
public setup(
core: CoreSetup,
- { expressions, packageInfo, getInternalStartServices }: SearchServiceSetupDependencies
+ {
+ expressions,
+ usageCollection,
+ packageInfo,
+ getInternalStartServices,
+ }: SearchServiceSetupDependencies
): ISearchSetup {
+ this.usageCollector = createUsageCollector(core, usageCollection);
this.esClient = getEsClient(core.injectedMetadata, core.http, packageInfo);
const aggTypesSetup = this.aggTypesRegistry.setup();
@@ -102,6 +112,7 @@ export class SearchService implements Plugin {
application: core.application,
http: core.http,
uiSettings: core.uiSettings,
+ usageCollector: this.usageCollector!,
},
core.injectedMetadata.getInjectedVar('esRequestTimeout') as number
);
@@ -134,6 +145,7 @@ export class SearchService implements Plugin {
types: aggTypesStart,
},
search,
+ usageCollector: this.usageCollector!,
searchSource: {
create: createSearchSource(dependencies.indexPatterns, searchSourceDependencies),
createEmpty: () => {
diff --git a/src/plugins/data/public/search/types.ts b/src/plugins/data/public/search/types.ts
index 5c4bb42a5948d0..ec74275f35c041 100644
--- a/src/plugins/data/public/search/types.ts
+++ b/src/plugins/data/public/search/types.ts
@@ -18,17 +18,22 @@
*/
import { Observable } from 'rxjs';
+import { PackageInfo } from 'kibana/server';
import { SearchAggsSetup, SearchAggsStart } from './aggs';
import { LegacyApiCaller } from './legacy/es_client';
import { SearchInterceptor } from './search_interceptor';
import { ISearchSource, SearchSourceFields } from './search_source';
-
+import { SearchUsageCollector } from './collectors';
import {
IKibanaSearchRequest,
IKibanaSearchResponse,
IEsSearchRequest,
IEsSearchResponse,
} from '../../common/search';
+import { IndexPatternsContract } from '../../common/index_patterns/index_patterns';
+import { ExpressionsSetup } from '../../../expressions/public';
+import { UsageCollectionSetup } from '../../../usage_collection/public';
+import { GetInternalStartServicesFn } from '../types';
export interface ISearchOptions {
signal?: AbortSignal;
@@ -69,5 +74,19 @@ export interface ISearchStart {
create: (fields?: SearchSourceFields) => Promise;
createEmpty: () => ISearchSource;
};
+ usageCollector?: SearchUsageCollector;
__LEGACY: ISearchStartLegacy;
}
+
+export { SEARCH_EVENT_TYPE } from './collectors';
+
+export interface SearchServiceSetupDependencies {
+ expressions: ExpressionsSetup;
+ usageCollection?: UsageCollectionSetup;
+ getInternalStartServices: GetInternalStartServicesFn;
+ packageInfo: PackageInfo;
+}
+
+export interface SearchServiceStartDependencies {
+ indexPatterns: IndexPatternsContract;
+}
diff --git a/src/plugins/data/public/types.ts b/src/plugins/data/public/types.ts
index aaef403979de6a..6d671272514244 100644
--- a/src/plugins/data/public/types.ts
+++ b/src/plugins/data/public/types.ts
@@ -30,10 +30,12 @@ import { QuerySetup, QueryStart } from './query';
import { IndexPatternSelectProps } from './ui/index_pattern_select';
import { IndexPatternsContract } from './index_patterns';
import { StatefulSearchBarProps } from './ui/search_bar/create_search_bar';
+import { UsageCollectionSetup } from '../../usage_collection/public';
export interface DataSetupDependencies {
expressions: ExpressionsSetup;
uiActions: UiActionsSetup;
+ usageCollection?: UsageCollectionSetup;
}
export interface DataStartDependencies {
diff --git a/src/plugins/data/server/plugin.ts b/src/plugins/data/server/plugin.ts
index bcf1f4f8ab60bb..8fa32f9bd564f9 100644
--- a/src/plugins/data/server/plugin.ts
+++ b/src/plugins/data/server/plugin.ts
@@ -82,7 +82,7 @@ export class DataServerPlugin implements Plugin) {
+ return async (callCluster: LegacyAPICaller): Promise => {
+ const config = await config$.pipe(first()).toPromise();
+
+ const response = await callCluster('search', {
+ index: config.kibana.index,
+ body: {
+ query: { term: { type: { value: 'search-telemetry' } } },
+ },
+ ignore: [404],
+ });
+
+ return response.hits.hits.length
+ ? (response.hits.hits[0]._source as Usage)
+ : {
+ successCount: 0,
+ errorCount: 0,
+ averageDuration: null,
+ };
+ };
+}
diff --git a/src/plugins/data/server/search/collectors/register.ts b/src/plugins/data/server/search/collectors/register.ts
new file mode 100644
index 00000000000000..ab0ea93edd49e2
--- /dev/null
+++ b/src/plugins/data/server/search/collectors/register.ts
@@ -0,0 +1,49 @@
+/*
+ * 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 { PluginInitializerContext } from 'kibana/server';
+import { UsageCollectionSetup } from '../../../../usage_collection/server';
+import { fetchProvider } from './fetch';
+
+export interface Usage {
+ successCount: number;
+ errorCount: number;
+ averageDuration: number | null;
+}
+
+export async function registerUsageCollector(
+ usageCollection: UsageCollectionSetup,
+ context: PluginInitializerContext
+) {
+ try {
+ const collector = usageCollection.makeUsageCollector({
+ type: 'search',
+ isReady: () => true,
+ fetch: fetchProvider(context.config.legacy.globalConfig$),
+ schema: {
+ successCount: { type: 'number' },
+ errorCount: { type: 'number' },
+ averageDuration: { type: 'long' },
+ },
+ });
+ usageCollection.registerCollector(collector);
+ } catch (err) {
+ return; // kibana plugin is not enabled (test environment)
+ }
+}
diff --git a/src/plugins/data/server/search/collectors/routes.ts b/src/plugins/data/server/search/collectors/routes.ts
new file mode 100644
index 00000000000000..38fb517e3c3f66
--- /dev/null
+++ b/src/plugins/data/server/search/collectors/routes.ts
@@ -0,0 +1,50 @@
+/*
+ * 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 { schema } from '@kbn/config-schema';
+import { CoreSetup } from '../../../../../core/server';
+import { DataPluginStart } from '../../plugin';
+import { SearchUsage } from './usage';
+
+export function registerSearchUsageRoute(
+ core: CoreSetup