Add collector to ui_metric app so that it reports its own telemetry. (#34210)

* Add collector to ui_metric app so that it reports its own telemetry, so app maintainers don't need to update their apps' collectors.
* Remove UI metrics and some collectors from Index Manamgement, ILM, and Rollups.
This commit is contained in:
CJ Cenizal 2019-04-02 14:41:39 -07:00 committed by GitHub
parent 3ebc1ae941
commit a6ea8474c5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 137 additions and 205 deletions

View file

@ -0,0 +1,57 @@
# UI Metric app
## Purpose
The purpose of the UI Metric app is to provide a tool for gathering data on how users interact with
various UIs within Kibana. It's useful for gathering _aggregate_ information, e.g. "How many times
has Button X been clicked" or "How many times has Page Y been viewed".
With some finagling, it's even possible to add more meaning to the info you gather, such as "How many
visualizations were created in less than 5 minutes".
### What it doesn't do
The UI Metric app doesn't gather any metadata around a user interaction, e.g. the user's identity,
the name of a dashboard they've viewed, or the timestamp of the interaction.
## How to use it
To track a user interaction, simply send a `POST` request to `/api/ui_metric/{APP_NAME}/{ACTION_NAME}`,
where `APP_MAME` and `ACTION_NAME` are underscore-delimited strings, e.g. `my_app` and `my_action`.
That's all you need to do!
### Tracking timed interactions
If you want to track how long it takes a user to do something, you'll need to implement the timing
logic yourself. You'll also need to predefine some buckets into which the UI metric can fall.
For example, if you're timing how long it takes to create a visualization, you may decide to
measure interactions that take less than 1 minute, 1-5 minutes, 5-20 minutes, and longer than 20 minutes.
To track these interactions, you'd use the timed length of the interaction to determine whether to
hit `/api/ui_metric/visualize/create_vis_1m`, `/api/ui_metric/visualize/create_vis_5m`,
`/api/ui_metric/visualize/create_vis_20m`, etc.
## How it works
Under the hood, your app and action will be stored in a saved object of type `user-metric` and the
ID `my_app:my_action`. This saved object will have a `count` property which will be incremented every
time the above URI is hit.
These saved objects are automatically consumed by the stats API and surfaced under the
`ui_metric` namespace.
```json
{
"ui_metric":{
"my_app":[
{
"key":"my_action",
"value":3
}
]
}
}
```
By storing these actions and their counts as key-value pairs, we can add more actions without having
to worry about exceeding the 1000-field soft limit in Elasticsearch.

View file

@ -18,6 +18,7 @@
*/
import { registerUserActionRoute } from './server/routes/api/ui_metric';
import { registerUiMetricUsageCollector } from './server/usage/index';
export default function (kibana) {
return new kibana.Plugin({
@ -30,6 +31,7 @@ export default function (kibana) {
init: function (server) {
registerUserActionRoute(server);
registerUiMetricUsageCollector(server);
}
});
}

View file

@ -0,0 +1,58 @@
/*
* 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.
*/
const UI_METRIC_USAGE_TYPE = 'ui_metric';
export function registerUiMetricUsageCollector(server: any) {
const collector = server.usage.collectorSet.makeUsageCollector({
type: UI_METRIC_USAGE_TYPE,
fetch: async (callCluster: any) => {
const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects;
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
const savedObjectsClient = new SavedObjectsClient(internalRepository);
const { saved_objects: rawUiMetrics } = await savedObjectsClient.find({
type: 'ui-metric',
fields: ['count'],
});
const uiMetricsByAppName = rawUiMetrics.reduce((accum: any, rawUiMetric: any) => {
const {
id,
attributes: { count },
} = rawUiMetric;
const [appName, metricType] = id.split(':');
if (!accum[appName]) {
accum[appName] = [];
}
const pair = { key: metricType, value: count };
accum[appName].push(pair);
return accum;
}, {});
return uiMetricsByAppName;
},
});
server.usage.collectorSet.register(collector);
}

View file

@ -0,0 +1,20 @@
/*
* 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 { registerUiMetricUsageCollector } from './collector';

View file

@ -8,7 +8,6 @@ export const BASE_PATH = '/management/elasticsearch/index_lifecycle_management/'
export const PLUGIN_ID = 'index_lifecycle_management';
export {
UIM_APP_NAME,
USER_ACTIONS,
UIM_APP_LOAD,
UIM_POLICY_CREATE,
UIM_POLICY_UPDATE,

View file

@ -19,19 +19,3 @@ export const UIM_CONFIG_SET_PRIORITY = 'config_set_priority';
export const UIM_CONFIG_FREEZE_INDEX = 'config_freeze_index';
export const UIM_INDEX_RETRY_STEP = 'index_retry_step';
export const UIM_EDIT_CLICK = 'edit_click';
export const USER_ACTIONS = [
UIM_APP_LOAD,
UIM_POLICY_CREATE,
UIM_POLICY_UPDATE,
UIM_POLICY_DELETE,
UIM_POLICY_ATTACH_INDEX,
UIM_POLICY_ATTACH_INDEX_TEMPLATE,
UIM_POLICY_DETACH_INDEX,
UIM_CONFIG_COLD_PHASE,
UIM_CONFIG_WARM_PHASE,
UIM_CONFIG_SET_PRIORITY,
UIM_CONFIG_FREEZE_INDEX,
UIM_INDEX_RETRY_STEP,
UIM_EDIT_CLICK,
];

View file

@ -13,7 +13,6 @@ import { registerIndexRoutes } from './server/routes/api/index';
import { registerLicenseChecker } from './server/lib/register_license_checker';
import { PLUGIN_ID } from './common/constants';
import { indexLifecycleDataEnricher } from './index_lifecycle_data';
import { registerIndexLifecycleManagementUsageCollector } from './server/usage';
export function indexLifecycleManagement(kibana) {
return new kibana.Plugin({
@ -54,7 +53,6 @@ export function indexLifecycleManagement(kibana) {
registerPoliciesRoutes(server);
registerLifecycleRoutes(server);
registerIndexRoutes(server);
registerIndexLifecycleManagementUsageCollector(server);
if (
server.config().get('xpack.ilm.ui.enabled') &&

View file

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { fetchUiMetrics } from '../../../../server/lib/ui_metric';
import { UIM_APP_NAME, USER_ACTIONS } from '../../common/constants';
const INDEX_LIFECYCLE_MANAGEMENT_USAGE_TYPE = 'index_lifecycle_management';
export function registerIndexLifecycleManagementUsageCollector(server) {
const collector = server.usage.collectorSet.makeUsageCollector({
type: INDEX_LIFECYCLE_MANAGEMENT_USAGE_TYPE,
fetch: async () => {
const uiMetrics = await fetchUiMetrics(server, UIM_APP_NAME, USER_ACTIONS);
return {
ui_metrics: uiMetrics,
};
},
});
server.usage.collectorSet.register(collector);
}

View file

@ -1,7 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { registerIndexLifecycleManagementUsageCollector } from './collector';

View file

@ -11,7 +11,6 @@ export * from './index_statuses';
export {
UIM_APP_NAME,
UIM_APP_LOAD,
USER_ACTIONS,
UIM_UPDATE_SETTINGS,
UIM_INDEX_CLEAR_CACHE,
UIM_INDEX_CLEAR_CACHE_MANY,

View file

@ -33,33 +33,3 @@ export const UIM_DETAIL_PANEL_MAPPING_TAB = 'detail_panel_mapping_tab';
export const UIM_DETAIL_PANEL_SETTINGS_TAB = 'detail_panel_settings_tab';
export const UIM_DETAIL_PANEL_STATS_TAB = 'detail_panel_stats_tab';
export const UIM_DETAIL_PANEL_SUMMARY_TAB = 'detail_panel_summary_tab';
export const USER_ACTIONS = [
UIM_APP_LOAD,
UIM_UPDATE_SETTINGS,
UIM_INDEX_CLEAR_CACHE,
UIM_INDEX_CLEAR_CACHE_MANY,
UIM_INDEX_CLOSE,
UIM_INDEX_CLOSE_MANY,
UIM_INDEX_DELETE,
UIM_INDEX_DELETE_MANY,
UIM_INDEX_FLUSH,
UIM_INDEX_FLUSH_MANY,
UIM_INDEX_FORCE_MERGE,
UIM_INDEX_FORCE_MERGE_MANY,
UIM_INDEX_FREEZE,
UIM_INDEX_FREEZE_MANY,
UIM_INDEX_OPEN,
UIM_INDEX_OPEN_MANY,
UIM_INDEX_REFRESH,
UIM_INDEX_REFRESH_MANY,
UIM_INDEX_SETTINGS_EDIT,
UIM_INDEX_UNFREEZE,
UIM_INDEX_UNFREEZE_MANY,
UIM_SHOW_DETAILS_CLICK,
UIM_DETAIL_PANEL_EDIT_SETTINGS_TAB,
UIM_DETAIL_PANEL_MAPPING_TAB,
UIM_DETAIL_PANEL_SETTINGS_TAB,
UIM_DETAIL_PANEL_STATS_TAB,
UIM_DETAIL_PANEL_SUMMARY_TAB,
];

View file

@ -13,7 +13,6 @@ import { registerStatsRoute } from './server/routes/api/stats';
import { registerLicenseChecker } from '../../server/lib/register_license_checker';
import { PLUGIN } from './common/constants';
import { addIndexManagementDataEnricher } from './index_management_data';
import { registerIndexManagementUsageCollector } from './server/usage';
export function indexManagement(kibana) {
return new kibana.Plugin({
@ -35,7 +34,6 @@ export function indexManagement(kibana) {
registerSettingsRoutes(router);
registerStatsRoute(router);
registerMappingRoute(router);
registerIndexManagementUsageCollector(server);
}
});
}

View file

@ -1,25 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { fetchUiMetrics } from '../../../../server/lib/ui_metric';
import { UIM_APP_NAME, USER_ACTIONS } from '../../common/constants';
const INDEX_MANAGEMENT_USAGE_TYPE = 'index_management';
export function registerIndexManagementUsageCollector(server) {
const collector = server.usage.collectorSet.makeUsageCollector({
type: INDEX_MANAGEMENT_USAGE_TYPE,
fetch: async () => {
const uiMetrics = await fetchUiMetrics(server, UIM_APP_NAME, USER_ACTIONS);
return {
ui_metrics: uiMetrics,
};
},
});
server.usage.collectorSet.register(collector);
}

View file

@ -1,7 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { registerIndexManagementUsageCollector } from './collector';

View file

@ -12,7 +12,6 @@ export const CONFIG_ROLLUPS = 'rollups:enableIndexPatterns';
export {
UIM_APP_NAME,
USER_ACTIONS,
UIM_APP_LOAD,
UIM_JOB_CREATE,
UIM_JOB_DELETE,

View file

@ -20,20 +20,3 @@ export const UIM_DETAIL_PANEL_TERMS_TAB_CLICK = 'detail_panel_terms_tab_click';
export const UIM_DETAIL_PANEL_HISTOGRAM_TAB_CLICK = 'detail_panel_histogram_tab_click';
export const UIM_DETAIL_PANEL_METRICS_TAB_CLICK = 'detail_panel_metrics_tab_click';
export const UIM_DETAIL_PANEL_JSON_TAB_CLICK = 'detail_panel_json_tab_click';
export const USER_ACTIONS = [
UIM_APP_LOAD,
UIM_JOB_CREATE,
UIM_JOB_DELETE,
UIM_JOB_DELETE_MANY,
UIM_JOB_START,
UIM_JOB_START_MANY,
UIM_JOB_STOP,
UIM_JOB_STOP_MANY,
UIM_SHOW_DETAILS_CLICK,
UIM_DETAIL_PANEL_SUMMARY_TAB_CLICK,
UIM_DETAIL_PANEL_TERMS_TAB_CLICK,
UIM_DETAIL_PANEL_HISTOGRAM_TAB_CLICK,
UIM_DETAIL_PANEL_METRICS_TAB_CLICK,
UIM_DETAIL_PANEL_JSON_TAB_CLICK,
];

View file

@ -5,8 +5,6 @@
*/
import { get } from 'lodash';
import { fetchUiMetrics } from '../../../../server/lib/ui_metric';
import { UIM_APP_NAME, USER_ACTIONS } from '../../common';
const ROLLUP_USAGE_TYPE = 'rollups';
@ -182,8 +180,6 @@ export function registerRollupUsageCollector(server) {
rollupVisualizationsFromSavedSearches,
} = await fetchRollupVisualizations(kibanaIndex, callCluster, rollupIndexPatternToFlagMap, rollupSavedSearchesToFlagMap);
const uiMetrics = await fetchUiMetrics(server, UIM_APP_NAME, USER_ACTIONS);
return {
index_patterns: {
total: rollupIndexPatterns.length,
@ -197,7 +193,6 @@ export function registerRollupUsageCollector(server) {
total: rollupVisualizationsFromSavedSearches,
},
},
ui_metrics: uiMetrics,
};
},
});

View file

@ -1,59 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import { Server } from 'hapi';
export interface UiMetricRecord {
metricType: string;
count: number;
}
export interface UiMetricAndCountKeyValuePair {
key: string;
value: number;
}
// This is a helper method for retrieving user interaction telemetry data stored via the OSS
// ui_metric API.
export function fetchUiMetrics(
server: Server,
appName: string,
metricTypes: string[]
): Promise<UiMetricAndCountKeyValuePair[]> {
const { SavedObjectsClient, getSavedObjectsRepository } = server.savedObjects;
const { callWithInternalUser } = server.plugins.elasticsearch.getCluster('admin');
const internalRepository = getSavedObjectsRepository(callWithInternalUser);
const savedObjectsClient = new SavedObjectsClient(internalRepository);
async function fetchUiMetric(metricType: string): Promise<UiMetricRecord | undefined> {
try {
const savedObjectId = `${appName}:${metricType}`;
const savedObject = await savedObjectsClient.get('ui-metric', savedObjectId);
return { metricType, count: savedObject.attributes.count };
} catch (error) {
return undefined;
}
}
return Promise.all(metricTypes.map(fetchUiMetric)).then(
(userActions): UiMetricAndCountKeyValuePair[] => {
const userActionAndCountKeyValuePairs = userActions.reduce(
(pairs: UiMetricAndCountKeyValuePair[], uiMetric: UiMetricRecord | undefined) => {
// UI metric is undefined if nobody has performed this interaction on the client yet.
if (uiMetric !== undefined) {
const { metricType, count } = uiMetric;
const pair: UiMetricAndCountKeyValuePair = { key: metricType, value: count };
pairs.push(pair);
}
return pairs;
},
[]
);
return userActionAndCountKeyValuePairs;
}
);
}

View file

@ -1,7 +0,0 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
export { fetchUiMetrics } from './fetch_ui_metrics';