Kibana Core APIedit

This functionality is in technical preview and may be changed or removed in a future release. Elastic will work to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.

Kibana Core provides a set of low-level API’s required to run all Kibana plugins. These API’s are injected into your plugin’s lifecycle methods and may be invoked during that lifecycle only:

import type { PluginInitializerContext, CoreSetup, CoreStart } from 'kibana/server';

export class MyPlugin {
  constructor(initializerContext: PluginInitializerContext) {}

  public setup(core: CoreSetup) {
    // called when plugin is setting up during Kibana's startup sequence
  }

  public start(core: CoreStart) {
    // called after all plugins are set up
  }

  public stop() {
    // called when plugin is torn down during Kibana's shutdown sequence
  }
}

Server-sideedit

Configuration serviceedit

Kibana provides ConfigService if a plugin developer may want to support adjustable runtime behavior for their plugins. Plugins can only read their own configuration values, it is not possible to access the configuration values from Kibana Core or other plugins directly.

// in Legacy platform
const basePath = config.get('server.basePath');
// in Kibana Platform 'basePath' belongs to the http service
const basePath = core.http.basePath.get(request);

To have access to your plugin config, you should:

  • Declare plugin-specific configPath (will fallback to plugin id if not specified) in kibana.json manifest file.
  • Export schema validation for the config from plugin’s main file. Schema is mandatory. If a plugin reads from the config without schema declaration, ConfigService will throw an error.

my_plugin/server/index.ts

import { schema, TypeOf } from '@kbn/config-schema';
export const plugin = …
export const config = {
  schema: schema.object(…),
};
export type MyPluginConfigType = TypeOf<typeof config.schema>;
  • Read config value exposed via PluginInitializerContext. my_plugin/server/index.ts
import type { PluginInitializerContext } from 'kibana/server';
export class MyPlugin {
  constructor(initializerContext: PluginInitializerContext) {
    this.config$ = initializerContext.config.create<MyPluginConfigType>();
    // or if config is optional:
    this.config$ = initializerContext.config.createIfExists<MyPluginConfigType>();
  }

If your plugin also has a client-side part, you can also expose configuration properties to it using the configuration exposeToBrowser allow-list property.

my_plugin/server/index.ts

import { schema, TypeOf } from '@kbn/config-schema';
import type { PluginConfigDescriptor } from 'kibana/server';

const configSchema = schema.object({
  secret: schema.string({ defaultValue: 'Only on server' }),
  uiProp: schema.string({ defaultValue: 'Accessible from client' }),
});

type ConfigType = TypeOf<typeof configSchema>;

export const config: PluginConfigDescriptor<ConfigType> = {
  exposeToBrowser: {
    uiProp: true,
  },
  schema: configSchema,
};

Configuration containing only the exposed properties will be then available on the client-side using the plugin’s initializerContext:

my_plugin/public/index.ts

interface ClientConfigType {
  uiProp: string;
}

export class MyPlugin implements Plugin<PluginSetup, PluginStart> {
  constructor(private readonly initializerContext: PluginInitializerContext) {}

  public async setup(core: CoreSetup, deps: {}) {
    const config = this.initializerContext.config.get<ClientConfigType>();
  }

All plugins are considered enabled by default. If you want to disable your plugin, you could declare the enabled flag in the plugin config. This is a special Kibana Platform key. Kibana reads its value and won’t create a plugin instance if enabled: false.

export const config = {
  schema: schema.object({ enabled: schema.boolean({ defaultValue: false }) }),
};
Handle plugin configuration deprecationsedit

If your plugin has deprecated configuration keys, you can describe them using the deprecations config descriptor field. Deprecations are managed on a per-plugin basis, meaning you don’t need to specify the whole property path, but use the relative path from your plugin’s configuration root.

my_plugin/server/index.ts

import { schema, TypeOf } from '@kbn/config-schema';
import type { PluginConfigDescriptor } from 'kibana/server';

const configSchema = schema.object({
  newProperty: schema.string({ defaultValue: 'Some string' }),
});

type ConfigType = TypeOf<typeof configSchema>;

export const config: PluginConfigDescriptor<ConfigType> = {
  schema: configSchema,
  deprecations: ({ rename, unused }) => [
    rename('oldProperty', 'newProperty'),
    unused('someUnusedProperty'),
  ],
};

In some cases, accessing the whole configuration for deprecations is necessary. For these edge cases, renameFromRoot and unusedFromRoot are also accessible when declaring deprecations.

my_plugin/server/index.ts

export const config: PluginConfigDescriptor<ConfigType> = {
  schema: configSchema,
  deprecations: ({ renameFromRoot, unusedFromRoot }) => [
    renameFromRoot('oldplugin.property', 'myplugin.property'),
    unusedFromRoot('oldplugin.deprecated'),
  ],
};

Logging serviceedit

Allows a plugin to provide status and diagnostic information. For detailed instructions see the logging service documentation.

import type { PluginInitializerContext, CoreSetup, Plugin, Logger } from 'kibana/server';

export class MyPlugin implements Plugin {
  private readonly logger: Logger;

  constructor(initializerContext: PluginInitializerContext) {
    this.logger = initializerContext.logger.get();
  }

  public setup(core: CoreSetup) {
    try {
      this.logger.debug('doing something...');
      // …
    } catch (e) {
      this.logger.error('failed doing something...');
    }
  }
}

Elasticsearch serviceedit

Elasticsearch service provides elasticsearch.client program API to communicate with Elasticsearch server REST API. elasticsearch.client interacts with Elasticsearch service on behalf of:

  • kibana_system user via elasticsearch.client.asInternalUser.* methods.
  • a current end-user via elasticsearch.client.asCurrentUser.* methods. In this case Elasticsearch client should be given the current user credentials. See Scoped services and Security.

Elasticsearch service API docs

import { CoreStart, Plugin } from 'kibana/public';

export class MyPlugin implements Plugin {
  public start(core: CoreStart) {
    async function asyncTask() {
      const result = await core.elasticsearch.client.asInternalUser.ping(…);
    }
    asyncTask();
  }
}

For advanced use-cases, such as a search, use Data plugin

Saved Objects serviceedit

Saved Objects service allows Kibana plugins to use Elasticsearch like a primary database. Think of it as an Object Document Mapper for Elasticsearch. Once a plugin has registered one or more Saved Object types, the Saved Objects client can be used to query or perform create, read, update and delete operations on each type.

By using Saved Objects your plugin can take advantage of the following features:

  • Migrations can evolve your document’s schema by transforming documents and ensuring that the field mappings on the index are always up to date.
  • a HTTP API is automatically exposed for each type (unless hidden=true is specified).
  • a Saved Objects client that can be used from both the server and the browser.
  • Users can import or export Saved Objects using the Saved Objects management UI or the Saved Objects import/export API.
  • By declaring references, an object’s entire reference graph will be exported. This makes it easy for users to export e.g. a dashboard object and have all the visualization objects required to display the dashboard included in the export.
  • When the X-Pack security and spaces plugins are enabled these transparently provide RBAC access control and the ability to organize Saved Objects into spaces.

This document contains developer guidelines and best-practices for plugins wanting to use Saved Objects.

Registering a Saved Object typeedit

Saved object type definitions should be defined in their own my_plugin/server/saved_objects directory.

The folder should contain a file per type, named after the snake_case name of the type, and an index.ts file exporting all the types.

src/plugins/my_plugin/server/saved_objects/dashboard_visualization.ts.

import { SavedObjectsType } from 'src/core/server';

export const dashboardVisualization: SavedObjectsType = {
  name: 'dashboard_visualization', 
  hidden: false,
  namespaceType: 'single',
  mappings: {
    dynamic: false,
    properties: {
      description: {
        type: 'text',
      },
      hits: {
        type: 'integer',
      },
    },
  },
  migrations: {
    '1.0.0': migratedashboardVisualizationToV1,
    '2.0.0': migratedashboardVisualizationToV2,
  },
};

Since the name of a Saved Object type forms part of the url path for the public Saved Objects HTTP API, these should follow our API URL path convention and always be written as snake case.

src/plugins/my_plugin/server/saved_objects/index.ts.

export { dashboardVisualization } from './dashboard_visualization';
export { dashboard } from './dashboard';

src/plugins/my_plugin/server/plugin.ts.

import { dashboard, dashboardVisualization } from './saved_objects';

export class MyPlugin implements Plugin {
  setup({ savedObjects }) {
    savedObjects.registerType(dashboard);
    savedObjects.registerType(dashboardVisualization);
  }
}

Mappingsedit

Each Saved Object type can define it’s own Elasticsearch field mappings. Because multiple Saved Object types can share the same index, mappings defined by a type will be nested under a top-level field that matches the type name.

For example, the mappings defined by the dashboard_visualization Saved Object type:

src/plugins/my_plugin/server/saved_objects/dashboard_visualization.ts.

import { SavedObjectsType } from 'src/core/server';

export const dashboardVisualization: SavedObjectsType = {
  name: 'dashboard_visualization',
  ...
  mappings: {
    properties: {
      dynamic: false,
      description: {
        type: 'text',
      },
      hits: {
        type: 'integer',
      },
    },
  },
  migrations: { ... },
};

Will result in the following mappings being applied to the .kibana index:

{
  "mappings": {
    "dynamic": "strict",
    "properties": {
      ...
      "dashboard_vizualization": {
        "dynamic": false,
        "properties": {
          "description": {
            "type": "text",
          },
          "hits": {
            "type": "integer",
          },
        },
      }
    }
  }
}

Do not use field mappings like you would use data types for the columns of a SQL database. Instead, field mappings are analogous to a SQL index. Only specify field mappings for the fields you wish to search on or query. By specifying dynamic: false in any level of your mappings, Elasticsearch will accept and store any other fields even if they are not specified in your mappings.

Since Elasticsearch has a default limit of 1000 fields per index, plugins should carefully consider the fields they add to the mappings. Similarly, Saved Object types should never use dynamic: true as this can cause an arbitrary amount of fields to be added to the .kibana index.

Referencesedit

When a Saved Object declares references to other Saved Objects, the Saved Objects Export API will automatically export the target object with all of it’s references. This makes it easy for users to export the entire reference graph of an object.

If a Saved Object can’t be used on it’s own, that is, it needs other objects to exist for a feature to function correctly, that Saved Object should declare references to all the objects it requires. For example, a dashboard object might have panels for several visualization objects. When these visualization objects don’t exist, the dashboard cannot be rendered correctly. The dashboard object should declare references to all it’s visualizations.

However, visualization objects can continue to be rendered or embedded into other dashboards even if the dashboard it was originally embedded into doesn’t exist. As a result, visualization objects should not declare references to dashboard objects.

For each referenced object, an id, type and name are added to the references array:

router.get(
  { path: '/some-path', validate: false },
  async (context, req, res) => {
    const object = await context.core.savedObjects.client.create(
      'dashboard',
      {
        title: 'my dashboard',
        panels: [
          { visualization: 'vis1' }, 
        ],
        indexPattern: 'indexPattern1'
      },
      { references: [
          { id: '...', type: 'visualization', name: 'vis1' },
          { id: '...', type: 'index_pattern', name: 'indexPattern1' },
        ]
      }
    )
    ...
  }
);

Note how dashboard.panels[0].visualization stores the name property of the reference (not the id directly) to be able to uniquely identify this reference. This guarantees that the id the reference points to always remains up to date. If a visualization id was directly stored in dashboard.panels[0].visualization there is a risk that this id gets updated without updating the reference in the references array.

Writing Migrationsedit

Saved Objects support schema changes between Kibana versions, which we call migrations. Migrations are applied when a Kibana installation is upgraded from one version to the next, when exports are imported via the Saved Objects Management UI, or when a new object is created via the HTTP API.

Each Saved Object type may define migrations for its schema. Migrations are specified by the Kibana version number, receive an input document, and must return the fully migrated document to be persisted to Elasticsearch.

Let’s say we want to define two migrations: - In version 1.1.0, we want to drop the subtitle field and append it to the title - In version 1.4.0, we want to add a new id field to every panel with a newly generated UUID.

First, the current mappings should always reflect the latest or "target" schema. Next, we should define a migration function for each step in the schema evolution:

src/plugins/my_plugin/server/saved_objects/dashboard_visualization.ts

import { SavedObjectsType, SavedObjectMigrationFn } from 'src/core/server';
import uuid from 'uuid';

interface DashboardVisualizationPre110 {
  title: string;
  subtitle: string;
  panels: Array<{}>;
}
interface DashboardVisualization110 {
  title: string;
  panels: Array<{}>;
}

interface DashboardVisualization140 {
  title: string;
  panels: Array<{ id: string }>;
}

const migrateDashboardVisualization110: SavedObjectMigrationFn<
  DashboardVisualizationPre110, 
  DashboardVisualization110
> = (doc) => {
  const { subtitle, ...attributesWithoutSubtitle } = doc.attributes;
  return {
    ...doc, 
    attributes: {
      ...attributesWithoutSubtitle,
      title: `${doc.attributes.title} - ${doc.attributes.subtitle}`,
    },
  };
};

const migrateDashboardVisualization140: SavedObjectMigrationFn<
  DashboardVisualization110,
  DashboardVisualization140
> = (doc) => {
  const outPanels = doc.attributes.panels?.map((panel) => {
    return { ...panel, id: uuid.v4() };
  });
  return {
    ...doc,
    attributes: {
      ...doc.attributes,
      panels: outPanels,
    },
  };
};

export const dashboardVisualization: SavedObjectsType = {
  name: 'dashboard_visualization', 
  /** ... */
  migrations: {
    // Takes a pre 1.1.0 doc, and converts it to 1.1.0
    '1.1.0': migrateDashboardVisualization110,

    // Takes a 1.1.0 doc, and converts it to 1.4.0
    '1.4.0': migrateDashboardVisualization140,  
  },
};

It is useful to define an interface for each version of the schema. This allows TypeScript to ensure that you are properly handling the input and output types correctly as the schema evolves.

Returning a shallow copy is necessary to avoid type errors when using different types for the input and output shape.

Migrations do not have to be defined for every version. The version number of a migration must always be the earliest Kibana version in which this migration was released. So if you are creating a migration which will be part of the v7.10.0 release, but will also be backported and released as v7.9.3, the migration version should be: 7.9.3.

Migrations should be written defensively, an exception in a migration function will prevent a Kibana upgrade from succeeding and will cause downtime for our users. Having said that, if a document is encountered that is not in the expected shape, migrations are encouraged to throw an exception to abort the upgrade. In most scenarios, it is better to fail an upgrade than to silently ignore a corrupt document which can cause unexpected behaviour at some future point in time.

It is critical that you have extensive tests to ensure that migrations behave as expected with all possible input documents. Given how simple it is to test all the branch conditions in a migration function and the high impact of a bug in this code, there’s really no reason not to aim for 100% test code coverage.

HTTP serviceedit

Allows plugins:

  • to extend the Kibana server with custom REST API.
  • to execute custom logic on an incoming request or server response.
  • implement custom authentication and authorization strategy.

See HTTP service API docs

import { schema } from '@kbn/config-schema';
import type { CoreSetup, Plugin } from 'kibana/server';

export class MyPlugin implements Plugin {
  public setup(core: CoreSetup) {
    const router = core.http.createRouter();

    const validate = {
      params: schema.object({
        id: schema.string(),
      }),
    };

    router.get({
      path: 'my_plugin/{id}',
      validate
    },
    async (context, request, response) => {
      const data = await findObject(request.params.id);
      if (!data) return response.notFound();
      return response.ok({
        body: data,
        headers: {
          'content-type': 'application/json'
        }
      });
    });
  }
}

UI settings serviceedit

The program interface to UI settings. It makes it possible for Kibana plugins to extend Kibana UI Settings Management with custom settings.

See:

import { schema } from '@kbn/config-schema';
import type { CoreSetup,Plugin } from 'kibana/server';

export class MyPlugin implements Plugin {
  public setup(core: CoreSetup) {
    core.uiSettings.register({
      custom: {
        value: '42',
        schema: schema.string(),
      },
    });
    const router = core.http.createRouter();
    router.get({
      path: 'my_plugin/{id}',
      validate: …,
    },
    async (context, request, response) => {
      const customSetting = await context.uiSettings.client.get('custom');
      …
    });
  }
}

Client-sideedit

Application serviceedit

Kibana has migrated to be a Single Page Application. Plugins should use Application service API to instruct Kibana what an application should be loaded & rendered in the UI in response to user interactions.

import { AppMountParameters, CoreSetup, Plugin, DEFAULT_APP_CATEGORIES } from 'kibana/public';

export class MyPlugin implements Plugin {
  public setup(core: CoreSetup) {
    core.application.register({ 
      category: DEFAULT_APP_CATEGORIES.kibana,
      id: 'my-plugin',
      title: 'my plugin title',
      euiIconType: '/path/to/some.svg',
      order: 100,
      appRoute: '/app/my_plugin', 
      async mount(params: AppMountParameters) { 
        // Load application bundle
        const { renderApp } = await import('./application');
        // Get start services
        const [coreStart, depsStart] = await core.getStartServices(); 
        // Render the application
        return renderApp(coreStart, depsStart, params); 
      },
    });
  }
}

See application.register interface

Application specific URL.

mount callback is invoked when a user navigates to the application-specific URL.

core.getStartServices method provides API available during start lifecycle.

mount method must return a function that will be called to unmount the application.

NOTE
you are free to use any UI library to render a plugin application in DOM. However, we recommend using React and EUI for all your basic UI components to create a consistent UI experience.

HTTP serviceedit

Provides API to communicate with the Kibana server. Feel free to use another HTTP client library to request 3rd party services.

import { CoreStart } from 'kibana/public';
interface ResponseType {…};
async function fetchData<ResponseType>(core: CoreStart) {
  return await core.http.get<>(
    '/api/my_plugin/',
    { query: … },
  );
}

See for all available API.

Elasticsearch serviceedit

Not available in the browser. Use Data plugin instead.