Kibana Plugin APIedit
This functionality is in technical preview and may be changed or removed in a future release. Elastic will apply best effort to fix any issues, but features in technical preview are not subject to the support SLA of official GA features.
Kibana platform plugins are a significant step toward stabilizing Kibana architecture for all the developers. We made sure plugins could continue to use most of the same technologies they use today, at least from a technical perspective.
Anatomy of a pluginedit
Plugins are defined as classes and present themselves to Kibana through a simple wrapper function. A plugin can have browser-side code, server-side code, or both. There is no architectural difference between a plugin in the browser and a plugin on the server. In both places, you describe your plugin similarly, and you interact with Core and other plugins in the same way.
The basic file structure of a Kibana plugin named demo
that
has both client-side and server-side code would be:
plugins/ demo kibana.json [1] public index.ts [2] plugin.ts [3] server index.ts [4] plugin.ts [5]
[1] kibana.json
is a static manifest file that is used to identify the
plugin and to specify if this plugin has server-side code, browser-side code, or both:
{ "id": "demo", "version": "kibana", "server": true, "ui": true }
Learn about the manifest file format.
package.json
files are irrelevant to and ignored by Kibana for discovering and loading plugins.
[2] public/index.ts
is the entry point into the client-side code of
this plugin. It must export a function named plugin
, which will
receive a standard set of core capabilities as an argument.
It should return an instance of its plugin class for
Kibana to load.
import type { PluginInitializerContext } from '@kbn/core/server'; import { MyPlugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new MyPlugin(initializerContext); }
[3] public/plugin.ts
is the client-side plugin definition itself.
Technically speaking, it does not need to be a class or even a separate
file from the entry point, but all plugins at Elastic should be
consistent in this way. See all conventions
for first-party Elastic plugins.
import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from '@kbn/core/server'; export class MyPlugin implements Plugin { 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 } }
[4] server/index.ts
is the entry-point into the server-side code of
this plugin. It is identical in almost every way to the client-side
entry-point:
import type { PluginInitializerContext } from '@kbn/core/server'; import { MyPlugin } from './plugin'; export function plugin(initializerContext: PluginInitializerContext) { return new MyPlugin(initializerContext); }
[5] server/plugin.ts
is the server-side plugin definition. The
shape of this plugin is the same as it’s client-side counter-part:
import type { Plugin, PluginInitializerContext, CoreSetup, CoreStart } from '@kbn/core/server'; export class MyPlugin implements Plugin { 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 } }
Kibana does not impose any technical restrictions on how the the internals of a plugin are architected, though there are certain considerations related to how plugins integrate with core APIs and APIs exposed by other plugins that may greatly impact how they are built.
Lifecycles & Core Servicesedit
The various independent domains that makeup core
are represented by a
series of services and many of those services expose public interfaces
that are provided to all plugins. Services expose different features
at different parts of their lifecycle. We describe the lifecycle of
core services and plugins with specifically-named functions on the
service definition.
Kibana has three lifecycles: setup
,
start
, and stop
. Each plugin’s setup
functions is called sequentially
while Kibana is setting up on the server or when it is being loaded in
the browser. The start
functions are called sequentially after setup
has been completed for all plugins. The stop
functions are called
sequentially while Kibana is gracefully shutting down the server or
when the browser tab or window is being closed.
The table below explains how each lifecycle relates to the state of Kibana.
lifecycle | purpose | server | browser |
---|---|---|---|
setup |
perform "registration" work to setup environment for runtime |
configure REST API endpoint, register saved object types, etc. |
configure application routes in SPA, register custom UI elements in extension points, etc. |
start |
bootstrap runtime logic |
respond to an incoming request, request Elasticsearch server, etc. |
start polling Kibana server, update DOM tree in response to user interactions, etc. |
stop |
cleanup runtime |
dispose of active handles before the server shutdown. |
store session data in the LocalStorage when the user navigates away from Kibana, etc. |
Conversely, there is no equivalent to uiExports
in Kibana Platform plugins.
As a general rule of thumb, features that were registered via uiExports
are
now registered during the setup
phase. Most of everything else should move
to the start
phase.
The lifecycle-specific contracts exposed by core services are always
passed as the first argument to the equivalent lifecycle function in a
plugin. For example, the core http
service exposes a function
createRouter
to all plugin setup
functions. To use this function to register
an HTTP route handler, a plugin just accesses it off of the first argument:
import type { CoreSetup } from '@kbn/core/server'; export class MyPlugin { public setup(core: CoreSetup) { const router = core.http.createRouter(); // handler is called when '/path' resource is requested with `GET` method router.get({ path: '/path', validate: false }, (context, req, res) => res.ok({ content: 'ok' })); } }
Different service interfaces can and will be passed to setup
, start
, and
stop
because certain functionality makes sense in the context of a
running plugin while other types of functionality may have restrictions
or may only make sense in the context of a plugin that is stopping.
For example, the stop
function in the browser gets invoked as part of
the window.onbeforeunload
event, which means you can’t necessarily
execute asynchronous code here reliably. For that reason,
core
likely wouldn’t provide any asynchronous functions to plugin
stop
functions in the browser.
The current lifecycle function for all plugins will be executed before the next
lifecycle starts. That is to say that all setup
functions are executed before
any start
functions are executed.
These are the contracts exposed by the core services for each lifecycle:
lifecycle | server contract | browser contract |
---|---|---|
constructor |
||
setup |
||
start |
Integrating with other pluginsedit
Plugins can expose public interfaces for other plugins to consume. Like
core
, those interfaces are bound to the lifecycle functions setup
and/or start
.
Anything returned from setup
or start
will act as the interface, and
while not a technical requirement, all first-party Elastic plugins
will expose types for that interface as well. Third party plugins
wishing to allow other plugins to integrate with it are also highly
encouraged to expose types for their plugin interfaces.
foobar plugin.ts:
import type { Plugin } from '@kbn/core/server'; export interface FoobarPluginSetup { getFoo(): string; } export interface FoobarPluginStart { getBar(): string; } export class MyPlugin implements Plugin<FoobarPluginSetup, FoobarPluginStart> { public setup(): FoobarPluginSetup { return { getFoo() { return 'foo'; }, }; } public start(): FoobarPluginStart { return { getBar() { return 'bar'; }, }; } }
Unlike core, capabilities exposed by plugins are not automatically
injected into all plugins. Instead, if a plugin wishes to use the public
interface provided by another plugin, it must first declare that
plugin as a dependency in it’s kibana.json
manifest file.
demo kibana.json:
{ "id": "demo", "requiredPlugins": ["foobar"], "server": true, "ui": true }
With that specified in the plugin manifest, the appropriate interfaces
are then available via the second argument of setup
and/or start
:
demo plugin.ts:
import type { CoreSetup, CoreStart } from '@kbn/core/server'; import type { FoobarPluginSetup, FoobarPluginStart } from '../../foobar/server'; interface DemoSetupPlugins { foobar: FoobarPluginSetup; } interface DemoStartPlugins { foobar: FoobarPluginStart; } export class AnotherPlugin { public setup(core: CoreSetup, plugins: DemoSetupPlugins) { const { foobar } = plugins; foobar.getFoo(); // 'foo' foobar.getBar(); // throws because getBar does not exist } public start(core: CoreStart, plugins: DemoStartPlugins) { const { foobar } = plugins; foobar.getFoo(); // throws because getFoo does not exist foobar.getBar(); // 'bar' } public stop() {} }
The interface for plugin’s dependencies must be manually composed. You can do this by importing the appropriate type from the plugin and constructing an interface where the property name is the plugin’s ID. |
|
These manually constructed types should then be used to specify the type of the second argument to the plugin. |
|
Notice that the type for the setup and start lifecycles are different. Plugin lifecycle functions can only access the APIs that are exposed during that lifecycle. |