Test reverting "Add plugin status API (#75819)" (#76707)

Co-authored-by: spalger <spalger@users.noreply.github.com>
This commit is contained in:
Spencer 2020-09-03 15:21:59 -07:00 committed by GitHub
parent fb304b2dbe
commit 5b853b2326
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 32 additions and 916 deletions

View file

@ -1,13 +0,0 @@
<!-- 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>>;
```

View file

@ -1,20 +0,0 @@
<!-- 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

View file

@ -12,73 +12,10 @@ 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. |

View file

@ -1,28 +0,0 @@
<!-- 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.

View file

@ -137,7 +137,7 @@ interface StatusSetup {
* Current status for all dependencies of the current plugin.
* Each key of the `Record` is a plugin id.
*/
dependencies$: Observable<Record<string, ServiceStatus>>;
plugins$: Observable<Record<string, ServiceStatus>>;
/**
* The status of this plugin as derived from its dependencies.

View file

@ -323,9 +323,6 @@ 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,

View file

@ -185,9 +185,6 @@ 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,

View file

@ -100,27 +100,15 @@ test('getPluginDependencies returns dependency tree of symbols', () => {
pluginsSystem.addPlugin(createPlugin('no-dep'));
expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(`
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 [],
},
Map {
Symbol(plugin-a) => Array [
Symbol(no-dep),
],
Symbol(plugin-b) => Array [
Symbol(plugin-a),
Symbol(no-dep),
],
Symbol(no-dep) => Array [],
}
`);
});

View file

@ -20,11 +20,10 @@
import { CoreContext } from '../core_context';
import { Logger } from '../logging';
import { PluginWrapper } from './plugin';
import { DiscoveredPlugin, PluginName } from './types';
import { DiscoveredPlugin, PluginName, PluginOpaqueId } 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 */
@ -46,19 +45,9 @@ export class PluginsSystem {
* @returns a ReadonlyMap of each plugin and an Array of its available dependencies
* @internal
*/
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(
public getPluginDependencies(): ReadonlyMap<PluginOpaqueId, PluginOpaqueId[]> {
// Return dependency map of opaque ids
return new Map(
[...this.plugins].map(([name, plugin]) => [
plugin.opaqueId,
[
@ -69,8 +58,6 @@ export class PluginsSystem {
].map((depId) => this.plugins.get(depId)!.opaqueId),
])
);
return { asNames, asOpaqueIds };
}
public async setupPlugins(deps: PluginsServiceSetupDeps) {

View file

@ -93,12 +93,6 @@ 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.

View file

@ -2858,17 +2858,10 @@ 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
@ -2961,8 +2954,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: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
// 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
```

View file

@ -41,7 +41,6 @@ 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();
@ -50,7 +49,7 @@ const rawConfigService = rawConfigServiceMock.create({});
beforeEach(() => {
mockConfigService.atPath.mockReturnValue(new BehaviorSubject({ autoListen: true }));
mockPluginsService.discover.mockResolvedValue({
pluginTree: { asOpaqueIds: new Map(), asNames: new Map() },
pluginTree: new Map(),
uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() },
});
});
@ -99,7 +98,7 @@ test('injects legacy dependency to context#setup()', async () => {
[pluginB, [pluginA]],
]);
mockPluginsService.discover.mockResolvedValue({
pluginTree: { asOpaqueIds: pluginDependencies, asNames: new Map() },
pluginTree: pluginDependencies,
uiPlugins: { internal: new Map(), public: new Map(), browserConfigs: new Map() },
});
@ -114,31 +113,6 @@ 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);

View file

@ -121,13 +121,10 @@ 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 KP plugin
// 1) Can access context from any NP 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.asOpaqueIds,
[this.legacy.legacyId, [...pluginTree.asOpaqueIds.keys()]],
]),
pluginDependencies: new Map([...pluginTree, [this.legacy.legacyId, [...pluginTree.keys()]]]),
});
const auditTrailSetup = this.auditTrail.setup();
@ -157,12 +154,6 @@ 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,
});

View file

@ -94,38 +94,6 @@ 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({
@ -147,17 +115,7 @@ describe('getSummaryStatus', () => {
detail: 'Vivamus pulvinar sem ac luctus ultrices.',
documentationUrl: 'http://helpmenow.com/problem1',
meta: {
affectedServices: {
s2: {
level: ServiceStatusLevels.unavailable,
summary: 'Lorem ipsum',
detail: 'Vivamus pulvinar sem ac luctus ultrices.',
documentationUrl: 'http://helpmenow.com/problem1',
meta: {
custom: { data: 'here' },
},
},
},
custom: { data: 'here' },
},
});
});

View file

@ -23,10 +23,7 @@ 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]>,
{ allAvailableSummary = `All services are available` }: { allAvailableSummary?: string } = {}
): ServiceStatus => {
export const getSummaryStatus = (statuses: Array<[string, ServiceStatus]>): ServiceStatus => {
const grouped = groupByLevel(statuses);
const highestSeverityLevel = getHighestSeverityLevel(grouped.keys());
const highestSeverityGroup = grouped.get(highestSeverityLevel)!;
@ -34,18 +31,13 @@ export const getSummaryStatus = (
if (highestSeverityLevel === ServiceStatusLevels.available) {
return {
level: ServiceStatusLevels.available,
summary: allAvailableSummary,
summary: `All services are available`,
};
} 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 },
},
};
} else {
return {

View file

@ -1,338 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PluginName } from '../plugins';
import { PluginsStatusService } from './plugins_status';
import { of, Observable, BehaviorSubject } from 'rxjs';
import { ServiceStatusLevels, CoreStatus, ServiceStatus } from './types';
import { first } from 'rxjs/operators';
import { ServiceStatusLevelSnapshotSerializer } from './test_utils';
expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer);
describe('PluginStatusService', () => {
const coreAllAvailable$: Observable<CoreStatus> = of({
elasticsearch: { level: ServiceStatusLevels.available, summary: 'elasticsearch avail' },
savedObjects: { level: ServiceStatusLevels.available, summary: 'savedObjects avail' },
});
const coreOneDegraded$: Observable<CoreStatus> = of({
elasticsearch: { level: ServiceStatusLevels.available, summary: 'elasticsearch avail' },
savedObjects: { level: ServiceStatusLevels.degraded, summary: 'savedObjects degraded' },
});
const coreOneCriticalOneDegraded$: Observable<CoreStatus> = of({
elasticsearch: { level: ServiceStatusLevels.critical, summary: 'elasticsearch critical' },
savedObjects: { level: ServiceStatusLevels.degraded, summary: 'savedObjects degraded' },
});
const pluginDependencies: Map<PluginName, PluginName[]> = new Map([
['a', []],
['b', ['a']],
['c', ['a', 'b']],
]);
describe('getDerivedStatus$', () => {
it(`defaults to core's most severe status`, async () => {
const serviceAvailable = new PluginsStatusService({
core$: coreAllAvailable$,
pluginDependencies,
});
expect(await serviceAvailable.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({
level: ServiceStatusLevels.available,
summary: 'All dependencies are available',
});
const serviceDegraded = new PluginsStatusService({
core$: coreOneDegraded$,
pluginDependencies,
});
expect(await serviceDegraded.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({
level: ServiceStatusLevels.degraded,
summary: '[savedObjects]: savedObjects degraded',
detail: 'See the status page for more information',
meta: expect.any(Object),
});
const serviceCritical = new PluginsStatusService({
core$: coreOneCriticalOneDegraded$,
pluginDependencies,
});
expect(await serviceCritical.getDerivedStatus$('a').pipe(first()).toPromise()).toEqual({
level: ServiceStatusLevels.critical,
summary: '[elasticsearch]: elasticsearch critical',
detail: 'See the status page for more information',
meta: expect.any(Object),
});
});
it(`provides a summary status when core and dependencies are at same severity level`, async () => {
const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies });
service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' }));
expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({
level: ServiceStatusLevels.degraded,
summary: '[2] services are degraded',
detail: 'See the status page for more information',
meta: expect.any(Object),
});
});
it(`allows dependencies status to take precedence over lower severity core statuses`, async () => {
const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies });
service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' }));
expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({
level: ServiceStatusLevels.unavailable,
summary: '[a]: a is not working',
detail: 'See the status page for more information',
meta: expect.any(Object),
});
});
it(`allows core status to take precedence over lower severity dependencies statuses`, async () => {
const service = new PluginsStatusService({
core$: coreOneCriticalOneDegraded$,
pluginDependencies,
});
service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a is not working' }));
expect(await service.getDerivedStatus$('b').pipe(first()).toPromise()).toEqual({
level: ServiceStatusLevels.critical,
summary: '[elasticsearch]: elasticsearch critical',
detail: 'See the status page for more information',
meta: expect.any(Object),
});
});
it(`allows a severe dependency status to take precedence over a less severe dependency status`, async () => {
const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies });
service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a is degraded' }));
service.set('b', of({ level: ServiceStatusLevels.unavailable, summary: 'b is not working' }));
expect(await service.getDerivedStatus$('c').pipe(first()).toPromise()).toEqual({
level: ServiceStatusLevels.unavailable,
summary: '[b]: b is not working',
detail: 'See the status page for more information',
meta: expect.any(Object),
});
});
});
describe('getAll$', () => {
it('defaults to empty record if no plugins', async () => {
const service = new PluginsStatusService({
core$: coreAllAvailable$,
pluginDependencies: new Map(),
});
expect(await service.getAll$().pipe(first()).toPromise()).toEqual({});
});
it('defaults to core status when no plugin statuses are set', async () => {
const serviceAvailable = new PluginsStatusService({
core$: coreAllAvailable$,
pluginDependencies,
});
expect(await serviceAvailable.getAll$().pipe(first()).toPromise()).toEqual({
a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' },
b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' },
c: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' },
});
const serviceDegraded = new PluginsStatusService({
core$: coreOneDegraded$,
pluginDependencies,
});
expect(await serviceDegraded.getAll$().pipe(first()).toPromise()).toEqual({
a: {
level: ServiceStatusLevels.degraded,
summary: '[savedObjects]: savedObjects degraded',
detail: 'See the status page for more information',
meta: expect.any(Object),
},
b: {
level: ServiceStatusLevels.degraded,
summary: '[2] services are degraded',
detail: 'See the status page for more information',
meta: expect.any(Object),
},
c: {
level: ServiceStatusLevels.degraded,
summary: '[3] services are degraded',
detail: 'See the status page for more information',
meta: expect.any(Object),
},
});
const serviceCritical = new PluginsStatusService({
core$: coreOneCriticalOneDegraded$,
pluginDependencies,
});
expect(await serviceCritical.getAll$().pipe(first()).toPromise()).toEqual({
a: {
level: ServiceStatusLevels.critical,
summary: '[elasticsearch]: elasticsearch critical',
detail: 'See the status page for more information',
meta: expect.any(Object),
},
b: {
level: ServiceStatusLevels.critical,
summary: '[2] services are critical',
detail: 'See the status page for more information',
meta: expect.any(Object),
},
c: {
level: ServiceStatusLevels.critical,
summary: '[3] services are critical',
detail: 'See the status page for more information',
meta: expect.any(Object),
},
});
});
it('uses the manually set status level if plugin specifies one', async () => {
const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies });
service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' }));
expect(await service.getAll$().pipe(first()).toPromise()).toEqual({
a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded
b: {
level: ServiceStatusLevels.degraded,
summary: '[savedObjects]: savedObjects degraded',
detail: 'See the status page for more information',
meta: expect.any(Object),
},
c: {
level: ServiceStatusLevels.degraded,
summary: '[2] services are degraded',
detail: 'See the status page for more information',
meta: expect.any(Object),
},
});
});
it('updates when a new plugin status observable is set', async () => {
const service = new PluginsStatusService({
core$: coreAllAvailable$,
pluginDependencies: new Map([['a', []]]),
});
const statusUpdates: Array<Record<PluginName, ServiceStatus>> = [];
const subscription = service
.getAll$()
.subscribe((pluginStatuses) => statusUpdates.push(pluginStatuses));
service.set('a', of({ level: ServiceStatusLevels.degraded, summary: 'a degraded' }));
service.set('a', of({ level: ServiceStatusLevels.unavailable, summary: 'a unavailable' }));
service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a available' }));
subscription.unsubscribe();
expect(statusUpdates).toEqual([
{ a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' } },
{ a: { level: ServiceStatusLevels.degraded, summary: 'a degraded' } },
{ a: { level: ServiceStatusLevels.unavailable, summary: 'a unavailable' } },
{ a: { level: ServiceStatusLevels.available, summary: 'a available' } },
]);
});
});
describe('getDependenciesStatus$', () => {
it('only includes dependencies of specified plugin', async () => {
const service = new PluginsStatusService({
core$: coreAllAvailable$,
pluginDependencies,
});
expect(await service.getDependenciesStatus$('a').pipe(first()).toPromise()).toEqual({});
expect(await service.getDependenciesStatus$('b').pipe(first()).toPromise()).toEqual({
a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' },
});
expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({
a: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' },
b: { level: ServiceStatusLevels.available, summary: 'All dependencies are available' },
});
});
it('uses the manually set status level if plugin specifies one', async () => {
const service = new PluginsStatusService({ core$: coreOneDegraded$, pluginDependencies });
service.set('a', of({ level: ServiceStatusLevels.available, summary: 'a status' }));
expect(await service.getDependenciesStatus$('c').pipe(first()).toPromise()).toEqual({
a: { level: ServiceStatusLevels.available, summary: 'a status' }, // a is available depsite savedObjects being degraded
b: {
level: ServiceStatusLevels.degraded,
summary: '[savedObjects]: savedObjects degraded',
detail: 'See the status page for more information',
meta: expect.any(Object),
},
});
});
it('throws error if unknown plugin passed', () => {
const service = new PluginsStatusService({ core$: coreAllAvailable$, pluginDependencies });
expect(() => {
service.getDependenciesStatus$('dont-exist');
}).toThrowError();
});
it('debounces events in quick succession', async () => {
const service = new PluginsStatusService({
core$: coreAllAvailable$,
pluginDependencies,
});
const available: ServiceStatus = {
level: ServiceStatusLevels.available,
summary: 'a available',
};
const degraded: ServiceStatus = {
level: ServiceStatusLevels.degraded,
summary: 'a degraded',
};
const pluginA$ = new BehaviorSubject(available);
service.set('a', pluginA$);
const statusUpdates: Array<Record<string, ServiceStatus>> = [];
const subscription = service
.getDependenciesStatus$('b')
.subscribe((status) => statusUpdates.push(status));
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
pluginA$.next(degraded);
pluginA$.next(available);
pluginA$.next(degraded);
pluginA$.next(available);
pluginA$.next(degraded);
pluginA$.next(available);
pluginA$.next(degraded);
// Waiting for the debounce timeout should cut a new update
await delay(100);
pluginA$.next(available);
await delay(100);
subscription.unsubscribe();
expect(statusUpdates).toMatchInlineSnapshot(`
Array [
Object {
"a": Object {
"level": degraded,
"summary": "a degraded",
},
},
Object {
"a": Object {
"level": available,
"summary": "a available",
},
},
]
`);
});
});
});

View file

@ -1,98 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { BehaviorSubject, Observable, combineLatest, of } from 'rxjs';
import { map, distinctUntilChanged, switchMap, debounceTime } from 'rxjs/operators';
import { isDeepStrictEqual } from 'util';
import { PluginName } from '../plugins';
import { ServiceStatus, CoreStatus } from './types';
import { getSummaryStatus } from './get_summary_status';
interface Deps {
core$: Observable<CoreStatus>;
pluginDependencies: ReadonlyMap<PluginName, PluginName[]>;
}
export class PluginsStatusService {
private readonly pluginStatuses = new Map<PluginName, Observable<ServiceStatus>>();
private readonly update$ = new BehaviorSubject(true);
constructor(private readonly deps: Deps) {}
public set(plugin: PluginName, status$: Observable<ServiceStatus>) {
this.pluginStatuses.set(plugin, status$);
this.update$.next(true); // trigger all existing Observables to update from the new source Observable
}
public getAll$(): Observable<Record<PluginName, ServiceStatus>> {
return this.getPluginStatuses$([...this.deps.pluginDependencies.keys()]);
}
public getDependenciesStatus$(plugin: PluginName): Observable<Record<PluginName, ServiceStatus>> {
const dependencies = this.deps.pluginDependencies.get(plugin);
if (!dependencies) {
throw new Error(`Unknown plugin: ${plugin}`);
}
return this.getPluginStatuses$(dependencies).pipe(
// Prevent many emissions at once from dependency status resolution from making this too noisy
debounceTime(100)
);
}
public getDerivedStatus$(plugin: PluginName): Observable<ServiceStatus> {
return combineLatest([this.deps.core$, this.getDependenciesStatus$(plugin)]).pipe(
map(([coreStatus, pluginStatuses]) => {
return getSummaryStatus(
[...Object.entries(coreStatus), ...Object.entries(pluginStatuses)],
{
allAvailableSummary: `All dependencies are available`,
}
);
})
);
}
private getPluginStatuses$(plugins: PluginName[]): Observable<Record<PluginName, ServiceStatus>> {
if (plugins.length === 0) {
return of({});
}
return this.update$.pipe(
switchMap(() => {
const pluginStatuses = plugins
.map(
(depName) =>
[depName, this.pluginStatuses.get(depName) ?? this.getDerivedStatus$(depName)] as [
PluginName,
Observable<ServiceStatus>
]
)
.map(([pName, status$]) =>
status$.pipe(map((status) => [pName, status] as [PluginName, ServiceStatus]))
);
return combineLatest(pluginStatuses).pipe(
map((statuses) => Object.fromEntries(statuses)),
distinctUntilChanged(isDeepStrictEqual)
);
})
);
}
}

View file

@ -40,9 +40,6 @@ const createSetupContractMock = () => {
const setupContract: jest.Mocked<StatusServiceSetup> = {
core$: new BehaviorSubject(availableCoreStatus),
overall$: new BehaviorSubject(available),
set: jest.fn(),
dependencies$: new BehaviorSubject({}),
derivedStatus$: new BehaviorSubject(available),
};
return setupContract;
@ -53,11 +50,6 @@ const createInternalSetupContractMock = () => {
core$: new BehaviorSubject(availableCoreStatus),
overall$: new BehaviorSubject(available),
isStatusPageAnonymous: jest.fn().mockReturnValue(false),
plugins: {
set: jest.fn(),
getDependenciesStatus$: jest.fn(),
getDerivedStatus$: jest.fn(),
},
};
return setupContract;

View file

@ -34,7 +34,6 @@ describe('StatusService', () => {
service = new StatusService(mockCoreContext.create());
});
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
const available: ServiceStatus<any> = {
level: ServiceStatusLevels.available,
summary: 'Available',
@ -54,7 +53,6 @@ describe('StatusService', () => {
savedObjects: {
status$: of(degraded),
},
pluginDependencies: new Map(),
});
expect(await setup.core$.pipe(first()).toPromise()).toEqual({
elasticsearch: available,
@ -70,7 +68,6 @@ describe('StatusService', () => {
savedObjects: {
status$: of(degraded),
},
pluginDependencies: new Map(),
});
const subResult1 = await setup.core$.pipe(first()).toPromise();
const subResult2 = await setup.core$.pipe(first()).toPromise();
@ -99,7 +96,6 @@ describe('StatusService', () => {
savedObjects: {
status$: savedObjects$,
},
pluginDependencies: new Map(),
});
const statusUpdates: CoreStatus[] = [];
@ -162,7 +158,6 @@ describe('StatusService', () => {
savedObjects: {
status$: of(degraded),
},
pluginDependencies: new Map(),
});
expect(await setup.overall$.pipe(first()).toPromise()).toMatchObject({
level: ServiceStatusLevels.degraded,
@ -178,7 +173,6 @@ describe('StatusService', () => {
savedObjects: {
status$: of(degraded),
},
pluginDependencies: new Map(),
});
const subResult1 = await setup.overall$.pipe(first()).toPromise();
const subResult2 = await setup.overall$.pipe(first()).toPromise();
@ -207,95 +201,26 @@ describe('StatusService', () => {
savedObjects: {
status$: savedObjects$,
},
pluginDependencies: new Map(),
});
const statusUpdates: ServiceStatus[] = [];
const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status));
// Wait for timers to ensure that duplicate events are still filtered out regardless of debouncing.
elasticsearch$.next(available);
await delay(100);
elasticsearch$.next(available);
await delay(100);
elasticsearch$.next({
level: ServiceStatusLevels.available,
summary: `Wow another summary`,
});
await delay(100);
savedObjects$.next(degraded);
await delay(100);
savedObjects$.next(available);
await delay(100);
savedObjects$.next(available);
await delay(100);
subscription.unsubscribe();
expect(statusUpdates).toMatchInlineSnapshot(`
Array [
Object {
"detail": "See the status page for more information",
"level": degraded,
"meta": Object {
"affectedServices": Object {
"savedObjects": Object {
"level": degraded,
"summary": "This is degraded!",
},
},
},
"summary": "[savedObjects]: This is degraded!",
},
Object {
"level": available,
"summary": "All services are available",
},
]
`);
});
it('debounces events in quick succession', async () => {
const savedObjects$ = new BehaviorSubject(available);
const setup = await service.setup({
elasticsearch: {
status$: new BehaviorSubject(available),
},
savedObjects: {
status$: savedObjects$,
},
pluginDependencies: new Map(),
});
const statusUpdates: ServiceStatus[] = [];
const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status));
// All of these should debounced into a single `available` status
savedObjects$.next(degraded);
savedObjects$.next(available);
savedObjects$.next(degraded);
savedObjects$.next(available);
savedObjects$.next(degraded);
savedObjects$.next(available);
savedObjects$.next(degraded);
// Waiting for the debounce timeout should cut a new update
await delay(100);
savedObjects$.next(available);
await delay(100);
subscription.unsubscribe();
expect(statusUpdates).toMatchInlineSnapshot(`
Array [
Object {
"detail": "See the status page for more information",
"level": degraded,
"meta": Object {
"affectedServices": Object {
"savedObjects": Object {
"level": degraded,
"summary": "This is degraded!",
},
},
},
"summary": "[savedObjects]: This is degraded!",
},
Object {

View file

@ -18,7 +18,7 @@
*/
import { Observable, combineLatest } from 'rxjs';
import { map, distinctUntilChanged, shareReplay, take, debounceTime } from 'rxjs/operators';
import { map, distinctUntilChanged, shareReplay, take } from 'rxjs/operators';
import { isDeepStrictEqual } from 'util';
import { CoreService } from '../../types';
@ -26,16 +26,13 @@ import { CoreContext } from '../core_context';
import { Logger } from '../logging';
import { InternalElasticsearchServiceSetup } from '../elasticsearch';
import { InternalSavedObjectsServiceSetup } from '../saved_objects';
import { PluginName } from '../plugins';
import { config, StatusConfigType } from './status_config';
import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types';
import { getSummaryStatus } from './get_summary_status';
import { PluginsStatusService } from './plugins_status';
interface SetupDeps {
elasticsearch: Pick<InternalElasticsearchServiceSetup, 'status$'>;
pluginDependencies: ReadonlyMap<PluginName, PluginName[]>;
savedObjects: Pick<InternalSavedObjectsServiceSetup, 'status$'>;
}
@ -43,29 +40,17 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
private readonly logger: Logger;
private readonly config$: Observable<StatusConfigType>;
private pluginsStatus?: PluginsStatusService;
constructor(coreContext: CoreContext) {
this.logger = coreContext.logger.get('status');
this.config$ = coreContext.configService.atPath<StatusConfigType>(config.path);
}
public async setup({ elasticsearch, pluginDependencies, savedObjects }: SetupDeps) {
public async setup(core: SetupDeps) {
const statusConfig = await this.config$.pipe(take(1)).toPromise();
const core$ = this.setupCoreStatus({ elasticsearch, savedObjects });
this.pluginsStatus = new PluginsStatusService({ core$, pluginDependencies });
const overall$: Observable<ServiceStatus> = combineLatest(
core$,
this.pluginsStatus.getAll$()
).pipe(
// Prevent many emissions at once from dependency status resolution from making this too noisy
debounceTime(100),
map(([coreStatus, pluginsStatus]) => {
const summary = getSummaryStatus([
...Object.entries(coreStatus),
...Object.entries(pluginsStatus),
]);
const core$ = this.setupCoreStatus(core);
const overall$: Observable<ServiceStatus> = core$.pipe(
map((coreStatus) => {
const summary = getSummaryStatus(Object.entries(coreStatus));
this.logger.debug(`Recalculated overall status`, { status: summary });
return summary;
}),
@ -75,11 +60,6 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
return {
core$,
overall$,
plugins: {
set: this.pluginsStatus.set.bind(this.pluginsStatus),
getDependenciesStatus$: this.pluginsStatus.getDependenciesStatus$.bind(this.pluginsStatus),
getDerivedStatus$: this.pluginsStatus.getDerivedStatus$.bind(this.pluginsStatus),
},
isStatusPageAnonymous: () => statusConfig.allowAnonymous,
};
}
@ -88,10 +68,7 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
public stop() {}
private setupCoreStatus({
elasticsearch,
savedObjects,
}: Pick<SetupDeps, 'elasticsearch' | 'savedObjects'>): Observable<CoreStatus> {
private setupCoreStatus({ elasticsearch, savedObjects }: SetupDeps): Observable<CoreStatus> {
return combineLatest([elasticsearch.status$, savedObjects.status$]).pipe(
map(([elasticsearchStatus, savedObjectsStatus]) => ({
elasticsearch: elasticsearchStatus,

View file

@ -19,7 +19,6 @@
import { Observable } from 'rxjs';
import { deepFreeze } from '../../utils';
import { PluginName } from '../plugins';
/**
* The current status of a service at a point in time.
@ -117,60 +116,6 @@ export interface CoreStatus {
/**
* API for accessing status of Core and this plugin's dependencies as well as for customizing this plugin's status.
*
* @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
* {@link ServiceStatusSetup.derivedStatus$ | core.status.derviedStatus$} API.
*
* Plugins may customize their status calculation by calling the {@link ServiceStatusSetup.set | core.status.set} 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
* 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
* 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;
* }
* })
* )
* );
* ```
*
* @public
*/
export interface StatusServiceSetup {
@ -189,43 +134,9 @@ export interface StatusServiceSetup {
* only depend on the statuses of {@link StatusServiceSetup.core$ | Core} or their dependencies.
*/
overall$: Observable<ServiceStatus>;
/**
* Allows a plugin to specify a custom status dependent on its own criteria.
* Completely overrides the default inherited status.
*
* @remarks
* See the {@link StatusServiceSetup.derivedStatus$} API for leveraging the default status
* calculation that is provided by Core.
*/
set(status$: Observable<ServiceStatus>): void;
/**
* Current status for all plugins this plugin depends on.
* Each key of the `Record` is a plugin id.
*/
dependencies$: Observable<Record<string, ServiceStatus>>;
/**
* The status of this plugin as derived from its dependencies.
*
* @remarks
* By default, plugins inherit this derived status from their dependencies.
* Calling {@link StatusSetup.set} overrides this default status.
*
* This may emit multliple times for a single status change event as propagates
* through the dependency tree
*/
derivedStatus$: Observable<ServiceStatus>;
}
/** @internal */
export interface InternalStatusServiceSetup extends Pick<StatusServiceSetup, 'core$' | 'overall$'> {
export interface InternalStatusServiceSetup extends StatusServiceSetup {
isStatusPageAnonymous: () => boolean;
// Namespaced under `plugins` key to improve clarity that these are APIs for plugins specifically.
plugins: {
set(plugin: PluginName, status$: Observable<ServiceStatus>): void;
getDependenciesStatus$(plugin: PluginName): Observable<Record<string, ServiceStatus>>;
getDerivedStatus$(plugin: PluginName): Observable<ServiceStatus>;
};
}