id | slug | title | summary | date | tags | ||||
---|---|---|---|---|---|---|---|---|---|
kibUsageCollectionPlugin |
/kibana-dev-docs/key-concepts/usage-collection-plugin |
Usage collection service |
The Usage Collection Service defines a set of APIs for other plugins to report the usage of their features. |
2021-02-24 |
|
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.
The 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 '../../../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
.
Not 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
- "How many times this threshold has been reached?"
- "What is the trend in usage of this api?"
- "How frequent are users hitting this error per day?"
- "What is the success rate of this operation?"
- "Which option is being selected the most/least?"
To create a usage counter for your plugin, use the API usageCollection.createUsageCounter
as follows:
// server/plugin.ts
import type { Plugin, CoreStart } from '../../../core/server';
import type { UsageCollectionSetup, UsageCounter } from '../../../plugins/usage_collection/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:
- Create the usage collector.
- Register the usage collector.
-
To create the usage collector, the API
usageCollection.makeUsageCollector
expects:type
: the key under which to nest all the usage reported by thefetch
method.schema
: field to define the expected output of thefetch
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 thefetch
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 thefetch
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 itsfetch
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 returnstrue
forisReady
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 yourfetch
method can run without the need of any previous dependencies, then you can return true forisReady
as shown in the example below. -
The
fetch
method needs to support multiple contexts in which it is called. For example, when a user requests the example of what we collect in the Kibana>Advanced Settings>Usage data section, the clients provided in the context of the function (CollectorFetchContext
) are scoped to that user's privileges. The reason is to avoid exposing via telemetry any data that user should not have access to (i.e.: if the user does not have access to certain indices, they shouldn't be allowed to see the number of documents that exists in it). In this case, thefetch
method receives the clientsesClient
andsoClient
scoped to the user who performed the HTTP API request. Alternatively, when requesting the usage data to be reported to the Remote Telemetry Service, the clients are scoped to the internal Kibana user (kibana_system
). Please, mind it might have lower-level access than the default super-adminelastic
test user.
In some scenarios, your collector might need to maintain its own client. An example of that is themonitoring
plugin, that maintains a connection to the Remote Monitoring Cluster to push its monitoring data. If that's the case, your plugin can opt-in to receive the additionalkibanaRequest
parameter by addingextendFetchContext.kibanaRequest: true
to the collector's config: it will be appended to the context of thefetch
method only if the request needs to be scoped to a user other than Kibana Internal, so beware that your collector will need to work for both scenarios (especially for the scenario whenkibanaRequest
is missing).
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:
- the number of API calls
- the number of times users installed and uninstalled the sample datasets
When using incrementCounter
for collecting usage data, 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):
- Swallow any exceptions thrown from the incrementCounter method and log a message in development.
- Don't block your application on the incrementCounter method (e.g.
don't use
await
) - Set the
refresh
option to false to prevent unecessary index refreshes which slows down Elasticsearch performance
Note: 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.
- The
/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 theusage
object of the response named after your usage collector'stype
field. This method tests the Metricbeat scenario described above where the elasticsearch client wraps the call with the request. - There is a dev script in x-pack that will give a sample of a payload of data that gets sent up to the telemetry cluster for the sending phase of telemetry. Collected data comes from:
-
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
-
- In Dev mode, Kibana will send telemetry data to a staging telemetry cluster. Assuming you have access to the staging cluster, you can log in and check the latest documents for your new fields.
- How should I design my data model?
Keep it simple, and keep it to a model that Kibana will be able to understand. Bear in mind the number of keys you are reporting as it may result in fields mapping explosion. Flat arrays, such as arrays of strings are fine. - If I accumulate an event counter in server memory, which my fetch method returns, won't it reset when the Kibana server restarts?
Yes, but that is not a major concern. A visualization on such info might be a date histogram that gets events-per-second or something, which would be impacted by server restarts, so we'll have to offset the beginning of the time range when we detect that the latest metric is smaller than the earliest metric. That would be a pretty custom visualization, but perhaps future Kibana enhancements will be able to support that.
/api/ui_counters/_report
: Used byui_metrics
andui_counters
usage collector instances to report their usage data to the server/api/stats
: Get the metrics and usage (details)