Skip to content

Commit

Permalink
Add plugin status API (#75819)
Browse files Browse the repository at this point in the history
# Conflicts:
#	rfcs/text/0010_service_status.md
  • Loading branch information
joshdover committed Sep 1, 2020
1 parent 6794779 commit 42e3bf6
Show file tree
Hide file tree
Showing 20 changed files with 915 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) &gt; [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md)

## StatusServiceSetup.dependencies$ property

Current status for all plugins this plugin depends on. Each key of the `Record` is a plugin id.

<b>Signature:</b>

```typescript
dependencies$: Observable<Record<string, ServiceStatus>>;
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) &gt; [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md)

## StatusServiceSetup.derivedStatus$ property

The status of this plugin as derived from its dependencies.

<b>Signature:</b>

```typescript
derivedStatus$: Observable<ServiceStatus>;
```

## Remarks

By default, plugins inherit this derived status from their dependencies. Calling overrides this default status.

This may emit multliple times for a single status change event as propagates through the dependency tree

Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,73 @@ API for accessing status of Core and this plugin's dependencies as well as for c
export interface StatusServiceSetup
```

## Remarks

By default, a plugin inherits it's current status from the most severe status level of any Core services and any plugins that it depends on. This default status is available on the API.

Plugins may customize their status calculation by calling the API with an Observable. Within this Observable, a plugin may choose to only depend on the status of some of its dependencies, to ignore severe status levels of particular Core services they are not concerned with, or to make its status dependent on other external services.

## Example 1

Customize a plugin's status to only depend on the status of SavedObjects:

```ts
core.status.set(
core.status.core$.pipe(
. map((coreStatus) => {
return coreStatus.savedObjects;
}) ;
);
);

```

## Example 2

Customize a plugin's status to include an external service:

```ts
const externalStatus$ = interval(1000).pipe(
switchMap(async () => {
const resp = await fetch(`https://myexternaldep.com/_healthz`);
const body = await resp.json();
if (body.ok) {
return of({ level: ServiceStatusLevels.available, summary: 'External Service is up'});
} else {
return of({ level: ServiceStatusLevels.available, summary: 'External Service is unavailable'});
}
}),
catchError((error) => {
of({ level: ServiceStatusLevels.unavailable, summary: `External Service is down`, meta: { error }})
})
);

core.status.set(
combineLatest([core.status.derivedStatus$, externalStatus$]).pipe(
map(([derivedStatus, externalStatus]) => {
if (externalStatus.level > derivedStatus) {
return externalStatus;
} else {
return derivedStatus;
}
})
)
);

```

## Properties

| Property | Type | Description |
| --- | --- | --- |
| [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | <code>Observable&lt;CoreStatus&gt;</code> | Current status for all Core services. |
| [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) | <code>Observable&lt;Record&lt;string, ServiceStatus&gt;&gt;</code> | Current status for all plugins this plugin depends on. Each key of the <code>Record</code> is a plugin id. |
| [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) | <code>Observable&lt;ServiceStatus&gt;</code> | The status of this plugin as derived from its dependencies. |
| [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) | <code>Observable&lt;ServiceStatus&gt;</code> | Overall system status for all of Kibana. |

## Methods

| Method | Description |
| --- | --- |
| [set(status$)](./kibana-plugin-core-server.statusservicesetup.set.md) | Allows a plugin to specify a custom status dependent on its own criteria. Completely overrides the default inherited status. |

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!-- Do not edit this file. It is automatically generated by API Documenter. -->

[Home](./index.md) &gt; [kibana-plugin-core-server](./kibana-plugin-core-server.md) &gt; [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) &gt; [set](./kibana-plugin-core-server.statusservicesetup.set.md)

## StatusServiceSetup.set() method

Allows a plugin to specify a custom status dependent on its own criteria. Completely overrides the default inherited status.

<b>Signature:</b>

```typescript
set(status$: Observable<ServiceStatus>): void;
```

## Parameters

| Parameter | Type | Description |
| --- | --- | --- |
| status$ | <code>Observable&lt;ServiceStatus&gt;</code> | |

<b>Returns:</b>

`void`

## Remarks

See the [StatusServiceSetup.derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) API for leveraging the default status calculation that is provided by Core.

3 changes: 3 additions & 0 deletions src/core/server/legacy/legacy_service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,9 @@ export class LegacyService implements CoreService {
status: {
core$: setupDeps.core.status.core$,
overall$: setupDeps.core.status.overall$,
set: setupDeps.core.status.plugins.set.bind(null, 'legacy'),
dependencies$: setupDeps.core.status.plugins.getDependenciesStatus$('legacy'),
derivedStatus$: setupDeps.core.status.plugins.getDerivedStatus$('legacy'),
},
uiSettings: {
register: setupDeps.core.uiSettings.register,
Expand Down
3 changes: 3 additions & 0 deletions src/core/server/plugins/plugin_context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
status: {
core$: deps.status.core$,
overall$: deps.status.overall$,
set: deps.status.plugins.set.bind(null, plugin.name),
dependencies$: deps.status.plugins.getDependenciesStatus$(plugin.name),
derivedStatus$: deps.status.plugins.getDerivedStatus$(plugin.name),
},
uiSettings: {
register: deps.uiSettings.register,
Expand Down
30 changes: 21 additions & 9 deletions src/core/server/plugins/plugins_system.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,15 +100,27 @@ test('getPluginDependencies returns dependency tree of symbols', () => {
pluginsSystem.addPlugin(createPlugin('no-dep'));

expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(`
Map {
Symbol(plugin-a) => Array [
Symbol(no-dep),
],
Symbol(plugin-b) => Array [
Symbol(plugin-a),
Symbol(no-dep),
],
Symbol(no-dep) => Array [],
Object {
"asNames": Map {
"plugin-a" => Array [
"no-dep",
],
"plugin-b" => Array [
"plugin-a",
"no-dep",
],
"no-dep" => Array [],
},
"asOpaqueIds": Map {
Symbol(plugin-a) => Array [
Symbol(no-dep),
],
Symbol(plugin-b) => Array [
Symbol(plugin-a),
Symbol(no-dep),
],
Symbol(no-dep) => Array [],
},
}
`);
});
Expand Down
21 changes: 17 additions & 4 deletions src/core/server/plugins/plugins_system.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@
import { CoreContext } from '../core_context';
import { Logger } from '../logging';
import { PluginWrapper } from './plugin';
import { DiscoveredPlugin, PluginName, PluginOpaqueId } from './types';
import { DiscoveredPlugin, PluginName } from './types';
import { createPluginSetupContext, createPluginStartContext } from './plugin_context';
import { PluginsServiceSetupDeps, PluginsServiceStartDeps } from './plugins_service';
import { withTimeout } from '../../utils';
import { PluginDependencies } from '.';

const Sec = 1000;
/** @internal */
Expand All @@ -45,9 +46,19 @@ export class PluginsSystem {
* @returns a ReadonlyMap of each plugin and an Array of its available dependencies
* @internal
*/
public getPluginDependencies(): ReadonlyMap<PluginOpaqueId, PluginOpaqueId[]> {
// Return dependency map of opaque ids
return new Map(
public getPluginDependencies(): PluginDependencies {
const asNames = new Map(
[...this.plugins].map(([name, plugin]) => [
plugin.name,
[
...new Set([
...plugin.requiredPlugins,
...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)),
]),
].map((depId) => this.plugins.get(depId)!.name),
])
);
const asOpaqueIds = new Map(
[...this.plugins].map(([name, plugin]) => [
plugin.opaqueId,
[
Expand All @@ -58,6 +69,8 @@ export class PluginsSystem {
].map((depId) => this.plugins.get(depId)!.opaqueId),
])
);

return { asNames, asOpaqueIds };
}

public async setupPlugins(deps: PluginsServiceSetupDeps) {
Expand Down
6 changes: 6 additions & 0 deletions src/core/server/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,12 @@ export type PluginName = string;
/** @public */
export type PluginOpaqueId = symbol;

/** @internal */
export interface PluginDependencies {
asNames: ReadonlyMap<PluginName, PluginName[]>;
asOpaqueIds: ReadonlyMap<PluginOpaqueId, PluginOpaqueId[]>;
}

/**
* Describes the set of required and optional properties plugin can define in its
* mandatory JSON manifest file.
Expand Down
13 changes: 10 additions & 3 deletions src/core/server/server.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -2853,10 +2853,17 @@ export type SharedGlobalConfig = RecursiveReadonly<{
// @public
export type StartServicesAccessor<TPluginsStart extends object = object, TStart = unknown> = () => Promise<[CoreStart, TPluginsStart, TStart]>;

// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ServiceStatusSetup"
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "ServiceStatusSetup"
//
// @public
export interface StatusServiceSetup {
core$: Observable<CoreStatus>;
dependencies$: Observable<Record<string, ServiceStatus>>;
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "StatusSetup"
derivedStatus$: Observable<ServiceStatus>;
overall$: Observable<ServiceStatus>;
set(status$: Observable<ServiceStatus>): void;
}

// @public
Expand Down Expand Up @@ -2949,8 +2956,8 @@ export const validBodyOutput: readonly ["data", "stream"];
// src/core/server/legacy/types.ts:165:3 - (ae-forgotten-export) The symbol "LegacyNavLinkSpec" needs to be exported by the entry point index.d.ts
// src/core/server/legacy/types.ts:166:3 - (ae-forgotten-export) The symbol "LegacyAppSpec" needs to be exported by the entry point index.d.ts
// src/core/server/legacy/types.ts:167:16 - (ae-forgotten-export) The symbol "LegacyPluginSpec" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:266:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:268:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "KibanaConfigType" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:272:3 - (ae-forgotten-export) The symbol "SharedGlobalConfigKeys" needs to be exported by the entry point index.d.ts
// src/core/server/plugins/types.ts:274:3 - (ae-forgotten-export) The symbol "PathConfigType" needs to be exported by the entry point index.d.ts

```
30 changes: 28 additions & 2 deletions src/core/server/server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ import { Server } from './server';
import { getEnvOptions } from './config/__mocks__/env';
import { loggingSystemMock } from './logging/logging_system.mock';
import { rawConfigServiceMock } from './config/raw_config_service.mock';
import { PluginName } from './plugins';

const env = new Env('.', getEnvOptions());
const logger = loggingSystemMock.create();
Expand All @@ -49,7 +50,7 @@ const rawConfigService = rawConfigServiceMock.create({});
beforeEach(() => {
mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
mockPluginsService.discover.mockResolvedValue({
pluginTree: new Map(),
pluginTree: { asOpaqueIds: new Map(), asNames: new Map() },
uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() },
});
});
Expand Down Expand Up @@ -98,7 +99,7 @@ test('injects legacy dependency to context#setup()', async () => {
[pluginB, [pluginA]],
]);
mockPluginsService.discover.mockResolvedValue({
pluginTree: pluginDependencies,
pluginTree: { asOpaqueIds: pluginDependencies, asNames: new Map() },
uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() },
});

Expand All @@ -113,6 +114,31 @@ test('injects legacy dependency to context#setup()', async () => {
});
});

test('injects legacy dependency to status#setup()', async () => {
const server = new Server(rawConfigService, env, logger);

const pluginDependencies = new Map<PluginName, PluginName[]>([
['a', []],
['b', ['a']],
]);
mockPluginsService.discover.mockResolvedValue({
pluginTree: { asOpaqueIds: new Map(), asNames: pluginDependencies },
uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() },
});

await server.setup();

expect(mockStatusService.setup).toHaveBeenCalledWith({
elasticsearch: expect.any(Object),
savedObjects: expect.any(Object),
pluginDependencies: new Map([
['a', []],
['b', ['a']],
['legacy', ['a', 'b']],
]),
});
});

test('runs services on "start"', async () => {
const server = new Server(rawConfigService, env, logger);

Expand Down
13 changes: 11 additions & 2 deletions src/core/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,10 +121,13 @@ export class Server {

const contextServiceSetup = this.context.setup({
// We inject a fake "legacy plugin" with dependencies on every plugin so that legacy plugins:
// 1) Can access context from any NP plugin
// 1) Can access context from any KP plugin
// 2) Can register context providers that will only be available to other legacy plugins and will not leak into
// New Platform plugins.
pluginDependencies: new Map([...pluginTree, [this.legacy.legacyId, [...pluginTree.keys()]]]),
pluginDependencies: new Map([
...pluginTree.asOpaqueIds,
[this.legacy.legacyId, [...pluginTree.asOpaqueIds.keys()]],
]),
});

const auditTrailSetup = this.auditTrail.setup();
Expand Down Expand Up @@ -154,6 +157,12 @@ export class Server {

const statusSetup = await this.status.setup({
elasticsearch: elasticsearchServiceSetup,
// We inject a fake "legacy plugin" with dependencies on every plugin so that legacy can access plugin status from
// any KP plugin
pluginDependencies: new Map([
...pluginTree.asNames,
['legacy', [...pluginTree.asNames.keys()]],
]),
savedObjects: savedObjectsSetup,
});

Expand Down
Loading

0 comments on commit 42e3bf6

Please sign in to comment.