Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add plugin status API #75819

Merged
merged 7 commits into from
Sep 1, 2020
Merged
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<!-- 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.

Original file line number Diff line number Diff line change
@@ -17,5 +17,13 @@ export interface StatusServiceSetup
| Property | Type | Description |
| --- | --- | --- |
| [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | <code>Observable&lt;CoreStatus&gt;</code> | Current status for all Core services. |
| [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. |
| [plugins$](./kibana-plugin-core-server.statusservicesetup.plugins_.md) | <code>Observable&lt;Record&lt;string, ServiceStatus&gt;&gt;</code> | Current status for all dependencies of the current plugin. Each key of the <code>Record</code> is a plugin id. |

## 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,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; [plugins$](./kibana-plugin-core-server.statusservicesetup.plugins_.md)

## StatusServiceSetup.plugins$ property

Current status for all dependencies of the current plugin. Each key of the `Record` is a plugin id.

<b>Signature:</b>

```typescript
plugins$: Observable<Record<string, ServiceStatus>>;
```
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
@@ -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'),
plugins$: setupDeps.core.status.plugins.getDepsStatus$('legacy'),
derivedStatus$: setupDeps.core.status.plugins.getDerivedStatus$('legacy'),
},
uiSettings: {
register: setupDeps.core.uiSettings.register,
3 changes: 3 additions & 0 deletions src/core/server/plugins/plugin_context.ts
Original file line number Diff line number Diff line change
@@ -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),
plugins$: deps.status.plugins.getDepsStatus$(plugin.name),
derivedStatus$: deps.status.plugins.getDerivedStatus$(plugin.name),
},
uiSettings: {
register: deps.uiSettings.register,
30 changes: 21 additions & 9 deletions src/core/server/plugins/plugins_system.test.ts
Original file line number Diff line number Diff line change
@@ -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 [],
},
}
`);
});
21 changes: 17 additions & 4 deletions src/core/server/plugins/plugins_system.ts
Original file line number Diff line number Diff line change
@@ -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 */
@@ -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,
[
@@ -58,6 +69,8 @@ export class PluginsSystem {
].map((depId) => this.plugins.get(depId)!.opaqueId),
])
);

return { asNames, asOpaqueIds };
}

public async setupPlugins(deps: PluginsServiceSetupDeps) {
6 changes: 6 additions & 0 deletions src/core/server/plugins/types.ts
Original file line number Diff line number Diff line change
@@ -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.
10 changes: 7 additions & 3 deletions src/core/server/server.api.md
Original file line number Diff line number Diff line change
@@ -2856,7 +2856,11 @@ export type StartServicesAccessor<TPluginsStart extends object = object, TStart
// @public
export interface StatusServiceSetup {
core$: Observable<CoreStatus>;
// 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>;
plugins$: Observable<Record<string, ServiceStatus>>;
set(status$: Observable<ServiceStatus>): void;
}

// @public
@@ -2949,8 +2953,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
@@ -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();
@@ -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() },
});
});
@@ -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() },
});

@@ -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);

13 changes: 11 additions & 2 deletions src/core/server/server.ts
Original file line number Diff line number Diff line change
@@ -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();
@@ -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,
});

44 changes: 43 additions & 1 deletion src/core/server/status/get_summary_status.test.ts
Original file line number Diff line number Diff line change
@@ -94,6 +94,38 @@ describe('getSummaryStatus', () => {
describe('summary', () => {
describe('when a single service is at highest level', () => {
it('returns all information about that single service', () => {
expect(
getSummaryStatus(
Object.entries({
s1: degraded,
s2: {
level: ServiceStatusLevels.unavailable,
summary: 'Lorem ipsum',
meta: {
custom: { data: 'here' },
},
},
})
)
).toEqual({
level: ServiceStatusLevels.unavailable,
summary: '[s2]: Lorem ipsum',
detail: 'See the status page for more information',
meta: {
affectedServices: {
s2: {
level: ServiceStatusLevels.unavailable,
summary: 'Lorem ipsum',
meta: {
custom: { data: 'here' },
},
},
},
},
});
});

it('allows the single service to override the detail and documentationUrl fields', () => {
expect(
getSummaryStatus(
Object.entries({
@@ -115,7 +147,17 @@ describe('getSummaryStatus', () => {
detail: 'Vivamus pulvinar sem ac luctus ultrices.',
documentationUrl: 'http://helpmenow.com/problem1',
meta: {
custom: { data: 'here' },
affectedServices: {
s2: {
level: ServiceStatusLevels.unavailable,
summary: 'Lorem ipsum',
detail: 'Vivamus pulvinar sem ac luctus ultrices.',
documentationUrl: 'http://helpmenow.com/problem1',
meta: {
custom: { data: 'here' },
},
},
},
},
});
});
12 changes: 10 additions & 2 deletions src/core/server/status/get_summary_status.ts
Original file line number Diff line number Diff line change
@@ -23,21 +23,29 @@ import { ServiceStatus, ServiceStatusLevels, ServiceStatusLevel } from './types'
* Returns a single {@link ServiceStatus} that summarizes the most severe status level from a group of statuses.
* @param statuses
*/
export const getSummaryStatus = (statuses: Array<[string, ServiceStatus]>): ServiceStatus => {
export const getSummaryStatus = (
statuses: Array<[string, ServiceStatus]>,
{ allAvailableSummary = `All services are available` }: { allAvailableSummary?: string } = {}
): ServiceStatus => {
const grouped = groupByLevel(statuses);
const highestSeverityLevel = getHighestSeverityLevel(grouped.keys());
const highestSeverityGroup = grouped.get(highestSeverityLevel)!;

if (highestSeverityLevel === ServiceStatusLevels.available) {
return {
level: ServiceStatusLevels.available,
summary: `All services are available`,
summary: allAvailableSummary,
};
} else if (highestSeverityGroup.size === 1) {
const [serviceName, status] = [...highestSeverityGroup.entries()][0];
return {
...status,
summary: `[${serviceName}]: ${status.summary!}`,
// TODO: include URL to status page
detail: status.detail ?? `See the status page for more information`,
meta: {
affectedServices: { [serviceName]: status },
},
Comment on lines +44 to +48
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed the summary output for when only a single service is unavailable to be more consistent with the case where more than one is unavailable.

};
} else {
return {
Loading