Skip to content

OpenSearch Dashboards Usage Collection Service

Usage Collection allows collecting usage data for other services to consume (telemetry and monitoring). To integrate with the telemetry services for usage collection of your feature, there are 2 steps:

  1. Create a usage collector.
  2. Register the usage collector.

Creating and Registering Usage Collector

All you need to provide is a type for organizing your fields, schema field to define the expected types of usage fields reported, and a fetch method for returning your usage data. Then you need to make the Telemetry service aware of the collector by registering it.

  1. Make sure usageCollection is in your optional Plugins:

    json // plugin/opensearch_dashboards.json { "id": "...", "optionalPlugins": ["usageCollection"] }

  2. Register Usage collector in the setup function:

    ```ts // server/plugin.ts import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { CoreSetup, CoreStart } from 'opensearch-dashboards/server';

    class Plugin { public setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) { registerMyPluginUsageCollector(plugins.usageCollection); }

    public start(core: CoreStart) {} } ```

  3. Creating and registering a Usage Collector. Ideally collectors would be defined in a separate directory server/collectors/register.ts.

    ```ts // server/collectors/register.ts import { UsageCollectionSetup } from 'src/plugins/usage_collection/server'; import { APICluster } from 'opensearch-dashboards/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({ type: 'MY_USAGE_TYPE', schema: { my_objects: { total: 'long', }, }, fetch: async (callCluster: APICluster, opensearchClient: IClusterClient) => {

    // query OpenSearch 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.
  • The fetch method needs to support multiple contexts in which it is called. For example, when stats are pulled from a OpenSearch Dashboards Metricbeat module, the Beat calls OpenSearch Dashboards's stats API to invoke usage collection. In this case, the fetch method is called as a result of an HTTP API request and callCluster wraps callWithRequest or opensearchClient wraps asCurrentUser, where the request headers are expected to have read privilege on the entire `.opensearch_dashboards' index.

Note: there will be many cases where you won't need to use the callCluster (or opensearchClient) 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, or use other clients like a custom SavedObjects client. In that case it's up to the plugin to initialize those clients like the example below:

// server/plugin.ts
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';
import { CoreSetup, CoreStart } from 'opensearch-dashboards/server';

class Plugin {
  private savedObjectsRepository?: ISavedObjectsRepository;

  public setup(core: CoreSetup, plugins: { usageCollection?: UsageCollectionSetup }) {
    registerMyPluginUsageCollector(() => this.savedObjectsRepository, plugins.usageCollection);
  }

  public start(core: CoreStart) {
    this.savedObjectsRepository = core.savedObjects.createInternalRepository();
  }
}
// server/collectors/register.ts
import { UsageCollectionSetup } from 'src/plugins/usage_collection/server';

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>(...)

  // register usage collector
  usageCollection.registerCollector(myCollector);
}

Schema Field

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 when registering the collector. Whenever the schema field is set or changed please run node scripts/telemetry_check.js --fix to update the stored schema json files.

Allowed Schema Types

The AllowedSchemaTypes is the list of allowed schema types for the usage fields getting reported:

'keyword', 'text', 'number', 'boolean', 'long', 'date', 'float'

Arrays

If any of your properties is an array, the schema definition must follow the convention below:

{ type: 'array', items: {...mySchemaDefinitionOfTheEntriesInTheArray} }

Example

export const myCollector = makeUsageCollector<Usage>({
  type: 'my_working_collector',
  isReady: () => true,
  fetch() {
    return {
      my_greeting: 'hello',
      some_obj: {
        total: 123,
      },
      some_array: ['value1', 'value2'],
      some_array_of_obj: [{total: 123}],
    };
  },
  schema: {
    my_greeting: {
      type: 'keyword',
    },
    some_obj: {
      total: {
        type: 'number',
      },
    },
    some_array: {
      type: 'array',
      items: { type: 'keyword' }    
    },
    some_array_of_obj: {
      type: 'array',
      items: { 
        total: {
          type: 'number',
        },
      },   
    },
  },
});

Update the telemetry payload and telemetry cluster field mappings

There is a module in the telemetry service that creates the payload of data that gets sent up to the telemetry cluster.

New fields added to the telemetry payload currently mean that telemetry cluster field mappings have to be updated, so they can be searched and aggregated in OpenSearch Dashboards visualizations. This is also a short-term obligation. In the next refactoring phase, collectors will need to use a proscribed data model that eliminates maintenance of mappings in the telemetry cluster.

Testing

There are a few ways you can test that your usage collector is working properly.

  1. The /api/stats?extended=true&legacy=true HTTP API in OpenSearch Dashboards (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 callCluster wraps callWithRequest.
  2. In Dev mode, OpenSearch Dashboards 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.
  3. If you catch the network traffic coming from your browser when a telemetry payload is sent, you can examine the request payload body to see the data. This can be tricky as telemetry payloads are sent only once per day per browser. Use incognito mode or clear your localStorage data to force a telemetry payload.

FAQ

  1. How should I design my data model?
    Keep it simple, and keep it to a model that OpenSearch Dashboards will be able to understand. In short, that means don't rely on nested fields (arrays with objects). Flat arrays, such as arrays of strings are fine.
  2. If I accumulate an event counter in server memory, which my fetch method returns, won't it reset when the OpenSearch Dashboards 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 OpenSearch Dashboards enhancements will be able to support that.

UI Metric app

The UI metrics implementation in its current state is not useful. We are working on improving the implementation to enable teams to use the data to visualize and gather information from what is being reported. Please refer to the telemetry team if you are interested in adding ui_metrics to your plugin.

Until a better implementation is introduced, please defer from adding any new ui metrics.

Purpose

The purpose of the UI Metric app is to provide a tool for gathering data on how users interact with various UIs within OpenSearch Dashboards. 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, use the reportUiStats method exposed by the plugin usageCollection in the public side:

  1. Similarly to the server-side usage collection, make sure usageCollection is in your optional Plugins:

    json // plugin/opensearch_dashboards.json { "id": "...", "optionalPlugins": ["usageCollection"] }

  2. Register Usage collector in the setup function:

    ts // public/plugin.ts class Plugin { setup(core, { usageCollection }) { if (usageCollection) { // Call the following method as many times as you want to report an increase in the count for this event usageCollection.reportUiStats(`<AppName>`, usageCollection.METRIC_TYPE.CLICK, `<EventName>`); } } }

Metric Types:

  • METRIC_TYPE.CLICK for tracking clicks trackMetric(METRIC_TYPE.CLICK, 'my_button_clicked');
  • METRIC_TYPE.LOADED for a component load or page load trackMetric(METRIC_TYPE.LOADED', 'my_component_loaded');
  • METRIC_TYPE.COUNT for a tracking a misc count trackMetric(METRIC_TYPE.COUNT', 'my_counter', <count> });

Call this function whenever you would like to track a user interaction within your app. The function accepts two arguments, metricType and eventNames. These should be underscore-delimited strings. For example, to track the my_event metric in the app my_app call trackUiMetric(METRIC_TYPE.*, 'my_event).

That's all you need to do!

To track multiple metrics within a single request, provide an array of events, e.g. trackMetric(METRIC_TYPE.*, ['my_event1', 'my_event2', 'my_event3']).

Disallowed characters

The colon character (:) should not be used in app name or event names. Colons play a special role in how metrics are stored as saved objects.

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 use a eventName of create_vis_1m, create_vis_5m, create_vis_20m, or create_vis_infinity.

How it works

Under the hood, your app and metric type will be stored in a saved object of type user-metric and the ID ui-metric:my_app:my_metric. 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.

{
  "ui_metric": {
    "my_app": [
      {
        "key": "my_metric",
        "value": 3
      }
    ]
  }
}

By storing these metrics and their counts as key-value pairs, we can add more metrics without having to worry about exceeding the 1000-field soft limit in OpenSearch.

The only caveat is that it makes it harder to consume in OpenSearch Dashboards when analysing each entry in the array separately. In the telemetry team we are working to find a solution to this. We are building a new way of reporting telemetry called Pulse that will help on making these UI-Metrics easier to consume.

Routes registered by this plugin

  • /api/ui_metric/report: Used by ui_metrics usage collector instances to report their usage data to the server
  • /api/stats: Get the metrics and usage (details)