src/platform/plugins/shared/usage_collection/README.mdx
The Usage Collection Service defines a set of APIs for other plugins to report the usage of their features. At the same time, it provides necessary the APIs for other services (i.e.: telemetry, monitoring, ...) to consume that usage data.
IMPORTANT: Usage collection and telemetry applies to internal Elastic Kibana developers only.
The way to report the usage of any feature depends on whether the actions to track occur in the UI, or the usage depends on any server-side data. For that reason, the set of APIs exposed in the public and server contexts are different.
In any case, to use any of these APIs, the plugin must optionally require the plugin usageCollection:
// plugin/kibana.json
{
"id": "...",
"optionalPlugins": ["usageCollection"]
}
Please, be aware that plugins listing usageCollection in the optionalPlugins list are allowed to run even when usageCollection is disabled. However, this also means that it may not be available. Make sure the plugin defines the types of its contract interfaces with usageCollection being optional as well.
public APIsThe APIs exposed in the public context aim to collect the aggregate number of events that occur in a period of time. They are not intended for user-behavioural tracking. The APIs available can be categorized in 2: Application Usage and UI Counters.
Kibana automatically tracks the number of minutes the users spend on each application, as well as the number of general clicks in the same app. There is no need for plugins to opt-in. However, if a plugin needs to collect the same metric for specific sections of the app (i.e.: tabs, flyouts, or any component that may be shown in specific situations), it can use the React component TrackApplicationView. For more info about the app-level and sub-views tracking, please read this collector's README.
Formerly known as UI Metrics, UI Counters provides instrumentation in the UI to count triggered events such as "component loaded", "button clicked", or counting when an event occurs. 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".
The events have a per day granularity.
To track a user interaction, use the API usageCollection.reportUiCounter as follows:
// public/plugin.ts
import { METRIC_TYPE } from '@kbn/analytics';
import { Plugin, CoreStart } from '@kbn/core/public';
export class MyPlugin implements Plugin {
public start(
core: CoreStart,
{ usageCollection }: { usageCollection?: UsageCollectionSetup }
) {
// Call the following method as many times as you want to report an increase in the count for this event
usageCollection?.reportUiCounter(`<AppName>`, METRIC_TYPE.CLICK, `<EventName>`);
}
}
METRIC_TYPE.CLICK for tracking clicks.METRIC_TYPE.LOADED for a component load, a page load, or a request load.METRIC_TYPE.COUNT is the generic counter for miscellaneous events.Call this function whenever you would like to track a user interaction within your app. The function
accepts three arguments, AppName, metricType and eventNames. These should be underscore-delimited strings.
That's all you need to do!
To track multiple metrics within a single request, provide an array of events
usageCollection.reportUiCounter(`<AppName>`, METRIC_TYPE.CLICK, [`<EventName1>`, `<EventName2>`]);
To track an event occurrence more than once in the same call, provide a 4th argument to the reportUiCounter function:
usageCollection.reportUiCounter(`<AppName>`, METRIC_TYPE.CLICK, `<EventName>`, 3);
The colon character (:) should not be used in the app name. Colons play a special role for appName in how metrics are stored as saved objects.
This API is not intended for tracking user-behavioural analytics. However, 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
use a eventName of create_vis_1m, create_vis_5m, create_vis_20m, or create_vis_infinity.
server APIsNot an API as such. However, Data Telemetry collects the usage of known patterns of indices, either via well-known index names (check the list here) or by identifying Elastic internal _meta keys in the index definitions: Beats indices or ingest-manager's maintained Data Streams.
This collector does not report the name of the indices nor any content. It only provides stats about usage of known shippers/ingest tools.
Usage counters allows plugins to report user triggered events from the server. This api has feature parity with UI Counters on the public plugin side of usage_collection.
Usage counters provide instrumentation on the server to count triggered events such as "api called", "threshold reached", and miscellaneous events count.
It is useful for gathering semi-aggregated events with a per day granularity. This allows tracking trends in usage and provides enough granularity for this type of telemetry to provide insights such as
To create a usage counter for your plugin, use the API usageCollection.createUsageCounter as follows:
// server/plugin.ts
import type { Plugin, CoreStart } from '@kbn/core/server';
import type { UsageCollectionSetup, UsageCounter } from '@kbn/usage-collection-plugin/server';
export class MyPlugin implements Plugin {
private usageCounter?: UsageCounter;
public setup(
core: CoreStart,
{ usageCollection }: { usageCollection?: UsageCollectionSetup }
) {
/**
* Create a usage counter for this plugin. Domain ID must be unique.
* It is advised to use the plugin name as the domain ID for most cases.
*/
this.usageCounter = usageCollection?.createUsageCounter('<Domain ID>');
try {
doSomeOperation();
this.usageCounter?.incrementCounter({
counterName: 'doSomeOperation_success',
incrementBy: 1,
});
} catch (err) {
this.usageCounter?.incrementCounter({
counterName: 'doSomeOperation_error',
counterType: 'error',
incrementBy: 1,
});
logger.error(err);
}
}
}
Pass the created usageCounter around in your service to instrument usage.
That's all you need to do! The Usage counters service will handle piping these counters all the way to the telemetry service.
Usage counters are reported inside the telemetry usage payload under stack_stats.kibana.plugins.usage_counters.
{
usage_counters: {
dailyEvents: [
{
domainId: '<Domain ID>',
counterName: 'doSomeOperation_success',
counterType: 'count',
lastUpdatedAt: '2021-11-20T11:43:00.961Z',
fromTimestamp: '2021-11-20T00:00:00Z',
total: 3,
},
{
domainId: '<Domain ID>',
counterName: 'doSomeOperation_success',
counterType: 'count',
lastUpdatedAt: '2021-11-21T10:30:00.961Z',
fromTimestamp: '2021-11-21T00:00:00Z',
total: 5,
},
{
domainId: '<Domain ID>',
counterName: 'doSomeOperation_error',
counterType: 'error',
lastUpdatedAt: '2021-11-20T11:43:00.961Z',
fromTimestamp: '2021-11-20T00:00:00Z',
total: 1,
},
],
},
}
In many cases, plugins need to report the custom usage of a feature. In this cases, the plugins must complete the following 2 steps in the setup lifecycle step:
To create the usage collector, the API usageCollection.makeUsageCollector expects:
type: the key under which to nest all the usage reported by the fetch method.schema: field to define the expected output of the fetch method.isReady: async method (that returns true or false) for letting the usage collection consumers know if they need to wait for any asynchronous action (initialization of clients or other services) before calling the fetch method.fetch: async method for returning the usage collector's data.Once the usage collector is created, it has to be registered to the usage collection set. Otherwise, it won't be used when consumers retrieve the usage collection.
Register Usage collector in the setup function:
// server/plugin.ts
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { Plugin, CoreSetup, CoreStart } from 'src/core/server';
class MyPlugin implements Plugin {
public setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) {
registerMyPluginUsageCollector(plugins.usageCollection);
}
public start(core: CoreStart) {}
}
Creating and registering a Usage Collector. Ideally collectors would be defined in a separate directory server/collectors/register.ts.
// server/collectors/register.ts
import { UsageCollectionSetup, CollectorFetchContext } from 'src/plugins/usage_collection/server';
interface Usage {
my_objects: {
total: number,
},
}
export function registerMyPluginUsageCollector(usageCollection?: UsageCollectionSetup): void {
// usageCollection is an optional dependency, so make sure to return if it is not registered.
if (!usageCollection) {
return;
}
// create usage collector
const myCollector = usageCollection.makeUsageCollector<Usage>({
type: 'MY_USAGE_TYPE',
schema: {
my_objects: {
total: {
type: 'long',
_meta: { description: 'The total number of objects in the cluster created in the last 24h' },
},
},
},
isReady: () => isCollectorFetchReady, // Method to return `true`/`false` or Promise(`true`/`false`) to confirm if the collector is ready for the `fetch` method to be called.
fetch: async (collectorFetchContext: CollectorFetchContext) => {
// query ES or saved objects and get some data
// summarize the data into a model
// return the modeled object that includes whatever you want to track
return {
my_objects: {
total: SOME_NUMBER
}
};
},
});
// register usage collector
usageCollection.registerCollector(myCollector);
}
Some background:
MY_USAGE_TYPE can be any string. It usually matches the plugin name. As a safety mechanism, we double check there are no duplicates at the moment of registering the collector.
isReady (added in v7.2.0 and v6.8.4) is a way for a usage collector to announce that some async process must finish first before it can return data in the fetch method (e.g. a client needs to ne initialized, or the task manager needs to run a task first). If any collector reports that it is not ready when we call its fetch method, we reset a flag to try again and, after a set amount of time, collect data from those collectors that are ready and skip any that are not. This means that if a collector returns true for isReady and it actually isn't ready to return data, there won't be telemetry data from that collector in that telemetry report (usually once per day). You should consider what it means if your collector doesn't return data in the first few documents when Kibana starts or, if we should wait for any other reason (e.g. the task manager needs to run your task first). If you need to tell telemetry collection to wait, you should implement this function with custom logic. If your fetch method can run without the need of any previous dependencies, then you can return true for isReady as shown in the example below.
The clients provided to the fetch method are scoped to the internal Kibana user (kibana_system).
Note: there will be many cases where you won't need to use the esClient or soClient function that gets passed in to your fetch method at all. Your feature might have an accumulating value in server memory, or read something from the OS.
In the case of using a custom ES or SavedObjects client, it is up to the plugin to initialize the client to save the data, and it is strongly recommended scoping that client to the kibana_system user.
The schema field is a proscribed data model assists with detecting changes in usage collector payloads. To define the collector schema add a schema field that specifies every possible field reported (including optional fields) when registering the collector. The schema supports descriptions as simple strings that allow developers to document what the data represents. The _meta field only supports a description property.
schema: {
my_greeting: {
type: 'keyword',
_meta: {
description: 'The greeting shown to the user. It reports only when overwritten by the user.',
}
}
}
Whenever the schema field is set or changed please run node scripts/telemetry_check.js --fix to update the stored schema json files.
The AllowedSchemaTypes is the list of allowed schema types for the usage fields getting reported by the fetch method:
'long', 'integer', 'short', 'byte', 'double', 'float', 'keyword', 'text', 'boolean', 'date'
If any of your properties is an array, the schema definition must follow the convention below:
{ type: 'array', items: {...mySchemaDefinitionOfTheEntriesInTheArray} }
export const myCollector = makeUsageCollector<Usage>({
type: 'my_working_collector',
isReady: () => true, // `fetch` doesn't require any validation for dependencies to be met
fetch() {
return {
my_greeting: 'hello',
some_obj: {
total: 123,
},
some_array: ['value1', 'value2'],
some_array_of_obj: [{total: 123}],
};
},
schema: {
my_greeting: {
type: 'keyword',
_meta: { description: 'The greeting shown to the user. It reports only when overwritten by the user.' }
},
some_obj: {
total: {
type: 'long',
_meta: { description: 'The total count of some_obj since the creation of the cluster' }
},
},
some_array: {
type: 'array',
items: {
type: 'keyword',
_meta: { description: 'Category assigned to ...' }
}
},
some_array_of_obj: {
type: 'array',
items: {
total: {
type: 'long',
_meta: { description: 'The daily total number of items.' }
},
},
},
},
});
There are several ways to collect data that can provide insight into how users
use your plugin or specific features. For tracking user interactions the
SavedObjectsRepository provided by Core provides a useful incrementCounter
method which can be used to increment one or more counter fields in a
document. Examples of interactions include tracking:
When using incrementCounter for collecting usage, you need to ensure
that usage collection happens on a best-effort basis and doesn't
negatively affect your plugin or users (see the example):
await)refresh option to false to prevent unnecessary index refreshes
which slows down Elasticsearch performanceNote: for brevity the following example does not follow Kibana's conventions for structuring your plugin code.
// src/plugins/dashboard/server/plugin.ts
import { PluginInitializerContext, Plugin, CoreStart, CoreSetup } from '../../../../src/core/server';
export class DashboardPlugin implements Plugin {
private readonly logger: Logger;
private readonly isDevEnvironment: boolean;
constructor(initializerContext: PluginInitializerContext) {
this.logger = initializerContext.logger.get();
this.isDevEnvironment = initializerContext.env.cliArgs.dev;
}
public setup(core) {
// Register a saved object type to store our usage counters
core.savedObjects.registerType({
// Don't expose this saved object type via the saved objects HTTP API
hidden: true,
mappings: {
// Since we're not querying or aggregating over our counter documents
// we don't define any fields.
dynamic: false,
properties: {},
},
name: 'dashboard_usage_counters',
namespaceType: 'single',
});
}
public start(core) {
const repository = core.savedObjects.createInternalRepository(['dashboard_usage_counters']);
// Initialize all the counter fields to 0 when our plugin starts
// NOTE: Usage collection happens on a best-effort basis, so we don't
// `await` the promise returned by `incrementCounter` and we swallow any
// exceptions in production.
repository
.incrementCounter('dashboard_usage_counters', 'dashboard_usage_counters', [
'apiCalls',
'settingToggled',
], {refresh: false, initialize: true})
.catch((e) => (this.isDevEnvironment ? this.logger.error(e) : e));
const router = core.http.createRouter();
router.post(
{
path: `api/v1/dashboard/counters/{counter}`,
validate: {
params: schema.object({
counter: schema.oneOf([schema.literal('apiCalls'), schema.literal('settingToggled')]),
}),
},
},
async (context, request, response) => {
request.params.id
// NOTE: Usage collection happens on a best-effort basis, so we don't
// `await` the promise returned by `incrementCounter` and we swallow any
// exceptions in production.
repository
.incrementCounter('dashboard_usage_counters', 'dashboard_usage_counters', [
counter
], {refresh: false})
.catch((e) => (this.isDevEnvironement ? this.logger.error(e) : e));
return response.ok();
}
);
}
}
There are a few ways you can test that your usage collector is working properly.
/api/stats?extended=true&legacy=true HTTP API in Kibana (added in 6.4.0) will call the fetch methods of all the registered collectors, and add them to a stats object you can see in a browser or in curl. To test that your usage collector has been registered correctly and that it has the model of data you expected it to have, call that HTTP API manually and you should see a key in the usage object of the response named after your usage collector's type field. This method tests the Metricbeat scenario described above where the elasticsearch client wraps the call with the request.The .monitoring-* indices, when Monitoring is enabled. Monitoring enhances the sent payload of telemetry by producing usage data potentially of multiple clusters that exist in the monitoring data. Monitoring data is time-based, and the time frame of collection is the last 15 minutes.
Live-pulled from ES API endpoints. This will get just real-time stats without context of historical data.
The dev script in x-pack can be run on the command-line with:
cd x-pack
node scripts/api_debug.js telemetry --host=http://localhost:5601
Where http://localhost:5601 is a Kibana server running in dev mode. If needed, authentication and basePath info can be provided in the command as well.
Automatic inclusion of all the stats fetched by collectors is added in #22336 / 6.5.0
/api/ui_counters/_report: Used by ui_metrics and ui_counters usage collector instances to report their usage data to the server/api/stats: Get the metrics and usage (details)