From 47354d03e2fdb2d311b4f595c0128ecb2e86e6d9 Mon Sep 17 00:00:00 2001 From: Josh Dover Date: Wed, 2 Sep 2020 11:22:51 -0600 Subject: [PATCH] Complete migration of legacy status API --- .i18nrc.json | 3 +- .../kibana-plugin-core-server.coresetup.md | 1 + ...na-plugin-core-server.coresetup.metrics.md | 13 + .../kibana-plugin-core-server.corestart.md | 2 +- ...na-plugin-core-server.corestart.metrics.md | 1 + .../core/server/kibana-plugin-core-server.md | 1 + ...-plugin-core-server.metricsservicestart.md | 13 + .../version_check/ensure_es_version.ts | 2 +- src/core/server/index.ts | 5 +- src/core/server/internal_types.ts | 3 +- src/core/server/legacy/legacy_service.test.ts | 2 + src/core/server/legacy/legacy_service.ts | 5 + .../server/metrics/metrics_service.mock.ts | 4 +- src/core/server/mocks.ts | 6 +- src/core/server/plugins/plugin_context.ts | 5 + src/core/server/server.api.md | 8 +- src/core/server/server.ts | 6 +- src/core/server/status/legacy_status.test.ts | 114 +++++++ src/core/server/status/legacy_status.ts | 152 +++++++++ .../server/status/routes/index.ts} | 2 +- .../routes/integration_tests/status.test.ts | 322 ++++++++++++++++++ src/core/server/status/routes/status.ts | 198 +++++++++++ src/core/server/status/status_service.mock.ts | 1 + src/core/server/status/status_service.test.ts | 153 +++++---- src/core/server/status/status_service.ts | 48 ++- src/core/server/status/types.ts | 10 +- src/legacy/server/kbn_server.js | 2 - src/legacy/server/plugins/lib/plugin.js | 11 - src/legacy/server/status/index.js | 61 ---- .../server/status/lib/__mocks__/_fs_stubs.js | 86 ----- .../server/status/lib/case_conversion.test.ts | 36 -- .../server/status/lib/case_conversion.ts | 24 -- src/legacy/server/status/lib/cgroup.js | 173 ---------- src/legacy/server/status/lib/cgroup.test.js | 224 ------------ .../status/lib/get_kibana_info_for_stats.js | 47 --- src/legacy/server/status/lib/get_os_info.js | 48 --- .../server/status/lib/get_os_info.test.js | 68 ---- src/legacy/server/status/lib/metrics.js | 146 -------- src/legacy/server/status/lib/metrics.test.js | 245 ------------- .../status/routes/api/register_stats.js | 164 --------- .../status/routes/api/register_status.js | 50 --- src/legacy/server/status/routes/index.js | 21 -- src/legacy/server/status/samples.js | 45 --- src/legacy/server/status/server_status.js | 116 ------- .../server/status/server_status.test.js | 145 -------- src/legacy/server/status/states.js | 85 ----- src/legacy/server/status/status.js | 108 ------ src/legacy/server/status/status.test.js | 147 -------- src/legacy/server/status/wrap_auth_config.js | 27 -- .../server/status/wrap_auth_config.test.js | 60 ---- src/plugins/usage_collection/server/plugin.ts | 19 +- .../usage_collection/server/routes/index.ts | 37 +- .../routes/integration_tests/stats.test.ts | 104 ++++++ .../usage_collection/server/routes/stats.ts | 200 +++++++++++ x-pack/legacy/plugins/xpack_main/index.js | 3 - .../server/lib/__tests__/setup_xpack_main.js | 77 +---- .../xpack_main/server/lib/setup_xpack_main.js | 24 +- .../server/routes/api/v1/settings.js | 15 +- .../lib/__tests__/mirror_plugin_status.js | 108 ------ .../legacy/server/lib/mirror_plugin_status.js | 22 -- .../register_license_checker.js | 32 +- .../translations/translations/ja-JP.json | 10 +- .../translations/translations/zh-CN.json | 10 +- 63 files changed, 1414 insertions(+), 2466 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md create mode 100644 docs/development/core/server/kibana-plugin-core-server.metricsservicestart.md create mode 100644 src/core/server/status/legacy_status.test.ts create mode 100644 src/core/server/status/legacy_status.ts rename src/{legacy/server/status/lib/index.js => core/server/status/routes/index.ts} (92%) create mode 100644 src/core/server/status/routes/integration_tests/status.test.ts create mode 100644 src/core/server/status/routes/status.ts delete mode 100644 src/legacy/server/status/index.js delete mode 100644 src/legacy/server/status/lib/__mocks__/_fs_stubs.js delete mode 100644 src/legacy/server/status/lib/case_conversion.test.ts delete mode 100644 src/legacy/server/status/lib/case_conversion.ts delete mode 100644 src/legacy/server/status/lib/cgroup.js delete mode 100644 src/legacy/server/status/lib/cgroup.test.js delete mode 100644 src/legacy/server/status/lib/get_kibana_info_for_stats.js delete mode 100644 src/legacy/server/status/lib/get_os_info.js delete mode 100644 src/legacy/server/status/lib/get_os_info.test.js delete mode 100644 src/legacy/server/status/lib/metrics.js delete mode 100644 src/legacy/server/status/lib/metrics.test.js delete mode 100644 src/legacy/server/status/routes/api/register_stats.js delete mode 100644 src/legacy/server/status/routes/api/register_status.js delete mode 100644 src/legacy/server/status/routes/index.js delete mode 100644 src/legacy/server/status/samples.js delete mode 100644 src/legacy/server/status/server_status.js delete mode 100644 src/legacy/server/status/server_status.test.js delete mode 100644 src/legacy/server/status/states.js delete mode 100644 src/legacy/server/status/status.js delete mode 100644 src/legacy/server/status/status.test.js delete mode 100644 src/legacy/server/status/wrap_auth_config.js delete mode 100644 src/legacy/server/status/wrap_auth_config.test.js create mode 100644 src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts create mode 100644 src/plugins/usage_collection/server/routes/stats.ts delete mode 100644 x-pack/legacy/server/lib/__tests__/mirror_plugin_status.js delete mode 100644 x-pack/legacy/server/lib/mirror_plugin_status.js diff --git a/.i18nrc.json b/.i18nrc.json index e8431fdb3f0e171..153a5a6cafecebe 100644 --- a/.i18nrc.json +++ b/.i18nrc.json @@ -57,7 +57,8 @@ "visTypeXy": "src/plugins/vis_type_xy", "visualizations": "src/plugins/visualizations", "visualize": "src/plugins/visualize", - "apmOss": "src/plugins/apm_oss" + "apmOss": "src/plugins/apm_oss", + "usageCollection": "src/plugins/usage_collection" }, "exclude": [ "src/legacy/ui/ui_render/ui_render_mixin.js" diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.md index ccc73d4fb858ec0..75da8df2ae15a8e 100644 --- a/docs/development/core/server/kibana-plugin-core-server.coresetup.md +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.md @@ -23,6 +23,7 @@ export interface CoreSetupStartServicesAccessor<TPluginsStart, TStart> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) | | [http](./kibana-plugin-core-server.coresetup.http.md) | HttpServiceSetup & {
resources: HttpResources;
} | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) | | [logging](./kibana-plugin-core-server.coresetup.logging.md) | LoggingServiceSetup | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) | +| [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | MetricsServiceSetup | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) | | [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | SavedObjectsServiceSetup | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) | | [status](./kibana-plugin-core-server.coresetup.status.md) | StatusServiceSetup | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) | | [uiSettings](./kibana-plugin-core-server.coresetup.uisettings.md) | UiSettingsServiceSetup | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md b/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md new file mode 100644 index 000000000000000..77c9e867ef8ea84 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.coresetup.metrics.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [CoreSetup](./kibana-plugin-core-server.coresetup.md) > [metrics](./kibana-plugin-core-server.coresetup.metrics.md) + +## CoreSetup.metrics property + +[MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) + +Signature: + +```typescript +metrics: MetricsServiceSetup; +``` diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.md b/docs/development/core/server/kibana-plugin-core-server.corestart.md index 610c85c71e3629c..0d5474fae5e168c 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.md @@ -20,7 +20,7 @@ export interface CoreStart | [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | CapabilitiesStart | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) | | [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | ElasticsearchServiceStart | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) | | [http](./kibana-plugin-core-server.corestart.http.md) | HttpServiceStart | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) | -| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | MetricsServiceStart | | +| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | MetricsServiceStart | [MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) | | [savedObjects](./kibana-plugin-core-server.corestart.savedobjects.md) | SavedObjectsServiceStart | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) | | [uiSettings](./kibana-plugin-core-server.corestart.uisettings.md) | UiSettingsServiceStart | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) | diff --git a/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md b/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md index a51c2f842c3464a..2c32f730c4c9b2b 100644 --- a/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md +++ b/docs/development/core/server/kibana-plugin-core-server.corestart.metrics.md @@ -4,6 +4,7 @@ ## CoreStart.metrics property +[MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) Signature: diff --git a/docs/development/core/server/kibana-plugin-core-server.md b/docs/development/core/server/kibana-plugin-core-server.md index dfffdffb08a082f..d3420b6c5b0855f 100644 --- a/docs/development/core/server/kibana-plugin-core-server.md +++ b/docs/development/core/server/kibana-plugin-core-server.md @@ -269,6 +269,7 @@ The plugin integrates with the core system via lifecycle events: `setup` | [LegacyElasticsearchClientConfig](./kibana-plugin-core-server.legacyelasticsearchclientconfig.md) | | | [LifecycleResponseFactory](./kibana-plugin-core-server.lifecycleresponsefactory.md) | Creates an object containing redirection or error response with error details, HTTP headers, and other data transmitted to the client. | | [LoggerConfigType](./kibana-plugin-core-server.loggerconfigtype.md) | | +| [MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) | APIs to retrieves metrics gathered and exposed by the core platform. | | [MIGRATION\_ASSISTANCE\_INDEX\_ACTION](./kibana-plugin-core-server.migration_assistance_index_action.md) | | | [MIGRATION\_DEPRECATION\_LEVEL](./kibana-plugin-core-server.migration_deprecation_level.md) | | | [MutatingOperationRefreshSetting](./kibana-plugin-core-server.mutatingoperationrefreshsetting.md) | Elasticsearch Refresh setting for mutating operation | diff --git a/docs/development/core/server/kibana-plugin-core-server.metricsservicestart.md b/docs/development/core/server/kibana-plugin-core-server.metricsservicestart.md new file mode 100644 index 000000000000000..8b3280d528c186b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-core-server.metricsservicestart.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) + +## MetricsServiceStart type + +APIs to retrieves metrics gathered and exposed by the core platform. + +Signature: + +```typescript +export declare type MetricsServiceStart = MetricsServiceSetup; +``` diff --git a/src/core/server/elasticsearch/version_check/ensure_es_version.ts b/src/core/server/elasticsearch/version_check/ensure_es_version.ts index 5f926215d167ff5..70ff8857117de83 100644 --- a/src/core/server/elasticsearch/version_check/ensure_es_version.ts +++ b/src/core/server/elasticsearch/version_check/ensure_es_version.ts @@ -72,7 +72,7 @@ export function mapNodesVersionCompatibility( kibanaVersion: string, ignoreVersionMismatch: boolean ): NodesVersionCompatibility { - if (Object.keys(nodesInfo.nodes).length === 0) { + if (Object.keys(nodesInfo.nodes ?? {}).length === 0) { return { isCompatible: false, message: 'Unable to retrieve version information from Elasticsearch nodes.', diff --git a/src/core/server/index.ts b/src/core/server/index.ts index 97aca74bfd48fbc..0ced7b7e799b9ea 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -60,7 +60,7 @@ import { SavedObjectsServiceStart, } from './saved_objects'; import { CapabilitiesSetup, CapabilitiesStart } from './capabilities'; -import { MetricsServiceStart } from './metrics'; +import { MetricsServiceSetup, MetricsServiceStart } from './metrics'; import { StatusServiceSetup } from './status'; import { Auditor, AuditTrailSetup, AuditTrailStart } from './audit_trail'; import { AppenderConfigType, appendersSchema, LoggingServiceSetup } from './logging'; @@ -319,6 +319,7 @@ export { OpsServerMetrics, OpsProcessMetrics, MetricsServiceSetup, + MetricsServiceStart, } from './metrics'; export { @@ -422,6 +423,8 @@ export interface CoreSetup = KbnServer as any; @@ -99,6 +100,7 @@ beforeEach(() => { status: statusServiceMock.createInternalSetupContract(), auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), + metrics: metricsServiceMock.createInternalSetupContract(), }, plugins: { 'plugin-id': 'plugin-value' }, uiPlugins: { diff --git a/src/core/server/legacy/legacy_service.ts b/src/core/server/legacy/legacy_service.ts index 6e6d5cfc24340e4..c1857d83a40fdc1 100644 --- a/src/core/server/legacy/legacy_service.ts +++ b/src/core/server/legacy/legacy_service.ts @@ -302,6 +302,10 @@ export class LegacyService implements CoreService { logging: { configure: (config$) => setupDeps.core.logging.configure([], config$), }, + metrics: { + collectionInterval: setupDeps.core.metrics.collectionInterval, + getOpsMetrics$: setupDeps.core.metrics.getOpsMetrics$, + }, savedObjects: { setClientFactoryProvider: setupDeps.core.savedObjects.setClientFactoryProvider, addClientWrapper: setupDeps.core.savedObjects.addClientWrapper, @@ -309,6 +313,7 @@ export class LegacyService implements CoreService { getImportExportObjectLimit: setupDeps.core.savedObjects.getImportExportObjectLimit, }, status: { + isStatusPageAnonymous: setupDeps.core.status.isStatusPageAnonymous, core$: setupDeps.core.status.core$, overall$: setupDeps.core.status.overall$, set: () => { diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts index 2af653004a479e6..3cd0a322e35e3b5 100644 --- a/src/core/server/metrics/metrics_service.mock.ts +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -76,8 +76,8 @@ type MetricsServiceContract = PublicMethodsOf; const createMock = () => { const mocked: jest.Mocked = { - setup: jest.fn().mockReturnValue(createInternalSetupContractMock()), - start: jest.fn().mockReturnValue(createInternalStartContractMock()), + setup: jest.fn().mockReturnValue(createSetupContractMock()), + start: jest.fn().mockReturnValue(createStartContractMock()), stop: jest.fn(), }; return mocked; diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 3c79706422cd438..9627fe62d8c05ac 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -51,6 +51,8 @@ export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_object export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; export { metricsServiceMock } from './metrics/metrics_service.mock'; export { renderingMock } from './rendering/rendering_service.mock'; +export { statusServiceMock } from './status/status_service.mock'; +export { contextServiceMock } from './context/context_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -133,6 +135,7 @@ function createCoreSetupMock({ uiSettings: uiSettingsMock, auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createSetupContract(), + metrics: metricsServiceMock.createSetupContract(), getStartServices: jest .fn, object, any]>, []>() .mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]), @@ -169,6 +172,7 @@ function createInternalCoreSetupMock() { uiSettings: uiSettingsServiceMock.createSetupContract(), auditTrail: auditTrailServiceMock.createSetupContract(), logging: loggingServiceMock.createInternalSetupContract(), + metrics: metricsServiceMock.createInternalSetupContract(), }; return setupDeps; } @@ -178,7 +182,7 @@ function createInternalCoreStartMock() { capabilities: capabilitiesServiceMock.createStartContract(), elasticsearch: elasticsearchServiceMock.createInternalStart(), http: httpServiceMock.createInternalStartContract(), - metrics: metricsServiceMock.createStartContract(), + metrics: metricsServiceMock.createInternalStartContract(), savedObjects: savedObjectsServiceMock.createInternalStartContract(), uiSettings: uiSettingsServiceMock.createStartContract(), auditTrail: auditTrailServiceMock.createStartContract(), diff --git a/src/core/server/plugins/plugin_context.ts b/src/core/server/plugins/plugin_context.ts index af0b0e19b32275b..5e7344615021230 100644 --- a/src/core/server/plugins/plugin_context.ts +++ b/src/core/server/plugins/plugin_context.ts @@ -176,6 +176,10 @@ export function createPluginSetupContext( logging: { configure: (config$) => deps.logging.configure(['plugins', plugin.name], config$), }, + metrics: { + collectionInterval: deps.metrics.collectionInterval, + getOpsMetrics$: deps.metrics.getOpsMetrics$, + }, savedObjects: { setClientFactoryProvider: deps.savedObjects.setClientFactoryProvider, addClientWrapper: deps.savedObjects.addClientWrapper, @@ -188,6 +192,7 @@ export function createPluginSetupContext( set: deps.status.plugins.set.bind(null, plugin.name), dependencies$: deps.status.plugins.getDependenciesStatus$(plugin.name), derivedStatus$: deps.status.plugins.getDerivedStatus$(plugin.name), + isStatusPageAnonymous: deps.status.isStatusPageAnonymous, }, uiSettings: { register: deps.uiSettings.register, diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index aef1bda9ccf4e1e..57931f71756e2ae 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -436,6 +436,8 @@ export interface CoreSetup Observable; } +// @public +export type MetricsServiceStart = MetricsServiceSetup; + // @public @deprecated (undocumented) export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; diff --git a/src/core/server/server.ts b/src/core/server/server.ts index a02b0f51b559f3d..6262717f4ce21ec 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -152,12 +152,15 @@ export class Server { savedObjects: savedObjectsSetup, }); - await this.metrics.setup({ http: httpSetup }); + const metricsSetup = await this.metrics.setup({ http: httpSetup }); const statusSetup = await this.status.setup({ elasticsearch: elasticsearchServiceSetup, pluginDependencies: pluginTree.asNames, savedObjects: savedObjectsSetup, + environment: environmentSetup, + http: httpSetup, + metrics: metricsSetup, }); const renderingSetup = await this.rendering.setup({ @@ -189,6 +192,7 @@ export class Server { httpResources: httpResourcesSetup, auditTrail: auditTrailSetup, logging: loggingSetup, + metrics: metricsSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); diff --git a/src/core/server/status/legacy_status.test.ts b/src/core/server/status/legacy_status.test.ts new file mode 100644 index 000000000000000..e3e55442cabd2ca --- /dev/null +++ b/src/core/server/status/legacy_status.test.ts @@ -0,0 +1,114 @@ +/* + * 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 { ServiceStatus, ServiceStatusLevels } from './types'; +import { calculateLegacyStatus } from './legacy_status'; + +const available: ServiceStatus = { level: ServiceStatusLevels.available, summary: 'Available' }; +const degraded: ServiceStatus = { + level: ServiceStatusLevels.degraded, + summary: 'This is degraded!', +}; +const unavailable: ServiceStatus = { + level: ServiceStatusLevels.unavailable, + summary: 'This is unavailable!', +}; +const critical: ServiceStatus = { + level: ServiceStatusLevels.critical, + summary: 'This is critical!', +}; + +describe('calculateLegacyStatus', () => { + it('translates the overall status to the legacy format', () => { + const legacyStatus = calculateLegacyStatus({ + overall: available, + core: {} as any, + plugins: {}, + versionWithoutSnapshot: '1.1.1', + }); + + expect(legacyStatus.overall).toEqual({ + state: 'green', + title: 'Green', + nickname: 'Looking good', + icon: 'success', + uiColor: 'secondary', + since: expect.any(String), + }); + }); + + it('combines core and plugins statuses into statuses array in legacy format', () => { + const legacyStatus = calculateLegacyStatus({ + overall: available, + core: { + elasticsearch: degraded, + savedObjects: critical, + }, + plugins: { + a: available, + b: unavailable, + c: degraded, + }, + versionWithoutSnapshot: '1.1.1', + }); + + expect(legacyStatus.statuses).toEqual([ + { + icon: 'warning', + id: 'core:elasticsearch@1.1.1', + message: 'This is degraded!', + since: expect.any(String), + state: 'yellow', + uiColor: 'warning', + }, + { + icon: 'danger', + id: 'core:savedObjects@1.1.1', + message: 'This is critical!', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + { + icon: 'success', + id: 'plugin:a@1.1.1', + message: 'Available', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'danger', + id: 'plugin:b@1.1.1', + message: 'This is unavailable!', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + { + icon: 'warning', + id: 'plugin:c@1.1.1', + message: 'This is degraded!', + since: expect.any(String), + state: 'yellow', + uiColor: 'warning', + }, + ]); + }); +}); diff --git a/src/core/server/status/legacy_status.ts b/src/core/server/status/legacy_status.ts new file mode 100644 index 000000000000000..f97e3652cf24b2e --- /dev/null +++ b/src/core/server/status/legacy_status.ts @@ -0,0 +1,152 @@ +/* + * 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 { pick } from 'lodash'; +import { i18n } from '@kbn/i18n'; +import { ServiceStatusLevels, ServiceStatus, CoreStatus } from './types'; +import { deepFreeze } from '../../utils'; +import { PluginName } from '../plugins'; + +interface Deps { + overall: ServiceStatus; + core: CoreStatus; + plugins: Record; + versionWithoutSnapshot: string; +} + +export interface LegacyStatusInfo { + overall: { + state: LegacyStatusState; + title: string; + nickname: string; + uiColor: LegacyStatusUiColor; + /** ISO-8601 date string w/o timezone */ + since: string; + icon?: string; + }; + statuses: StatusComponentHttp[]; +} + +export const calculateLegacyStatus = ({ + core, + overall, + plugins, + versionWithoutSnapshot, +}: Deps): LegacyStatusInfo => { + const since = new Date().toISOString(); + const overallLegacy: LegacyStatusInfo['overall'] = { + since, + ...pick(STATUS_LEVEL_LEGACY_ATTRS[overall.level.toString()], [ + 'state', + 'title', + 'nickname', + 'icon', + 'uiColor', + ]), + }; + const coreStatuses = Object.entries(core).map(([serviceName, s]) => + serviceStatusToHttpComponent(`core:${serviceName}@${versionWithoutSnapshot}`, s, since) + ); + const pluginStatuses = Object.entries(plugins).map(([pluginName, s]) => + serviceStatusToHttpComponent(`plugin:${pluginName}@${versionWithoutSnapshot}`, s, since) + ); + + const componentStatuses: StatusComponentHttp[] = [...coreStatuses, ...pluginStatuses]; + + return { + overall: overallLegacy, + statuses: componentStatuses, + }; +}; + +interface StatusComponentHttp { + id: string; + state: LegacyStatusState; + message: string; + uiColor: LegacyStatusUiColor; + icon: string; + since: string; +} + +const serviceStatusToHttpComponent = ( + serviceName: string, + status: ServiceStatus, + since: string +): StatusComponentHttp => ({ + id: serviceName, + message: status.summary, + since, + ...pick(STATUS_LEVEL_LEGACY_ATTRS[status.level.toString()], ['state', 'icon', 'uiColor']), // TODO: only pick needed fields +}); + +type LegacyStatusState = 'green' | 'yellow' | 'red'; +type LegacyStatusIcon = 'danger' | 'warning' | 'success'; +type LegacyStatusUiColor = 'secondary' | 'warning' | 'danger'; + +interface LegacyStateAttr { + id: LegacyStatusState; + state: LegacyStatusState; + title: string; + icon: LegacyStatusIcon; + uiColor: LegacyStatusUiColor; + nickname: string; +} + +const STATUS_LEVEL_LEGACY_ATTRS = deepFreeze>({ + [ServiceStatusLevels.critical.toString()]: { + id: 'red', + state: 'red', + title: i18n.translate('core.status.redTitle', { + defaultMessage: 'Red', + }), + icon: 'danger', + uiColor: 'danger', + nickname: 'Danger Will Robinson! Danger!', + }, + [ServiceStatusLevels.unavailable.toString()]: { + id: 'red', + state: 'red', + title: i18n.translate('core.status.redTitle', { + defaultMessage: 'Red', + }), + icon: 'danger', + uiColor: 'danger', + nickname: 'Danger Will Robinson! Danger!', + }, + [ServiceStatusLevels.degraded.toString()]: { + id: 'yellow', + state: 'yellow', + title: i18n.translate('core.status.yellowTitle', { + defaultMessage: 'Yellow', + }), + icon: 'warning', + uiColor: 'warning', + nickname: "I'll be back", + }, + [ServiceStatusLevels.available.toString()]: { + id: 'green', + state: 'green', + title: i18n.translate('core.status.greenTitle', { + defaultMessage: 'Green', + }), + icon: 'success', + uiColor: 'secondary', + nickname: 'Looking good', + }, +}); diff --git a/src/legacy/server/status/lib/index.js b/src/core/server/status/routes/index.ts similarity index 92% rename from src/legacy/server/status/lib/index.js rename to src/core/server/status/routes/index.ts index 93db8b2d2256139..db2e8daf0b9ac64 100644 --- a/src/legacy/server/status/lib/index.js +++ b/src/core/server/status/routes/index.ts @@ -17,4 +17,4 @@ * under the License. */ -export { getKibanaInfoForStats } from './get_kibana_info_for_stats'; +export { registerStatusRoute } from './status'; diff --git a/src/core/server/status/routes/integration_tests/status.test.ts b/src/core/server/status/routes/integration_tests/status.test.ts new file mode 100644 index 000000000000000..e0f86342e3a8a29 --- /dev/null +++ b/src/core/server/status/routes/integration_tests/status.test.ts @@ -0,0 +1,322 @@ +/* + * 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 } from 'rxjs'; +import { first } from 'rxjs/operators'; +import supertest from 'supertest'; +import { omit } from 'lodash'; + +import { createCoreContext, createHttpServer } from '../../../http/test_utils'; +import { ContextService } from '../../../context'; +import { metricsServiceMock } from '../../../metrics/metrics_service.mock'; +import { MetricsServiceSetup } from '../../../metrics'; +import { HttpService, InternalHttpServiceSetup } from '../../../http'; + +import { registerStatusRoute } from '../status'; +import { ServiceStatus, ServiceStatusLevels } from '../../types'; +import { statusServiceMock } from '../../status_service.mock'; + +const coreId = Symbol('core'); + +describe('GET /api/status', () => { + let server: HttpService; + let httpSetup: InternalHttpServiceSetup; + let metrics: jest.Mocked; + + const setupServer = async ({ allowAnonymous = true }: { allowAnonymous?: boolean } = {}) => { + const coreContext = createCoreContext({ coreId }); + const contextService = new ContextService(coreContext); + + server = createHttpServer(coreContext); + httpSetup = await server.setup({ + context: contextService.setup({ pluginDependencies: new Map() }), + }); + + metrics = metricsServiceMock.createSetupContract(); + const status = statusServiceMock.createSetupContract(); + const pluginsStatus$ = new BehaviorSubject>({ + a: { level: ServiceStatusLevels.available, summary: 'a is available' }, + b: { level: ServiceStatusLevels.degraded, summary: 'b is degraded' }, + c: { level: ServiceStatusLevels.unavailable, summary: 'c is unavailable' }, + d: { level: ServiceStatusLevels.critical, summary: 'd is critical' }, + }); + + const router = httpSetup.createRouter(''); + registerStatusRoute({ + router, + config: { + allowAnonymous, + packageInfo: { + branch: 'xbranch', + buildNum: 1234, + buildSha: 'xsha', + dist: true, + version: '9.9.9-SNAPSHOT', + }, + serverName: 'xkibana', + uuid: 'xxxx-xxxxx', + }, + metrics, + status: { + overall$: status.overall$, + core$: status.core$, + plugins$: pluginsStatus$, + }, + }); + + // Register dummy auth provider for testing auth + httpSetup.registerAuth((req, res, auth) => { + if (req.headers.authorization === 'let me in') { + return auth.authenticated(); + } else { + return auth.notHandled(); + } + }); + + await server.start(); + }; + + afterEach(async () => { + await server.stop(); + }); + + describe('allowAnonymous: false', () => { + it('rejects requests with no credentials', async () => { + await setupServer({ allowAnonymous: false }); + await supertest(httpSetup.server.listener).get('/api/status').expect(401); + }); + + it('rejects requests with bad credentials', async () => { + await setupServer({ allowAnonymous: false }); + await supertest(httpSetup.server.listener) + .get('/api/status') + .set('Authorization', 'fake creds') + .expect(401); + }); + + it('accepts authenticated requests', async () => { + await setupServer({ allowAnonymous: false }); + await supertest(httpSetup.server.listener) + .get('/api/status') + .set('Authorization', 'let me in') + .expect(200); + }); + }); + + it('returns basic server info & metrics', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200); + + expect(result.body.name).toEqual('xkibana'); + expect(result.body.uuid).toEqual('xxxx-xxxxx'); + expect(result.body.version).toEqual({ + number: '9.9.9', + build_hash: 'xsha', + build_number: 1234, + build_snapshot: true, + }); + const metricsMockValue = await metrics.getOpsMetrics$().pipe(first()).toPromise(); + expect(result.body.metrics).toEqual({ + last_updated: expect.any(String), + collection_interval_in_millis: metrics.collectionInterval, + ...omit(metricsMockValue, ['collected_at']), + requests: { + ...metricsMockValue.requests, + status_codes: metricsMockValue.requests.statusCodes, + }, + }); + }); + + describe('legacy status format', () => { + it('returns legacy status format when no query params provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200); + expect(result.body.status).toEqual({ + overall: { + icon: 'success', + nickname: 'Looking good', + since: expect.any(String), + state: 'green', + title: 'Green', + uiColor: 'secondary', + }, + statuses: [ + { + icon: 'success', + id: 'core:elasticsearch@9.9.9', + message: 'Service is working', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'success', + id: 'core:savedObjects@9.9.9', + message: 'Service is working', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'success', + id: 'plugin:a@9.9.9', + message: 'a is available', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'warning', + id: 'plugin:b@9.9.9', + message: 'b is degraded', + since: expect.any(String), + state: 'yellow', + uiColor: 'warning', + }, + { + icon: 'danger', + id: 'plugin:c@9.9.9', + message: 'c is unavailable', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + { + icon: 'danger', + id: 'plugin:d@9.9.9', + message: 'd is critical', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + ], + }); + }); + + it('returns legacy status format when v8format=false is provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener) + .get('/api/status?v8format=false') + .expect(200); + expect(result.body.status).toEqual({ + overall: { + icon: 'success', + nickname: 'Looking good', + since: expect.any(String), + state: 'green', + title: 'Green', + uiColor: 'secondary', + }, + statuses: [ + { + icon: 'success', + id: 'core:elasticsearch@9.9.9', + message: 'Service is working', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'success', + id: 'core:savedObjects@9.9.9', + message: 'Service is working', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'success', + id: 'plugin:a@9.9.9', + message: 'a is available', + since: expect.any(String), + state: 'green', + uiColor: 'secondary', + }, + { + icon: 'warning', + id: 'plugin:b@9.9.9', + message: 'b is degraded', + since: expect.any(String), + state: 'yellow', + uiColor: 'warning', + }, + { + icon: 'danger', + id: 'plugin:c@9.9.9', + message: 'c is unavailable', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + { + icon: 'danger', + id: 'plugin:d@9.9.9', + message: 'd is critical', + since: expect.any(String), + state: 'red', + uiColor: 'danger', + }, + ], + }); + }); + }); + + describe('v8format', () => { + it('returns new status format when v8format=true is provided', async () => { + await setupServer(); + const result = await supertest(httpSetup.server.listener) + .get('/api/status?v8format=true') + .expect(200); + expect(result.body.status).toEqual({ + core: { + elasticsearch: { + level: 'available', + summary: 'Service is working', + }, + savedObjects: { + level: 'available', + summary: 'Service is working', + }, + }, + overall: { + level: 'available', + summary: 'Service is working', + }, + plugins: { + a: { + level: 'available', + summary: 'a is available', + }, + b: { + level: 'degraded', + summary: 'b is degraded', + }, + c: { + level: 'unavailable', + summary: 'c is unavailable', + }, + d: { + level: 'critical', + summary: 'd is critical', + }, + }, + }); + }); + }); +}); diff --git a/src/core/server/status/routes/status.ts b/src/core/server/status/routes/status.ts new file mode 100644 index 000000000000000..867d4cc541f8b0d --- /dev/null +++ b/src/core/server/status/routes/status.ts @@ -0,0 +1,198 @@ +/* + * 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 { Observable, combineLatest, ReplaySubject } from 'rxjs'; +import { first } from 'rxjs/operators'; +import { schema } from '@kbn/config-schema'; + +import { IRouter } from '../../http'; +import { MetricsServiceSetup } from '../../metrics'; +import { ServiceStatus, CoreStatus } from '../types'; +import { PluginName } from '../../plugins'; +import { calculateLegacyStatus, LegacyStatusInfo } from '../legacy_status'; +import { PackageInfo } from '../../config'; + +const SNAPSHOT_POSTFIX = /-SNAPSHOT$/; + +interface Deps { + router: IRouter; + config: { + allowAnonymous: boolean; + packageInfo: PackageInfo; + serverName: string; + uuid: string; + }; + metrics: MetricsServiceSetup; + status: { + overall$: Observable; + core$: Observable; + plugins$: Observable>; + }; +} + +interface SerializableServiceStatus extends Omit { + level: string; +} + +interface StatusInfo { + overall: SerializableServiceStatus; + core: Record; + plugins: Record; +} + +interface StatusHttpBody { + name: string; + uuid: string; + version: { + number: string; + build_hash: string; + build_number: number; + build_snapshot: boolean; + }; + status: StatusInfo | LegacyStatusInfo; + metrics: { + /** ISO-8601 date string w/o timezone */ + last_updated: string; + collection_interval_in_millis: number; + process: { + memory: { + heap: { + total_in_bytes: number; + used_in_bytes: number; + size_limit: number; + }; + resident_set_size_in_bytes: number; + }; + event_loop_delay: number; + pid: number; + uptime_in_millis: number; + }; + os: { + load: Record; + memory: { + total_in_bytes: number; + used_in_bytes: number; + free_in_bytes: number; + }; + uptime_in_millis: number; + platform: string; + platformRelease: string; + }; + response_times: { + max_in_millis: number; + }; + requests: { + total: number; + disconnects: number; + statusCodes: Record; + status_codes: Record; + }; + concurrent_connections: number; + }; +} + +export const registerStatusRoute = ({ router, config, metrics, status }: Deps) => { + // Since this observable is not subscribed to elsewhere, we need to subscribe + // here to eagerly load the plugins status when Kibana starts up. + const plugins$ = new ReplaySubject>(); + status.plugins$.subscribe(plugins$); + + router.get( + { + path: '/api/status', + options: { + authRequired: !config.allowAnonymous, + tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page + }, + validate: { + query: schema.object({ + v8format: schema.boolean({ defaultValue: false }), + }), + }, + }, + async (context, req, res) => { + const { version, buildSha, buildNum } = config.packageInfo; + const versionWithoutSnapshot = version.replace(SNAPSHOT_POSTFIX, ''); + const [overall, core, plugins] = await combineLatest([ + status.overall$, + status.core$, + plugins$, + ]) + .pipe(first()) + .toPromise(); + + let statusInfo: StatusInfo | LegacyStatusInfo; + if (req.query?.v8format) { + statusInfo = { + overall: serializeStatus(overall), + core: serializeStatusRecord((core as unknown) as Record), + plugins: serializeStatusRecord(plugins), + }; + } else { + statusInfo = calculateLegacyStatus({ + overall, + core, + plugins, + versionWithoutSnapshot, + }); + } + + const lastMetrics = await metrics.getOpsMetrics$().pipe(first()).toPromise(); + + const body: StatusHttpBody = { + name: config.serverName, + uuid: config.uuid, + version: { + number: versionWithoutSnapshot, + build_hash: buildSha, + build_number: buildNum, + build_snapshot: SNAPSHOT_POSTFIX.test(version), + }, + status: statusInfo, + metrics: { + last_updated: lastMetrics.collected_at.toISOString(), + collection_interval_in_millis: metrics.collectionInterval, + os: lastMetrics.os, + process: lastMetrics.process, + response_times: lastMetrics.response_times, + concurrent_connections: lastMetrics.concurrent_connections, + requests: { + ...lastMetrics.requests, + status_codes: lastMetrics.requests.statusCodes, + }, + }, + }; + + return res.ok({ body }); + } + ); +}; + +const serializeStatus = (status: ServiceStatus): SerializableServiceStatus => ({ + ...status, + level: status.level.toString(), +}); + +const serializeStatusRecord = ( + statuses: Record +): Record => + Object.keys(statuses).reduce((acc, serviceName) => { + acc[serviceName] = serializeStatus(statuses[serviceName]); + return acc; + }, {} as Record); diff --git a/src/core/server/status/status_service.mock.ts b/src/core/server/status/status_service.mock.ts index 42b3eecdca310f2..ee66e947328dfba 100644 --- a/src/core/server/status/status_service.mock.ts +++ b/src/core/server/status/status_service.mock.ts @@ -43,6 +43,7 @@ const createSetupContractMock = () => { set: jest.fn(), dependencies$: new BehaviorSubject({}), derivedStatus$: new BehaviorSubject(available), + isStatusPageAnonymous: jest.fn().mockReturnValue(false), }; return setupContract; diff --git a/src/core/server/status/status_service.test.ts b/src/core/server/status/status_service.test.ts index dcb1e0a559f5ddc..afacaff044b6f03 100644 --- a/src/core/server/status/status_service.test.ts +++ b/src/core/server/status/status_service.test.ts @@ -24,6 +24,9 @@ import { StatusService } from './status_service'; import { first } from 'rxjs/operators'; import { mockCoreContext } from '../core_context.mock'; import { ServiceStatusLevelSnapshotSerializer } from './test_utils'; +import { environmentServiceMock } from '../environment/environment_service.mock'; +import { httpServiceMock } from '../http/http_service.mock'; +import { metricsServiceMock } from '../metrics/metrics_service.mock'; expect.addSnapshotSerializer(ServiceStatusLevelSnapshotSerializer); @@ -44,18 +47,36 @@ describe('StatusService', () => { summary: 'This is degraded!', }; + type SetupDeps = Parameters[0]; + const setupDeps = (overrides: Partial): SetupDeps => { + return { + elasticsearch: { + status$: of(available), + }, + savedObjects: { + status$: of(available), + }, + pluginDependencies: new Map(), + environment: environmentServiceMock.createSetupContract(), + http: httpServiceMock.createInternalSetupContract(), + metrics: metricsServiceMock.createInternalSetupContract(), + ...overrides, + }; + }; + describe('setup', () => { describe('core$', () => { it('rolls up core status observables into single observable', async () => { - const setup = await service.setup({ - elasticsearch: { - status$: of(available), - }, - savedObjects: { - status$: of(degraded), - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: of(available), + }, + savedObjects: { + status$: of(degraded), + }, + }) + ); expect(await setup.core$.pipe(first()).toPromise()).toEqual({ elasticsearch: available, savedObjects: degraded, @@ -63,15 +84,16 @@ describe('StatusService', () => { }); it('replays last event', async () => { - const setup = await service.setup({ - elasticsearch: { - status$: of(available), - }, - savedObjects: { - status$: of(degraded), - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: of(available), + }, + savedObjects: { + status$: of(degraded), + }, + }) + ); const subResult1 = await setup.core$.pipe(first()).toPromise(); const subResult2 = await setup.core$.pipe(first()).toPromise(); const subResult3 = await setup.core$.pipe(first()).toPromise(); @@ -92,15 +114,16 @@ describe('StatusService', () => { it('does not emit duplicate events', async () => { const elasticsearch$ = new BehaviorSubject(available); const savedObjects$ = new BehaviorSubject(degraded); - const setup = await service.setup({ - elasticsearch: { - status$: elasticsearch$, - }, - savedObjects: { - status$: savedObjects$, - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: elasticsearch$, + }, + savedObjects: { + status$: savedObjects$, + }, + }) + ); const statusUpdates: CoreStatus[] = []; const subscription = setup.core$.subscribe((status) => statusUpdates.push(status)); @@ -155,15 +178,16 @@ describe('StatusService', () => { describe('overall$', () => { it('exposes an overall summary', async () => { - const setup = await service.setup({ - elasticsearch: { - status$: of(degraded), - }, - savedObjects: { - status$: of(degraded), - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: of(degraded), + }, + savedObjects: { + status$: of(degraded), + }, + }) + ); expect(await setup.overall$.pipe(first()).toPromise()).toMatchObject({ level: ServiceStatusLevels.degraded, summary: '[2] services are degraded', @@ -171,15 +195,16 @@ describe('StatusService', () => { }); it('replays last event', async () => { - const setup = await service.setup({ - elasticsearch: { - status$: of(degraded), - }, - savedObjects: { - status$: of(degraded), - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: of(degraded), + }, + savedObjects: { + status$: of(degraded), + }, + }) + ); const subResult1 = await setup.overall$.pipe(first()).toPromise(); const subResult2 = await setup.overall$.pipe(first()).toPromise(); const subResult3 = await setup.overall$.pipe(first()).toPromise(); @@ -200,15 +225,16 @@ describe('StatusService', () => { it('does not emit duplicate events', async () => { const elasticsearch$ = new BehaviorSubject(available); const savedObjects$ = new BehaviorSubject(degraded); - const setup = await service.setup({ - elasticsearch: { - status$: elasticsearch$, - }, - savedObjects: { - status$: savedObjects$, - }, - pluginDependencies: new Map(), - }); + const setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: elasticsearch$, + }, + savedObjects: { + status$: savedObjects$, + }, + }) + ); const statusUpdates: ServiceStatus[] = []; const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); @@ -256,15 +282,16 @@ describe('StatusService', () => { 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 setup = await service.setup( + setupDeps({ + elasticsearch: { + status$: new BehaviorSubject(available), + }, + savedObjects: { + status$: savedObjects$, + }, + }) + ); const statusUpdates: ServiceStatus[] = []; const subscription = setup.overall$.subscribe((status) => statusUpdates.push(status)); diff --git a/src/core/server/status/status_service.ts b/src/core/server/status/status_service.ts index 8fe65eddb61d310..9acf93f2f81977a 100644 --- a/src/core/server/status/status_service.ts +++ b/src/core/server/status/status_service.ts @@ -17,7 +17,7 @@ * under the License. */ -import { Observable, combineLatest } from 'rxjs'; +import { Observable, combineLatest, Subscription } from 'rxjs'; import { map, distinctUntilChanged, shareReplay, take, debounceTime } from 'rxjs/operators'; import { isDeepStrictEqual } from 'util'; @@ -25,8 +25,12 @@ import { CoreService } from '../../types'; import { CoreContext } from '../core_context'; import { Logger } from '../logging'; import { InternalElasticsearchServiceSetup } from '../elasticsearch'; +import { InternalHttpServiceSetup } from '../http'; import { InternalSavedObjectsServiceSetup } from '../saved_objects'; import { PluginName } from '../plugins'; +import { InternalMetricsServiceSetup } from '../metrics'; +import { registerStatusRoute } from './routes'; +import { InternalEnvironmentServiceSetup } from '../environment'; import { config, StatusConfigType } from './status_config'; import { ServiceStatus, CoreStatus, InternalStatusServiceSetup } from './types'; @@ -35,7 +39,10 @@ import { PluginsStatusService } from './plugins_status'; interface SetupDeps { elasticsearch: Pick; + environment: InternalEnvironmentServiceSetup; pluginDependencies: ReadonlyMap; + http: InternalHttpServiceSetup; + metrics: InternalMetricsServiceSetup; savedObjects: Pick; } @@ -44,13 +51,21 @@ export class StatusService implements CoreService { private readonly config$: Observable; private pluginsStatus?: PluginsStatusService; + private overallSubscription?: Subscription; - constructor(coreContext: CoreContext) { + constructor(private readonly coreContext: CoreContext) { this.logger = coreContext.logger.get('status'); this.config$ = coreContext.configService.atPath(config.path); } - public async setup({ elasticsearch, pluginDependencies, savedObjects }: SetupDeps) { + public async setup({ + elasticsearch, + pluginDependencies, + http, + metrics, + savedObjects, + environment, + }: SetupDeps) { const statusConfig = await this.config$.pipe(take(1)).toPromise(); const core$ = this.setupCoreStatus({ elasticsearch, savedObjects }); this.pluginsStatus = new PluginsStatusService({ core$, pluginDependencies }); @@ -73,6 +88,26 @@ export class StatusService implements CoreService { shareReplay(1) ); + // Create an unused subscription to ensure all underlying lazy observables are started. + this.overallSubscription = overall$.subscribe(); + + const router = http.createRouter(''); + registerStatusRoute({ + router, + config: { + allowAnonymous: statusConfig.allowAnonymous, + packageInfo: this.coreContext.env.packageInfo, + serverName: http.getServerInfo().name, + uuid: environment.instanceUuid, + }, + metrics, + status: { + overall$, + plugins$: this.pluginsStatus.getAll$(), + core$, + }, + }); + return { core$, overall$, @@ -87,7 +122,12 @@ export class StatusService implements CoreService { public start() {} - public stop() {} + public stop() { + if (this.overallSubscription) { + this.overallSubscription.unsubscribe(); + this.overallSubscription = undefined; + } + } private setupCoreStatus({ elasticsearch, diff --git a/src/core/server/status/types.ts b/src/core/server/status/types.ts index f884b80316fa816..40a186bff6e84b3 100644 --- a/src/core/server/status/types.ts +++ b/src/core/server/status/types.ts @@ -217,11 +217,17 @@ export interface StatusServiceSetup { * through the dependency tree */ derivedStatus$: Observable; + + /** + * Whether or not the status HTTP APIs are available to unauthenticated users when an authentication provider is + * present. + */ + isStatusPageAnonymous: () => boolean; } /** @internal */ -export interface InternalStatusServiceSetup extends Pick { - isStatusPageAnonymous: () => boolean; +export interface InternalStatusServiceSetup + extends Pick { // Namespaced under `plugins` key to improve clarity that these are APIs for plugins specifically. plugins: { set(plugin: PluginName, status$: Observable): void; diff --git a/src/legacy/server/kbn_server.js b/src/legacy/server/kbn_server.js index a5eefd140c8fa25..6e5e7ddb62057eb 100644 --- a/src/legacy/server/kbn_server.js +++ b/src/legacy/server/kbn_server.js @@ -28,7 +28,6 @@ import httpMixin from './http'; import { coreMixin } from './core'; import { loggingMixin } from './logging'; import warningsMixin from './warnings'; -import { statusMixin } from './status'; import pidMixin from './pid'; import configCompleteMixin from './config/complete'; import { optimizeMixin } from '../../optimize'; @@ -91,7 +90,6 @@ export default class KbnServer { loggingMixin, warningsMixin, - statusMixin, // writes pid file pidMixin, diff --git a/src/legacy/server/plugins/lib/plugin.js b/src/legacy/server/plugins/lib/plugin.js index 2b392d13d595acc..48389061199ff1d 100644 --- a/src/legacy/server/plugins/lib/plugin.js +++ b/src/legacy/server/plugins/lib/plugin.js @@ -79,12 +79,7 @@ export class Plugin { ); } - // Many of the plugins are simply adding static assets to the server and we don't need - // to track their "status". Since plugins must have an init() function to even set its status - // we shouldn't even create a status unless the plugin can use it. if (this.externalInit) { - this.status = kbnServer.status.createForPlugin(this); - server.expose('status', this.status); await this.externalInit(server, options); } }; @@ -93,12 +88,6 @@ export class Plugin { plugin: { register, name: id, version }, options: config.has(configPrefix) ? config.get(configPrefix) : null, }); - - // Only change the plugin status to green if the - // initial status has not been changed - if (this.status && this.status.state === 'uninitialized') { - this.status.green('Ready'); - } } async postInit() { diff --git a/src/legacy/server/status/index.js b/src/legacy/server/status/index.js deleted file mode 100644 index ab7ec471a67ffa0..000000000000000 --- a/src/legacy/server/status/index.js +++ /dev/null @@ -1,61 +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 ServerStatus from './server_status'; -import { Metrics } from './lib/metrics'; -import { registerStatusApi, registerStatsApi } from './routes'; -import Oppsy from 'oppsy'; -import { cloneDeep } from 'lodash'; -import { getOSInfo } from './lib/get_os_info'; - -export function statusMixin(kbnServer, server, config) { - kbnServer.status = new ServerStatus(kbnServer.server); - const { usageCollection } = server.newPlatform.setup.plugins; - - const metrics = new Metrics(config, server); - - const oppsy = new Oppsy(server); - oppsy.on('ops', (event) => { - // Oppsy has a bad race condition that will modify this data before - // we ship it off to the buffer. Let's create our copy first. - event = cloneDeep(event); - // Oppsy used to provide this, but doesn't anymore. Grab it ourselves. - server.listener.getConnections((_, count) => { - event.concurrent_connections = count; - - // captures (performs transforms on) the latest event data and stashes - // the metrics for status/stats API payload - metrics.capture(event).then((data) => { - kbnServer.metrics = data; - }); - }); - }); - oppsy.start(config.get('ops.interval')); - - server.events.on('stop', () => { - oppsy.stop(); - }); - - // init routes - registerStatusApi(kbnServer, server, config); - registerStatsApi(usageCollection, server, config, kbnServer); - - // expore shared functionality - server.decorate('server', 'getOSInfo', getOSInfo); -} diff --git a/src/legacy/server/status/lib/__mocks__/_fs_stubs.js b/src/legacy/server/status/lib/__mocks__/_fs_stubs.js deleted file mode 100644 index 2be6402baa5feee..000000000000000 --- a/src/legacy/server/status/lib/__mocks__/_fs_stubs.js +++ /dev/null @@ -1,86 +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. - */ - -export function cGroups(hierarchy) { - if (!hierarchy) { - hierarchy = Math.random().toString(36).substring(7); - } - - const cpuAcctDir = `/sys/fs/cgroup/cpuacct/${hierarchy}`; - const cpuDir = `/sys/fs/cgroup/cpu/${hierarchy}`; - - const cGroupContents = [ - '10:freezer:/', - '9:net_cls,net_prio:/', - '8:pids:/', - '7:blkio:/', - '6:memory:/', - '5:devices:/user.slice', - '4:hugetlb:/', - '3:perf_event:/', - '2:cpu,cpuacct,cpuset:/' + hierarchy, - '1:name=systemd:/user.slice/user-1000.slice/session-2359.scope', - ].join('\n'); - - const cpuStatContents = ['nr_periods 0', 'nr_throttled 10', 'throttled_time 20'].join('\n'); - - return { - hierarchy, - cGroupContents, - cpuStatContents, - cpuAcctDir, - cpuDir, - files: { - '/proc/self/cgroup': cGroupContents, - [`${cpuAcctDir}/cpuacct.usage`]: '357753491408', - [`${cpuDir}/cpu.cfs_period_us`]: '100000', - [`${cpuDir}/cpu.cfs_quota_us`]: '5000', - [`${cpuDir}/cpu.stat`]: cpuStatContents, - }, - }; -} - -class FSError extends Error { - constructor(fileName, code) { - super('Stub File System Stub Error: ' + fileName); - this.code = code; - this.stack = null; - } -} - -let _mockFiles = Object.create({}); - -export const setMockFiles = (mockFiles) => { - _mockFiles = Object.create({}); - if (mockFiles) { - const files = Object.keys(mockFiles); - for (const file of files) { - _mockFiles[file] = mockFiles[file]; - } - } -}; - -export const readFileMock = (fileName, callback) => { - if (_mockFiles.hasOwnProperty(fileName)) { - callback(null, _mockFiles[fileName]); - } else { - const err = new FSError(fileName, 'ENOENT'); - callback(err, null); - } -}; diff --git a/src/legacy/server/status/lib/case_conversion.test.ts b/src/legacy/server/status/lib/case_conversion.test.ts deleted file mode 100644 index a231ee0ba4b0fde..000000000000000 --- a/src/legacy/server/status/lib/case_conversion.test.ts +++ /dev/null @@ -1,36 +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 { keysToSnakeCaseShallow } from './case_conversion'; - -describe('keysToSnakeCaseShallow', () => { - test("should convert all of an object's keys to snake case", () => { - const data = { - camelCase: 'camel_case', - 'kebab-case': 'kebab_case', - snake_case: 'snake_case', - }; - - const result = keysToSnakeCaseShallow(data); - - expect(result.camel_case).toBe('camel_case'); - expect(result.kebab_case).toBe('kebab_case'); - expect(result.snake_case).toBe('snake_case'); - }); -}); diff --git a/src/legacy/server/status/lib/case_conversion.ts b/src/legacy/server/status/lib/case_conversion.ts deleted file mode 100644 index a3ae15028daeb21..000000000000000 --- a/src/legacy/server/status/lib/case_conversion.ts +++ /dev/null @@ -1,24 +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 { mapKeys, snakeCase } from 'lodash'; - -export function keysToSnakeCaseShallow(object: Record) { - return mapKeys(object, (value, key) => snakeCase(key)); -} diff --git a/src/legacy/server/status/lib/cgroup.js b/src/legacy/server/status/lib/cgroup.js deleted file mode 100644 index 4d21cafbedcaaa3..000000000000000 --- a/src/legacy/server/status/lib/cgroup.js +++ /dev/null @@ -1,173 +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 fs from 'fs'; -import { promisify } from 'bluebird'; -import { join as joinPath } from 'path'; - -// Logic from elasticsearch/core/src/main/java/org/elasticsearch/monitor/os/OsProbe.java - -const CONTROL_GROUP_RE = new RegExp('\\d+:([^:]+):(/.*)'); -const CONTROLLER_SEPARATOR_RE = ','; - -const PROC_SELF_CGROUP_FILE = '/proc/self/cgroup'; -const PROC_CGROUP_CPU_DIR = '/sys/fs/cgroup/cpu'; -const PROC_CGROUP_CPUACCT_DIR = '/sys/fs/cgroup/cpuacct'; - -const GROUP_CPUACCT = 'cpuacct'; -const CPUACCT_USAGE_FILE = 'cpuacct.usage'; - -const GROUP_CPU = 'cpu'; -const CPU_FS_PERIOD_US_FILE = 'cpu.cfs_period_us'; -const CPU_FS_QUOTA_US_FILE = 'cpu.cfs_quota_us'; -const CPU_STATS_FILE = 'cpu.stat'; - -const readFile = promisify(fs.readFile); - -export function readControlGroups() { - return readFile(PROC_SELF_CGROUP_FILE).then((data) => { - const response = {}; - - data - .toString() - .split(/\n/) - .forEach((line) => { - const matches = line.match(CONTROL_GROUP_RE); - - if (matches === null) { - return; - } - - const controllers = matches[1].split(CONTROLLER_SEPARATOR_RE); - controllers.forEach((controller) => { - response[controller] = matches[2]; - }); - }); - - return response; - }); -} - -function fileContentsToInteger(path) { - return readFile(path).then((data) => { - return parseInt(data.toString(), 10); - }); -} - -function readCPUAcctUsage(controlGroup) { - return fileContentsToInteger(joinPath(PROC_CGROUP_CPUACCT_DIR, controlGroup, CPUACCT_USAGE_FILE)); -} - -function readCPUFsPeriod(controlGroup) { - return fileContentsToInteger(joinPath(PROC_CGROUP_CPU_DIR, controlGroup, CPU_FS_PERIOD_US_FILE)); -} - -function readCPUFsQuota(controlGroup) { - return fileContentsToInteger(joinPath(PROC_CGROUP_CPU_DIR, controlGroup, CPU_FS_QUOTA_US_FILE)); -} - -export function readCPUStat(controlGroup) { - return new Promise((resolve, reject) => { - const stat = { - number_of_elapsed_periods: -1, - number_of_times_throttled: -1, - time_throttled_nanos: -1, - }; - - readFile(joinPath(PROC_CGROUP_CPU_DIR, controlGroup, CPU_STATS_FILE)) - .then((data) => { - data - .toString() - .split(/\n/) - .forEach((line) => { - const fields = line.split(/\s+/); - - switch (fields[0]) { - case 'nr_periods': - stat.number_of_elapsed_periods = parseInt(fields[1], 10); - break; - - case 'nr_throttled': - stat.number_of_times_throttled = parseInt(fields[1], 10); - break; - - case 'throttled_time': - stat.time_throttled_nanos = parseInt(fields[1], 10); - break; - } - }); - - resolve(stat); - }) - .catch((err) => { - if (err.code === 'ENOENT') { - return resolve(stat); - } - - reject(err); - }); - }); -} - -export function getAllStats(options = {}) { - return new Promise((resolve, reject) => { - readControlGroups() - .then((groups) => { - const cpuPath = options.cpuPath || groups[GROUP_CPU]; - const cpuAcctPath = options.cpuAcctPath || groups[GROUP_CPUACCT]; - - // prevents undefined cgroup paths - if (!cpuPath || !cpuAcctPath) { - return resolve(null); - } - - return Promise.all([ - readCPUAcctUsage(cpuAcctPath), - readCPUFsPeriod(cpuPath), - readCPUFsQuota(cpuPath), - readCPUStat(cpuPath), - ]) - .then(([cpuAcctUsage, cpuFsPeriod, cpuFsQuota, cpuStat]) => { - resolve({ - cpuacct: { - control_group: cpuAcctPath, - usage_nanos: cpuAcctUsage, - }, - - cpu: { - control_group: cpuPath, - cfs_period_micros: cpuFsPeriod, - cfs_quota_micros: cpuFsQuota, - stat: cpuStat, - }, - }); - }) - .catch(rejectUnlessFileNotFound); - }) - .catch(rejectUnlessFileNotFound); - - function rejectUnlessFileNotFound(err) { - if (err.code === 'ENOENT') { - resolve(null); - } else { - reject(err); - } - } - }); -} diff --git a/src/legacy/server/status/lib/cgroup.test.js b/src/legacy/server/status/lib/cgroup.test.js deleted file mode 100644 index 62feba45d1b3c6e..000000000000000 --- a/src/legacy/server/status/lib/cgroup.test.js +++ /dev/null @@ -1,224 +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. - */ - -jest.mock('fs', () => ({ - readFile: jest.fn(), -})); - -import fs from 'fs'; -import { cGroups as cGroupsFsStub, setMockFiles, readFileMock } from './__mocks__/_fs_stubs'; -import { getAllStats, readControlGroups, readCPUStat } from './cgroup'; - -describe('Control Group', function () { - const fsStub = cGroupsFsStub(); - - beforeAll(() => { - fs.readFile.mockImplementation(readFileMock); - }); - - afterEach(() => { - setMockFiles(); - }); - - describe('readControlGroups', () => { - it('parses the file', async () => { - setMockFiles({ '/proc/self/cgroup': fsStub.cGroupContents }); - const cGroup = await readControlGroups(); - - expect(cGroup).toEqual({ - freezer: '/', - net_cls: '/', - net_prio: '/', - pids: '/', - blkio: '/', - memory: '/', - devices: '/user.slice', - hugetlb: '/', - perf_event: '/', - cpu: `/${fsStub.hierarchy}`, - cpuacct: `/${fsStub.hierarchy}`, - cpuset: `/${fsStub.hierarchy}`, - 'name=systemd': '/user.slice/user-1000.slice/session-2359.scope', - }); - }); - }); - - describe('readCPUStat', () => { - it('parses the file', async () => { - setMockFiles({ '/sys/fs/cgroup/cpu/fakeGroup/cpu.stat': fsStub.cpuStatContents }); - const cpuStat = await readCPUStat('fakeGroup'); - - expect(cpuStat).toEqual({ - number_of_elapsed_periods: 0, - number_of_times_throttled: 10, - time_throttled_nanos: 20, - }); - }); - - it('returns default stats for missing file', async () => { - setMockFiles(); - const cpuStat = await readCPUStat('fakeGroup'); - - expect(cpuStat).toEqual({ - number_of_elapsed_periods: -1, - number_of_times_throttled: -1, - time_throttled_nanos: -1, - }); - }); - }); - - describe('getAllStats', () => { - it('can override the cpu group path', async () => { - setMockFiles({ - '/proc/self/cgroup': fsStub.cGroupContents, - [`${fsStub.cpuAcctDir}/cpuacct.usage`]: '357753491408', - '/sys/fs/cgroup/cpu/docker/cpu.cfs_period_us': '100000', - '/sys/fs/cgroup/cpu/docker/cpu.cfs_quota_us': '5000', - '/sys/fs/cgroup/cpu/docker/cpu.stat': fsStub.cpuStatContents, - }); - - const stats = await getAllStats({ cpuPath: '/docker' }); - - expect(stats).toEqual({ - cpuacct: { - control_group: `/${fsStub.hierarchy}`, - usage_nanos: 357753491408, - }, - cpu: { - control_group: '/docker', - cfs_period_micros: 100000, - cfs_quota_micros: 5000, - stat: { - number_of_elapsed_periods: 0, - number_of_times_throttled: 10, - time_throttled_nanos: 20, - }, - }, - }); - }); - - it('handles an undefined control group', async () => { - setMockFiles({ - '/proc/self/cgroup': '', - [`${fsStub.cpuAcctDir}/cpuacct.usage`]: '357753491408', - [`${fsStub.cpuDir}/cpu.stat`]: fsStub.cpuStatContents, - [`${fsStub.cpuDir}/cpu.cfs_period_us`]: '100000', - [`${fsStub.cpuDir}/cpu.cfs_quota_us`]: '5000', - }); - - const stats = await getAllStats(); - - expect(stats).toBe(null); - }); - - it('can override the cpuacct group path', async () => { - setMockFiles({ - '/proc/self/cgroup': fsStub.cGroupContents, - '/sys/fs/cgroup/cpuacct/docker/cpuacct.usage': '357753491408', - [`${fsStub.cpuDir}/cpu.cfs_period_us`]: '100000', - [`${fsStub.cpuDir}/cpu.cfs_quota_us`]: '5000', - [`${fsStub.cpuDir}/cpu.stat`]: fsStub.cpuStatContents, - }); - - const stats = await getAllStats({ cpuAcctPath: '/docker' }); - - expect(stats).toEqual({ - cpuacct: { - control_group: '/docker', - usage_nanos: 357753491408, - }, - cpu: { - control_group: `/${fsStub.hierarchy}`, - cfs_period_micros: 100000, - cfs_quota_micros: 5000, - stat: { - number_of_elapsed_periods: 0, - number_of_times_throttled: 10, - time_throttled_nanos: 20, - }, - }, - }); - }); - - it('extracts control group stats', async () => { - setMockFiles(fsStub.files); - const stats = await getAllStats(); - - expect(stats).toEqual({ - cpuacct: { - control_group: `/${fsStub.hierarchy}`, - usage_nanos: 357753491408, - }, - cpu: { - control_group: `/${fsStub.hierarchy}`, - cfs_period_micros: 100000, - cfs_quota_micros: 5000, - stat: { - number_of_elapsed_periods: 0, - number_of_times_throttled: 10, - time_throttled_nanos: 20, - }, - }, - }); - }); - - it('returns null when all files are missing', async () => { - setMockFiles(); - const stats = await getAllStats(); - expect(stats).toBeNull(); - }); - - it('returns null if CPU accounting files are missing', async () => { - setMockFiles({ - '/proc/self/cgroup': fsStub.cGroupContents, - [`${fsStub.cpuDir}/cpu.stat`]: fsStub.cpuStatContents, - }); - const stats = await getAllStats(); - - expect(stats).toBeNull(); - }); - - it('returns -1 stat values if cpuStat file is missing', async () => { - setMockFiles({ - '/proc/self/cgroup': fsStub.cGroupContents, - [`${fsStub.cpuAcctDir}/cpuacct.usage`]: '357753491408', - [`${fsStub.cpuDir}/cpu.cfs_period_us`]: '100000', - [`${fsStub.cpuDir}/cpu.cfs_quota_us`]: '5000', - }); - const stats = await getAllStats(); - - expect(stats).toEqual({ - cpu: { - cfs_period_micros: 100000, - cfs_quota_micros: 5000, - control_group: `/${fsStub.hierarchy}`, - stat: { - number_of_elapsed_periods: -1, - number_of_times_throttled: -1, - time_throttled_nanos: -1, - }, - }, - cpuacct: { - control_group: `/${fsStub.hierarchy}`, - usage_nanos: 357753491408, - }, - }); - }); - }); -}); diff --git a/src/legacy/server/status/lib/get_kibana_info_for_stats.js b/src/legacy/server/status/lib/get_kibana_info_for_stats.js deleted file mode 100644 index 62628a2c40ff936..000000000000000 --- a/src/legacy/server/status/lib/get_kibana_info_for_stats.js +++ /dev/null @@ -1,47 +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 { get } from 'lodash'; - -const snapshotRegex = /-snapshot/i; - -/** - * This provides a meta data attribute along with Kibana stats. - * - * @param {Object} kbnServer manager of Kibana services - see `src/legacy/server/kbn_server` in Kibana core - * @param {Object} config Server config - * @param {String} host Kibana host - * @return {Object} The object containing a "kibana" field and source instance details. - */ -export function getKibanaInfoForStats(server, kbnServer) { - const config = server.config(); - const status = kbnServer.status.toJSON(); - - return { - uuid: config.get('server.uuid'), - name: config.get('server.name'), - index: config.get('kibana.index'), - host: config.get('server.host'), - locale: config.get('i18n.locale'), - transport_address: `${config.get('server.host')}:${config.get('server.port')}`, - version: kbnServer.version.replace(snapshotRegex, ''), - snapshot: snapshotRegex.test(kbnServer.version), - status: get(status, 'overall.state'), - }; -} diff --git a/src/legacy/server/status/lib/get_os_info.js b/src/legacy/server/status/lib/get_os_info.js deleted file mode 100644 index e3835fec34c88b2..000000000000000 --- a/src/legacy/server/status/lib/get_os_info.js +++ /dev/null @@ -1,48 +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 os from 'os'; -import getos from 'getos'; -import { promisify } from 'util'; - -/** - * Returns an object of OS information/ - */ -export async function getOSInfo() { - const osInfo = { - platform: os.platform(), - // Include the platform name in the release to avoid grouping unrelated platforms together. - // release 1.0 across windows, linux, and darwin don't mean anything useful. - platformRelease: `${os.platform()}-${os.release()}`, - }; - - // Get distribution information for linux - if (os.platform() === 'linux') { - try { - const distro = await promisify(getos)(); - osInfo.distro = distro.dist; - // Include distro name in release for same reason as above. - osInfo.distroRelease = `${distro.dist}-${distro.release}`; - } catch (e) { - // ignore errors - } - } - - return osInfo; -} diff --git a/src/legacy/server/status/lib/get_os_info.test.js b/src/legacy/server/status/lib/get_os_info.test.js deleted file mode 100644 index 11af7e158809091..000000000000000 --- a/src/legacy/server/status/lib/get_os_info.test.js +++ /dev/null @@ -1,68 +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. - */ - -jest.mock('os', () => ({ - platform: jest.fn(), - release: jest.fn(), -})); -jest.mock('getos'); - -import os from 'os'; -import getos from 'getos'; - -import { getOSInfo } from './get_os_info'; - -describe('getOSInfo', () => { - it('returns basic OS info on non-linux', async () => { - os.platform.mockImplementation(() => 'darwin'); - os.release.mockImplementation(() => '1.0.0'); - - const osInfo = await getOSInfo(); - - expect(osInfo).toEqual({ - platform: 'darwin', - platformRelease: 'darwin-1.0.0', - }); - }); - - it('returns basic OS info and distro info on linux', async () => { - os.platform.mockImplementation(() => 'linux'); - os.release.mockImplementation(() => '4.9.93-linuxkit-aufs'); - - // Mock getos response - getos.mockImplementation((cb) => - cb(null, { - os: 'linux', - dist: 'Ubuntu Linux', - codename: 'precise', - release: '12.04', - }) - ); - - const osInfo = await getOSInfo(); - - expect(osInfo).toEqual({ - platform: 'linux', - platformRelease: 'linux-4.9.93-linuxkit-aufs', - // linux distro info - distro: 'Ubuntu Linux', - distroRelease: 'Ubuntu Linux-12.04', - }); - }); -}); diff --git a/src/legacy/server/status/lib/metrics.js b/src/legacy/server/status/lib/metrics.js deleted file mode 100644 index 2631b245e72ab4a..000000000000000 --- a/src/legacy/server/status/lib/metrics.js +++ /dev/null @@ -1,146 +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 os from 'os'; -import v8 from 'v8'; -import { get, isObject, merge } from 'lodash'; -import { keysToSnakeCaseShallow } from './case_conversion'; -import { getAllStats as cGroupStats } from './cgroup'; -import { getOSInfo } from './get_os_info'; - -const requestDefaults = { - disconnects: 0, - statusCodes: {}, - total: 0, -}; - -export class Metrics { - constructor(config, server) { - this.config = config; - this.server = server; - this.checkCGroupStats = true; - } - - static getStubMetrics() { - return { - process: { - memory: { - heap: {}, - }, - }, - os: { - cpu: {}, - memory: {}, - }, - response_times: {}, - requests: {}, - }; - } - - async capture(hapiEvent) { - const timestamp = new Date().toISOString(); - const event = await this.captureEvent(hapiEvent); - const cgroup = await this.captureCGroupsIfAvailable(); - - const metrics = { - last_updated: timestamp, - collection_interval_in_millis: this.config.get('ops.interval'), - }; - - return merge(metrics, event, cgroup); - } - - async captureEvent(hapiEvent) { - const heapStats = v8.getHeapStatistics(); - const port = this.config.get('server.port'); - const avgInMillis = get(hapiEvent, ['responseTimes', port, 'avg']); // sadly, it's possible for this to be NaN - const maxInMillis = get(hapiEvent, ['responseTimes', port, 'max']); - - return { - process: { - memory: { - heap: { - // https://nodejs.org/docs/latest-v8.x/api/process.html#process_process_memoryusage - total_in_bytes: get(hapiEvent, 'psmem.heapTotal'), - used_in_bytes: get(hapiEvent, 'psmem.heapUsed'), - size_limit: heapStats.heap_size_limit, - }, - resident_set_size_in_bytes: get(hapiEvent, 'psmem.rss'), - }, - event_loop_delay: get(hapiEvent, 'psdelay'), - pid: process.pid, - uptime_in_millis: process.uptime() * 1000, - }, - os: { - load: { - '1m': get(hapiEvent, 'osload.0'), - '5m': get(hapiEvent, 'osload.1'), - '15m': get(hapiEvent, 'osload.2'), - }, - memory: { - total_in_bytes: os.totalmem(), - free_in_bytes: os.freemem(), - used_in_bytes: get(hapiEvent, 'osmem.total') - get(hapiEvent, 'osmem.free'), - }, - uptime_in_millis: os.uptime() * 1000, - ...(await getOSInfo()), - }, - response_times: { - avg_in_millis: isNaN(avgInMillis) ? undefined : avgInMillis, // convert NaN to undefined - max_in_millis: maxInMillis, - }, - requests: { - ...requestDefaults, - ...keysToSnakeCaseShallow(get(hapiEvent, ['requests', port])), - }, - concurrent_connections: hapiEvent.concurrent_connections, - }; - } - - async captureCGroups() { - try { - const cgroup = await cGroupStats({ - cpuPath: this.config.get('cpu.cgroup.path.override'), - cpuAcctPath: this.config.get('cpuacct.cgroup.path.override'), - }); - - if (isObject(cgroup)) { - return { - os: { - cgroup, - }, - }; - } - } catch (e) { - this.server.log(['error', 'metrics', 'cgroup'], e); - } - } - - async captureCGroupsIfAvailable() { - if (this.checkCGroupStats === true) { - const cgroup = await this.captureCGroups(); - - if (isObject(cgroup)) { - return cgroup; - } - - this.checkCGroupStats = false; - } - } -} diff --git a/src/legacy/server/status/lib/metrics.test.js b/src/legacy/server/status/lib/metrics.test.js deleted file mode 100644 index cc9c2607a2b595e..000000000000000 --- a/src/legacy/server/status/lib/metrics.test.js +++ /dev/null @@ -1,245 +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. - */ - -jest.mock('fs', () => ({ - readFile: jest.fn(), -})); - -jest.mock('os', () => ({ - freemem: jest.fn(), - totalmem: jest.fn(), - uptime: jest.fn(), - platform: jest.fn(), - release: jest.fn(), -})); - -jest.mock('process', () => ({ - uptime: jest.fn(), -})); - -import fs from 'fs'; -import os from 'os'; -import _ from 'lodash'; -import sinon from 'sinon'; -import { cGroups as cGroupsFsStub, setMockFiles, readFileMock } from './__mocks__/_fs_stubs'; -import { Metrics } from './metrics'; - -describe('Metrics', function () { - fs.readFile.mockImplementation(readFileMock); - - const sampleConfig = { - ops: { - interval: 5000, - }, - server: { - port: 5603, - }, - }; - const config = { get: (path) => _.get(sampleConfig, path) }; - - let metrics; - - beforeEach(() => { - const server = { log: sinon.mock() }; - - metrics = new Metrics(config, server); - }); - - afterEach(() => { - setMockFiles(); - }); - - describe('capture', () => { - it('merges all metrics', async () => { - setMockFiles(); - sinon - .stub(metrics, 'captureEvent') - .returns({ a: [{ b: 2 }, { d: 4 }], process: { uptime_ms: 1980 } }); - sinon.stub(metrics, 'captureCGroupsIfAvailable').returns({ a: [{ c: 3 }, { e: 5 }] }); - sinon.stub(Date.prototype, 'toISOString').returns('2017-04-14T18:35:41.534Z'); - - const capturedMetrics = await metrics.capture(); - expect(capturedMetrics).toMatchObject({ - last_updated: '2017-04-14T18:35:41.534Z', - collection_interval_in_millis: 5000, - a: [ - { b: 2, c: 3 }, - { d: 4, e: 5 }, - ], - process: { uptime_ms: 1980 }, - }); - }); - }); - - describe('captureEvent', () => { - it('parses the hapi event', async () => { - sinon.stub(os, 'uptime').returns(12000); - sinon.stub(process, 'uptime').returns(5000); - - os.freemem.mockImplementation(() => 12); - os.totalmem.mockImplementation(() => 24); - - const pidMock = jest.fn(); - pidMock.mockReturnValue(8675309); - Object.defineProperty(process, 'pid', { get: pidMock }); // - - const hapiEvent = { - requests: { 5603: { total: 22, disconnects: 0, statusCodes: { 200: 22 } } }, - responseTimes: { 5603: { avg: 1.8636363636363635, max: 4 } }, - osload: [2.20751953125, 2.02294921875, 1.89794921875], - osmem: { total: 17179869184, free: 102318080 }, - osup: 1008991, - psup: 7.168, - psmem: { rss: 193716224, heapTotal: 168194048, heapUsed: 130553400, external: 1779619 }, - concurrent_connections: 0, - psdelay: 1.6091690063476562, - host: 'blahblah.local', - }; - - expect(await metrics.captureEvent(hapiEvent)).toMatchObject({ - concurrent_connections: 0, - os: { - load: { - '15m': 1.89794921875, - '1m': 2.20751953125, - '5m': 2.02294921875, - }, - memory: { - free_in_bytes: 12, - total_in_bytes: 24, - }, - uptime_in_millis: 12000000, - }, - process: { - memory: { - heap: { - total_in_bytes: 168194048, - used_in_bytes: 130553400, - }, - resident_set_size_in_bytes: 193716224, - }, - pid: 8675309, - }, - requests: { - disconnects: 0, - total: 22, - }, - response_times: { - avg_in_millis: 1.8636363636363635, - max_in_millis: 4, - }, - }); - }); - - it('parses event with missing fields / NaN for responseTimes.avg', async () => { - const hapiEvent = { - requests: { - 5603: { total: 22, disconnects: 0, statusCodes: { 200: 22 } }, - }, - responseTimes: { 5603: { avg: NaN, max: 4 } }, - host: 'blahblah.local', - }; - - expect(await metrics.captureEvent(hapiEvent)).toMatchObject({ - process: { memory: { heap: {} }, pid: 8675309, uptime_in_millis: 5000000 }, - os: { - load: {}, - memory: { free_in_bytes: 12, total_in_bytes: 24 }, - }, - response_times: { max_in_millis: 4 }, - requests: { total: 22, disconnects: 0 }, - }); - }); - }); - - describe('captureCGroups', () => { - afterEach(() => { - setMockFiles(); - }); - - it('returns undefined if cgroups do not exist', async () => { - setMockFiles(); - - const stats = await metrics.captureCGroups(); - - expect(stats).toBe(undefined); - }); - - it('returns cgroups', async () => { - const fsStub = cGroupsFsStub(); - setMockFiles(fsStub.files); - - const capturedMetrics = await metrics.captureCGroups(); - - expect(capturedMetrics).toMatchObject({ - os: { - cgroup: { - cpuacct: { - control_group: `/${fsStub.hierarchy}`, - usage_nanos: 357753491408, - }, - cpu: { - control_group: `/${fsStub.hierarchy}`, - cfs_period_micros: 100000, - cfs_quota_micros: 5000, - stat: { - number_of_elapsed_periods: 0, - number_of_times_throttled: 10, - time_throttled_nanos: 20, - }, - }, - }, - }, - }); - }); - }); - - describe('captureCGroupsIfAvailable', () => { - afterEach(() => { - setMockFiles(); - }); - - it('marks cgroups as unavailable and prevents subsequent calls', async () => { - setMockFiles(); - sinon.spy(metrics, 'captureCGroups'); - - expect(metrics.checkCGroupStats).toBe(true); - - await metrics.captureCGroupsIfAvailable(); - expect(metrics.checkCGroupStats).toBe(false); - - await metrics.captureCGroupsIfAvailable(); - sinon.assert.calledOnce(metrics.captureCGroups); - }); - - it('allows subsequent calls if cgroups are available', async () => { - const fsStub = cGroupsFsStub(); - setMockFiles(fsStub.files); - sinon.spy(metrics, 'captureCGroups'); - - expect(metrics.checkCGroupStats).toBe(true); - - await metrics.captureCGroupsIfAvailable(); - expect(metrics.checkCGroupStats).toBe(true); - - await metrics.captureCGroupsIfAvailable(); - sinon.assert.calledTwice(metrics.captureCGroups); - }); - }); -}); diff --git a/src/legacy/server/status/routes/api/register_stats.js b/src/legacy/server/status/routes/api/register_stats.js deleted file mode 100644 index 2cd780d21f681b2..000000000000000 --- a/src/legacy/server/status/routes/api/register_stats.js +++ /dev/null @@ -1,164 +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 Joi from 'joi'; -import boom from 'boom'; -import { defaultsDeep } from 'lodash'; -import { i18n } from '@kbn/i18n'; -import { wrapAuthConfig } from '../../wrap_auth_config'; -import { getKibanaInfoForStats } from '../../lib'; - -const STATS_NOT_READY_MESSAGE = i18n.translate('server.stats.notReadyMessage', { - defaultMessage: 'Stats are not ready yet. Please try again later.', -}); - -/* - * API for Kibana meta info and accumulated operations stats - * Including ?extended in the query string fetches Elasticsearch cluster_uuid and usageCollection data - * - Requests to set isExtended = true - * GET /api/stats?extended=true - * GET /api/stats?extended - * - No value or 'false' is isExtended = false - * - Any other value causes a statusCode 400 response (Bad Request) - * Including ?exclude_usage in the query string excludes the usage stats from the response. Same value semantics as ?extended - */ -export function registerStatsApi(usageCollection, server, config, kbnServer) { - const wrapAuth = wrapAuthConfig(config.get('status.allowAnonymous')); - - const getClusterUuid = async (callCluster) => { - const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid' }); - return uuid; - }; - - const getUsage = async (callCluster) => { - const usage = await usageCollection.bulkFetchUsage(callCluster); - return usageCollection.toObject(usage); - }; - - let lastMetrics = null; - /* kibana_stats gets singled out from the collector set as it is used - * for health-checking Kibana and fetch does not rely on fetching data - * from ES */ - server.newPlatform.start.core.metrics.getOpsMetrics$().subscribe((metrics) => { - lastMetrics = { - ...metrics, - timestamp: new Date().toISOString(), - }; - }); - - server.route( - wrapAuth({ - method: 'GET', - path: '/api/stats', - config: { - validate: { - query: Joi.object({ - extended: Joi.string().valid('', 'true', 'false'), - legacy: Joi.string().valid('', 'true', 'false'), - exclude_usage: Joi.string().valid('', 'true', 'false'), - }), - }, - tags: ['api'], - }, - async handler(req) { - const isExtended = req.query.extended !== undefined && req.query.extended !== 'false'; - const isLegacy = req.query.legacy !== undefined && req.query.legacy !== 'false'; - const shouldGetUsage = - req.query.exclude_usage === undefined || req.query.exclude_usage === 'false'; - - let extended; - if (isExtended) { - const { callWithRequest } = req.server.plugins.elasticsearch.getCluster('admin'); - const callCluster = (...args) => callWithRequest(req, ...args); - const collectorsReady = await usageCollection.areAllCollectorsReady(); - - if (shouldGetUsage && !collectorsReady) { - return boom.serverUnavailable(STATS_NOT_READY_MESSAGE); - } - - const usagePromise = shouldGetUsage ? getUsage(callCluster) : Promise.resolve({}); - try { - const [usage, clusterUuid] = await Promise.all([ - usagePromise, - getClusterUuid(callCluster), - ]); - - let modifiedUsage = usage; - if (isLegacy) { - // In an effort to make telemetry more easily augmented, we need to ensure - // we can passthrough the data without every part of the process needing - // to know about the change; however, to support legacy use cases where this - // wasn't true, we need to be backwards compatible with how the legacy data - // looked and support those use cases here. - modifiedUsage = Object.keys(usage).reduce((accum, usageKey) => { - if (usageKey === 'kibana') { - accum = { - ...accum, - ...usage[usageKey], - }; - } else if (usageKey === 'reporting') { - accum = { - ...accum, - xpack: { - ...accum.xpack, - reporting: usage[usageKey], - }, - }; - } else { - // I don't think we need to it this for the above conditions, but do it for most as it will - // match the behavior done in monitoring/bulk_uploader - defaultsDeep(accum, { [usageKey]: usage[usageKey] }); - } - - return accum; - }, {}); - - extended = { - usage: modifiedUsage, - clusterUuid, - }; - } else { - extended = usageCollection.toApiFieldNames({ - usage: modifiedUsage, - clusterUuid, - }); - } - } catch (e) { - throw boom.boomify(e); - } - } - - if (!lastMetrics) { - return boom.serverUnavailable(STATS_NOT_READY_MESSAGE); - } - const kibanaStats = usageCollection.toApiFieldNames({ - ...lastMetrics, - kibana: getKibanaInfoForStats(server, kbnServer), - last_updated: new Date().toISOString(), - collection_interval_in_millis: config.get('ops.interval'), - }); - - return { - ...kibanaStats, - ...extended, - }; - }, - }) - ); -} diff --git a/src/legacy/server/status/routes/api/register_status.js b/src/legacy/server/status/routes/api/register_status.js deleted file mode 100644 index 259a00667810f88..000000000000000 --- a/src/legacy/server/status/routes/api/register_status.js +++ /dev/null @@ -1,50 +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 { wrapAuthConfig } from '../../wrap_auth_config'; - -const matchSnapshot = /-SNAPSHOT$/; - -export function registerStatusApi(kbnServer, server, config) { - const wrapAuth = wrapAuthConfig(config.get('status.allowAnonymous')); - - server.route( - wrapAuth({ - method: 'GET', - path: '/api/status', - config: { - tags: ['api'], - }, - async handler() { - return { - name: config.get('server.name'), - uuid: config.get('server.uuid'), - version: { - number: config.get('pkg.version').replace(matchSnapshot, ''), - build_hash: config.get('pkg.buildSha'), - build_number: config.get('pkg.buildNum'), - build_snapshot: matchSnapshot.test(config.get('pkg.version')), - }, - status: kbnServer.status.toJSON(), - metrics: kbnServer.metrics, - }; - }, - }) - ); -} diff --git a/src/legacy/server/status/routes/index.js b/src/legacy/server/status/routes/index.js deleted file mode 100644 index 12736a76d4915cb..000000000000000 --- a/src/legacy/server/status/routes/index.js +++ /dev/null @@ -1,21 +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. - */ - -export { registerStatusApi } from './api/register_status'; -export { registerStatsApi } from './api/register_stats'; diff --git a/src/legacy/server/status/samples.js b/src/legacy/server/status/samples.js deleted file mode 100644 index 9c41e29945a7738..000000000000000 --- a/src/legacy/server/status/samples.js +++ /dev/null @@ -1,45 +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 _ from 'lodash'; - -function Samples(max) { - this.vals = {}; - this.max = max || Infinity; - this.length = 0; -} - -Samples.prototype.add = function (sample) { - const vals = this.vals; - const length = (this.length = Math.min(this.length + 1, this.max)); - - _.forOwn(sample, function (val, name) { - if (val == null) val = null; - - if (!vals[name]) vals[name] = new Array(length); - vals[name].unshift([Date.now(), val]); - vals[name].length = length; - }); -}; - -Samples.prototype.toJSON = function () { - return this.vals; -}; - -export default Samples; diff --git a/src/legacy/server/status/server_status.js b/src/legacy/server/status/server_status.js deleted file mode 100644 index 81d07de55faaf49..000000000000000 --- a/src/legacy/server/status/server_status.js +++ /dev/null @@ -1,116 +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 _ from 'lodash'; - -import * as states from './states'; -import Status from './status'; -// eslint-disable-next-line @kbn/eslint/no-restricted-paths -import { pkg } from '../../../core/server/utils'; - -export default class ServerStatus { - constructor(server) { - this.server = server; - this._created = {}; - } - - create(id) { - const status = new Status(id, this.server); - this._created[status.id] = status; - return status; - } - - createForPlugin(plugin) { - if (plugin.version === 'kibana') plugin.version = pkg.version; - const status = this.create(`plugin:${plugin.id}@${plugin.version}`); - status.plugin = plugin; - return status; - } - - each(fn) { - const self = this; - _.forOwn(self._created, function (status, i, list) { - if (status.state !== 'disabled') { - fn.call(self, status, i, list); - } - }); - } - - get(id) { - return this._created[id]; - } - - getForPluginId(pluginId) { - return _.find(this._created, (s) => s.plugin && s.plugin.id === pluginId); - } - - getState(id) { - const status = this.get(id); - if (!status) return undefined; - return status.state || 'uninitialized'; - } - - getStateForPluginId(pluginId) { - const status = this.getForPluginId(pluginId); - if (!status) return undefined; - return status.state || 'uninitialized'; - } - - overall() { - const state = Object - // take all created status objects - .values(this._created) - // get the state descriptor for each status - .map((status) => states.get(status.state)) - // reduce to the state with the highest severity, defaulting to green - .reduce((a, b) => (a.severity > b.severity ? a : b), states.get('green')); - - const statuses = _.filter(this._created, { state: state.id }); - const since = _.get(_.sortBy(statuses, 'since'), [0, 'since']); - - return { - state: state.id, - title: state.title, - nickname: _.sample(state.nicknames), - icon: state.icon, - uiColor: states.get(state.id).uiColor, - since: since, - }; - } - - isGreen() { - return this.overall().state === 'green'; - } - - notGreen() { - return !this.isGreen(); - } - - toString() { - const overall = this.overall(); - return `${overall.title} – ${overall.nickname}`; - } - - toJSON() { - return { - overall: this.overall(), - statuses: _.values(this._created), - }; - } -} diff --git a/src/legacy/server/status/server_status.test.js b/src/legacy/server/status/server_status.test.js deleted file mode 100644 index bf94d693b131027..000000000000000 --- a/src/legacy/server/status/server_status.test.js +++ /dev/null @@ -1,145 +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 { find } from 'lodash'; -import sinon from 'sinon'; - -import * as states from './states'; -import Status from './status'; -import ServerStatus from './server_status'; - -describe('ServerStatus class', function () { - const plugin = { id: 'name', version: '1.2.3' }; - - let server; - let serverStatus; - - beforeEach(function () { - server = { expose: sinon.stub(), logWithMetadata: sinon.stub() }; - serverStatus = new ServerStatus(server); - }); - - describe('#create(id)', () => { - it('should create a new plugin with an id', () => { - const status = serverStatus.create('someid'); - expect(status).toBeInstanceOf(Status); - }); - }); - - describe('#createForPlugin(plugin)', function () { - it('should create a new status by plugin', function () { - const status = serverStatus.createForPlugin(plugin); - expect(status).toBeInstanceOf(Status); - }); - }); - - describe('#get(id)', () => { - it('exposes statuses by their id', () => { - const status = serverStatus.create('statusid'); - expect(serverStatus.get('statusid')).toBe(status); - }); - - it('does not get the status for a plugin', () => { - serverStatus.createForPlugin(plugin); - expect(serverStatus.get(plugin)).toBe(undefined); - }); - }); - - describe('#getForPluginId(plugin)', function () { - it('exposes plugin status for the plugin', function () { - const status = serverStatus.createForPlugin(plugin); - expect(serverStatus.getForPluginId(plugin.id)).toBe(status); - }); - - it('does not get plain statuses by their id', function () { - serverStatus.create('someid'); - expect(serverStatus.getForPluginId('someid')).toBe(undefined); - }); - }); - - describe('#getState(id)', function () { - it('should expose the state of a status by id', function () { - const status = serverStatus.create('someid'); - status.green(); - expect(serverStatus.getState('someid')).toBe('green'); - }); - }); - - describe('#getStateForPluginId(plugin)', function () { - it('should expose the state of a plugin by id', function () { - const status = serverStatus.createForPlugin(plugin); - status.green(); - expect(serverStatus.getStateForPluginId(plugin.id)).toBe('green'); - }); - }); - - describe('#overall()', function () { - it('considers each status to produce a summary', function () { - const status = serverStatus.createForPlugin(plugin); - - expect(serverStatus.overall().state).toBe('uninitialized'); - - const match = function (overall, state) { - expect(overall).toHaveProperty('state', state.id); - expect(overall).toHaveProperty('title', state.title); - expect(overall).toHaveProperty('icon', state.icon); - expect(overall).toHaveProperty('uiColor', state.uiColor); - expect(state.nicknames).toContain(overall.nickname); - }; - - status.green(); - match(serverStatus.overall(), states.get('green')); - - status.yellow(); - match(serverStatus.overall(), states.get('yellow')); - - status.red(); - match(serverStatus.overall(), states.get('red')); - }); - }); - - describe('#toJSON()', function () { - it('serializes to overall status and individuals', function () { - const pluginOne = { id: 'one', version: '1.0.0' }; - const pluginTwo = { id: 'two', version: '2.0.0' }; - const pluginThree = { id: 'three', version: 'kibana' }; - - const service = serverStatus.create('some service'); - const p1 = serverStatus.createForPlugin(pluginOne); - const p2 = serverStatus.createForPlugin(pluginTwo); - const p3 = serverStatus.createForPlugin(pluginThree); - - service.green(); - p1.yellow(); - p2.red(); - - const json = JSON.parse(JSON.stringify(serverStatus)); - expect(json).toHaveProperty('overall'); - expect(json.overall.state).toEqual(serverStatus.overall().state); - expect(json.statuses).toHaveLength(4); - - const out = (status) => find(json.statuses, { id: status.id }); - expect(out(service)).toHaveProperty('state', 'green'); - expect(out(p1)).toHaveProperty('state', 'yellow'); - expect(out(p2)).toHaveProperty('state', 'red'); - expect(out(p3)).toHaveProperty('id'); - expect(out(p3).id).not.toContain('undefined'); - }); - }); -}); diff --git a/src/legacy/server/status/states.js b/src/legacy/server/status/states.js deleted file mode 100644 index 4a34684571c3cdb..000000000000000 --- a/src/legacy/server/status/states.js +++ /dev/null @@ -1,85 +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 _ from 'lodash'; -import { i18n } from '@kbn/i18n'; - -export const getAll = () => [ - { - id: 'red', - title: i18n.translate('server.status.redTitle', { - defaultMessage: 'Red', - }), - icon: 'danger', - uiColor: 'danger', - severity: 1000, - nicknames: ['Danger Will Robinson! Danger!'], - }, - { - id: 'uninitialized', - title: i18n.translate('server.status.uninitializedTitle', { - defaultMessage: 'Uninitialized', - }), - icon: 'spinner', - uiColor: 'default', - severity: 900, - nicknames: ['Initializing'], - }, - { - id: 'yellow', - title: i18n.translate('server.status.yellowTitle', { - defaultMessage: 'Yellow', - }), - icon: 'warning', - uiColor: 'warning', - severity: 800, - nicknames: ['S.N.A.F.U', "I'll be back", 'brb'], - }, - { - id: 'green', - title: i18n.translate('server.status.greenTitle', { - defaultMessage: 'Green', - }), - icon: 'success', - uiColor: 'secondary', - severity: 0, - nicknames: ['Looking good'], - }, - { - id: 'disabled', - title: i18n.translate('server.status.disabledTitle', { - defaultMessage: 'Disabled', - }), - severity: -1, - icon: 'toggle-off', - uiColor: 'default', - nicknames: ['Am I even a thing?'], - }, -]; - -export const getAllById = () => _.keyBy(exports.getAll(), 'id'); - -export const defaults = { - icon: 'question', - severity: Infinity, -}; - -export function get(id) { - return exports.getAllById()[id] || _.defaults({ id: id }, exports.defaults); -} diff --git a/src/legacy/server/status/status.js b/src/legacy/server/status/status.js deleted file mode 100644 index 10e94da3ac352ca..000000000000000 --- a/src/legacy/server/status/status.js +++ /dev/null @@ -1,108 +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 * as states from './states'; -import { EventEmitter } from 'events'; - -export default class Status extends EventEmitter { - constructor(id, server) { - super(); - - if (!id || typeof id !== 'string') { - throw new TypeError('Status constructor requires an `id` string'); - } - - this.id = id; - this.since = new Date(); - this.state = 'uninitialized'; - this.message = 'uninitialized'; - - this.on('change', function (previous, previousMsg) { - this.since = new Date(); - - const tags = ['status', this.id, this.state === 'red' ? 'error' : 'info']; - - server.logWithMetadata( - tags, - `Status changed from ${previous} to ${this.state}${ - this.message ? ' - ' + this.message : '' - }`, - { - state: this.state, - message: this.message, - prevState: previous, - prevMsg: previousMsg, - } - ); - }); - } - - toJSON() { - return { - id: this.id, - state: this.state, - icon: states.get(this.state).icon, - message: this.message, - uiColor: states.get(this.state).uiColor, - since: this.since, - }; - } - - on(eventName, handler) { - super.on(eventName, handler); - - if (eventName === this.state) { - setImmediate(() => handler(this.state, this.message)); - } - } - - once(eventName, handler) { - if (eventName === this.state) { - setImmediate(() => handler(this.state, this.message)); - } else { - super.once(eventName, handler); - } - } -} - -states.getAll().forEach(function (state) { - Status.prototype[state.id] = function (message) { - if (this.state === 'disabled') return; - - const previous = this.state; - const previousMsg = this.message; - - this.error = null; - this.message = message || state.title; - this.state = state.id; - - if (message instanceof Error) { - this.error = message; - this.message = message.message; - } - - if (previous === this.state && previousMsg === this.message) { - // noop - return; - } - - this.emit(state.id, previous, previousMsg, this.state, this.message); - this.emit('change', previous, previousMsg, this.state, this.message); - }; -}); diff --git a/src/legacy/server/status/status.test.js b/src/legacy/server/status/status.test.js deleted file mode 100644 index def7b5a2182e185..000000000000000 --- a/src/legacy/server/status/status.test.js +++ /dev/null @@ -1,147 +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 sinon from 'sinon'; -import ServerStatus from './server_status'; - -describe('Status class', function () { - const plugin = { id: 'test', version: '1.2.3' }; - - let server; - let serverStatus; - - beforeEach(function () { - server = { expose: sinon.stub(), logWithMetadata: sinon.stub() }; - serverStatus = new ServerStatus(server); - }); - - it('should have an "uninitialized" state initially', () => { - expect(serverStatus.createForPlugin(plugin)).toHaveProperty('state', 'uninitialized'); - }); - - it('emits change when the status is set', function (done) { - const status = serverStatus.createForPlugin(plugin); - - status.once('change', function (prevState, prevMsg, newState, newMsg) { - expect(newState).toBe('green'); - expect(newMsg).toBe('GREEN'); - expect(prevState).toBe('uninitialized'); - - status.once('change', function (prevState, prevMsg, newState, newMsg) { - expect(newState).toBe('red'); - expect(newMsg).toBe('RED'); - expect(prevState).toBe('green'); - expect(prevMsg).toBe('GREEN'); - - done(); - }); - - status.red('RED'); - }); - - status.green('GREEN'); - }); - - it('should only trigger the change listener when something changes', function () { - const status = serverStatus.createForPlugin(plugin); - const stub = sinon.stub(); - status.on('change', stub); - status.green('Ready'); - status.green('Ready'); - status.red('Not Ready'); - sinon.assert.calledTwice(stub); - }); - - it('should create a JSON representation of the status', function () { - const status = serverStatus.createForPlugin(plugin); - status.green('Ready'); - - const json = status.toJSON(); - expect(json.id).toEqual(status.id); - expect(json.state).toEqual('green'); - expect(json.message).toEqual('Ready'); - }); - - it('should call on handler if status is already matched', function (done) { - const status = serverStatus.createForPlugin(plugin); - const msg = 'Test Ready'; - status.green(msg); - - status.on('green', function (prev, prevMsg) { - expect(arguments.length).toBe(2); - expect(prev).toBe('green'); - expect(prevMsg).toBe(msg); - expect(status.message).toBe(msg); - done(); - }); - }); - - it('should call once handler if status is already matched', function (done) { - const status = serverStatus.createForPlugin(plugin); - const msg = 'Test Ready'; - status.green(msg); - - status.once('green', function (prev, prevMsg) { - expect(arguments.length).toBe(2); - expect(prev).toBe('green'); - expect(prevMsg).toBe(msg); - expect(status.message).toBe(msg); - done(); - }); - }); - - function testState(color) { - it(`should change the state to ${color} when #${color}() is called`, function () { - const status = serverStatus.createForPlugin(plugin); - const message = 'testing ' + color; - status[color](message); - expect(status).toHaveProperty('state', color); - expect(status).toHaveProperty('message', message); - }); - - it(`should trigger the "change" listener when #${color}() is called`, function (done) { - const status = serverStatus.createForPlugin(plugin); - const message = 'testing ' + color; - status.on('change', function (prev, prevMsg) { - expect(status.state).toBe(color); - expect(status.message).toBe(message); - - expect(prev).toBe('uninitialized'); - expect(prevMsg).toBe('uninitialized'); - done(); - }); - status[color](message); - }); - - it(`should trigger the "${color}" listener when #${color}() is called`, function (done) { - const status = serverStatus.createForPlugin(plugin); - const message = 'testing ' + color; - status.on(color, function () { - expect(status.state).toBe(color); - expect(status.message).toBe(message); - done(); - }); - status[color](message); - }); - } - - testState('green'); - testState('yellow'); - testState('red'); -}); diff --git a/src/legacy/server/status/wrap_auth_config.js b/src/legacy/server/status/wrap_auth_config.js deleted file mode 100644 index 04e71a02d30de66..000000000000000 --- a/src/legacy/server/status/wrap_auth_config.js +++ /dev/null @@ -1,27 +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 { assign, identity } from 'lodash'; - -export const wrapAuthConfig = (allowAnonymous) => { - if (allowAnonymous) { - return (options) => assign(options, { config: { auth: false } }); - } - return identity; -}; diff --git a/src/legacy/server/status/wrap_auth_config.test.js b/src/legacy/server/status/wrap_auth_config.test.js deleted file mode 100644 index fa0230a96a5873e..000000000000000 --- a/src/legacy/server/status/wrap_auth_config.test.js +++ /dev/null @@ -1,60 +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 { wrapAuthConfig } from './wrap_auth_config'; - -describe('Status wrapAuthConfig', () => { - let options; - - beforeEach(() => { - options = { - method: 'GET', - path: '/status', - handler: function (request, h) { - return h.response(); - }, - }; - }); - - it('should return a function', () => { - expect(typeof wrapAuthConfig()).toBe('function'); - expect(typeof wrapAuthConfig(true)).toBe('function'); - expect(typeof wrapAuthConfig(false)).toBe('function'); - }); - - it('should not add auth config by default', () => { - const wrapAuth = wrapAuthConfig(); - const wrapped = wrapAuth(options); - expect(wrapped).not.toHaveProperty('config'); - }); - - it('should not add auth config if allowAnonymous is false', () => { - const wrapAuth = wrapAuthConfig(false); - const wrapped = wrapAuth(options); - expect(wrapped).not.toHaveProperty('config'); - }); - - it('should add auth config if allowAnonymous is true', () => { - const wrapAuth = wrapAuthConfig(true); - const wrapped = wrapAuth(options); - expect(wrapped).toHaveProperty('config'); - expect(wrapped.config).toHaveProperty('auth'); - expect(wrapped.config.auth).toBe(false); - }); -}); diff --git a/src/plugins/usage_collection/server/plugin.ts b/src/plugins/usage_collection/server/plugin.ts index 00584e1fd5d86ac..74e70d5ea9d3548 100644 --- a/src/plugins/usage_collection/server/plugin.ts +++ b/src/plugins/usage_collection/server/plugin.ts @@ -49,8 +49,25 @@ export class UsageCollectionPlugin implements Plugin { maximumWaitTimeForAllCollectorsInS: config.maximumWaitTimeForAllCollectorsInS, }); + const globalConfig = await this.initializerContext.config.legacy.globalConfig$ + .pipe(first()) + .toPromise(); + const router = core.http.createRouter(); - setupRoutes(router, () => this.savedObjects); + setupRoutes({ + router, + getSavedObjects: () => this.savedObjects, + collectorSet, + config: { + allowAnonymous: core.status.isStatusPageAnonymous(), + kibanaIndex: globalConfig.kibana.index, + kibanaVersion: this.initializerContext.env.packageInfo.version, + server: core.http.getServerInfo(), + uuid: this.initializerContext.env.instanceUuid, + }, + metrics: core.metrics, + overallStatus$: core.status.overall$, + }); return collectorSet; } diff --git a/src/plugins/usage_collection/server/routes/index.ts b/src/plugins/usage_collection/server/routes/index.ts index e6beef3fbdc59bf..b367ddc184be78f 100644 --- a/src/plugins/usage_collection/server/routes/index.ts +++ b/src/plugins/usage_collection/server/routes/index.ts @@ -17,12 +17,39 @@ * under the License. */ -import { IRouter, ISavedObjectsRepository } from 'kibana/server'; +import { + IRouter, + ISavedObjectsRepository, + MetricsServiceSetup, + ServiceStatus, +} from 'kibana/server'; +import { Observable } from 'rxjs'; +import { CollectorSet } from '../collector'; import { registerUiMetricRoute } from './report_metrics'; +import { registerStatsRoute } from './stats'; -export function setupRoutes( - router: IRouter, - getSavedObjects: () => ISavedObjectsRepository | undefined -) { +export function setupRoutes({ + router, + getSavedObjects, + ...rest +}: { + router: IRouter; + getSavedObjects: () => ISavedObjectsRepository | undefined; + config: { + allowAnonymous: boolean; + kibanaIndex: string; + kibanaVersion: string; + uuid: string; + server: { + name: string; + hostname: string; + port: number; + }; + }; + collectorSet: CollectorSet; + metrics: MetricsServiceSetup; + overallStatus$: Observable; +}) { registerUiMetricRoute(router, getSavedObjects); + registerStatsRoute({ router, ...rest }); } diff --git a/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts b/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts new file mode 100644 index 000000000000000..2b39eb626e4196a --- /dev/null +++ b/src/plugins/usage_collection/server/routes/integration_tests/stats.test.ts @@ -0,0 +1,104 @@ +/* + * 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 } from 'rxjs'; +import { UnwrapPromise } from '@kbn/utility-types'; + +import { + MetricsServiceSetup, + ServiceStatus, + ServiceStatusLevels, +} from '../../../../../core/server'; +import { + contextServiceMock, + loggingSystemMock, + metricsServiceMock, +} from '../../../../../core/server/mocks'; +import { createHttpServer } from '../../../../../core/server/test_utils'; +import { registerStatsRoute } from '../stats'; +import supertest from 'supertest'; +import { CollectorSet } from '../../collector'; + +type HttpService = ReturnType; +type HttpSetup = UnwrapPromise>; + +describe('/api/stats', () => { + let server: HttpService; + let httpSetup: HttpSetup; + let overallStatus$: BehaviorSubject; + let metrics: MetricsServiceSetup; + + beforeEach(async () => { + server = createHttpServer(); + httpSetup = await server.setup({ + context: contextServiceMock.createSetupContract(), + }); + overallStatus$ = new BehaviorSubject({ + level: ServiceStatusLevels.available, + summary: 'everything is working', + }); + metrics = metricsServiceMock.createSetupContract(); + + const router = httpSetup.createRouter(''); + registerStatsRoute({ + router, + collectorSet: new CollectorSet({ + logger: loggingSystemMock.create().asLoggerFactory().get(), + }), + config: { + allowAnonymous: true, + kibanaIndex: '.kibana-test', + kibanaVersion: '8.8.8-SNAPSHOT', + server: { + name: 'mykibana', + hostname: 'mykibana.com', + port: 1234, + }, + uuid: 'xxx-xxxxx', + }, + metrics, + overallStatus$, + }); + + await server.start(); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('successfully returns data', async () => { + const response = await supertest(httpSetup.server.listener).get('/api/stats').expect(200); + expect(response.body).toMatchObject({ + kibana: { + uuid: 'xxx-xxxxx', + name: 'mykibana', + index: '.kibana-test', + host: 'mykibana.com', + locale: 'en', + transport_address: `mykibana.com:1234`, + version: '8.8.8', + snapshot: true, + status: 'green', + }, + last_updated: expect.any(String), + collection_interval_ms: expect.any(Number), + }); + }); +}); diff --git a/src/plugins/usage_collection/server/routes/stats.ts b/src/plugins/usage_collection/server/routes/stats.ts new file mode 100644 index 000000000000000..107c6f3c15e4649 --- /dev/null +++ b/src/plugins/usage_collection/server/routes/stats.ts @@ -0,0 +1,200 @@ +/* + * 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 { schema } from '@kbn/config-schema'; +import { i18n } from '@kbn/i18n'; +import defaultsDeep from 'lodash/defaultsDeep'; +import { Observable } from 'rxjs'; +import { first } from 'rxjs/operators'; + +import { + IRouter, + LegacyAPICaller, + MetricsServiceSetup, + ServiceStatus, + ServiceStatusLevels, +} from '../../../../core/server'; +import { CollectorSet } from '../collector'; + +const STATS_NOT_READY_MESSAGE = i18n.translate('usageCollection.stats.notReadyMessage', { + defaultMessage: 'Stats are not ready yet. Please try again later.', +}); + +const SNAPSHOT_REGEX = /-snapshot/i; + +export function registerStatsRoute({ + router, + config, + collectorSet, + metrics, + overallStatus$, +}: { + router: IRouter; + config: { + allowAnonymous: boolean; + kibanaIndex: string; + kibanaVersion: string; + uuid: string; + server: { + name: string; + hostname: string; + port: number; + }; + }; + collectorSet: CollectorSet; + metrics: MetricsServiceSetup; + overallStatus$: Observable; +}) { + const getUsage = async (callCluster: LegacyAPICaller): Promise => { + const usage = await collectorSet.bulkFetchUsage(callCluster); + return collectorSet.toObject(usage); + }; + + const getClusterUuid = async (callCluster: LegacyAPICaller): Promise => { + const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid' }); + return uuid; + }; + + router.get( + { + path: '/api/stats', + options: { + // Even when allowAnonymous is true, we attempt to authenticate in order to deny anonymous users access to + // extended stats. + authRequired: config.allowAnonymous ? 'optional' : true, + tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page + }, + validate: { + query: schema.object({ + extended: schema.maybe( + schema.oneOf([schema.literal(''), schema.literal('true'), schema.literal('false')]) + ), + legacy: schema.maybe( + schema.oneOf([schema.literal(''), schema.literal('true'), schema.literal('false')]) + ), + exclude_usage: schema.maybe( + schema.oneOf([schema.literal(''), schema.literal('true'), schema.literal('false')]) + ), + }), + }, + }, + async (context, req, res) => { + const isExtended = req.query.extended === '' || req.query.extended === 'true'; + const isLegacy = req.query.legacy === '' || req.query.legacy === 'true'; + const shouldGetUsage = + req.query.exclude_usage === undefined || req.query.exclude_usage === 'false'; + + let extended; + if (isExtended) { + // Unauthenticated users should never have access to these stats. + if (!req.auth.isAuthenticated) { + return res.unauthorized({ + body: 'Unauthenticated for extended stats', + }); + } + + const callCluster = context.core.elasticsearch.legacy.client.callAsCurrentUser; + const collectorsReady = await collectorSet.areAllCollectorsReady(); + + if (shouldGetUsage && !collectorsReady) { + return res.customError({ statusCode: 503, body: { message: STATS_NOT_READY_MESSAGE } }); + } + + const usagePromise = shouldGetUsage ? getUsage(callCluster) : Promise.resolve({}); + const [usage, clusterUuid] = await Promise.all([usagePromise, getClusterUuid(callCluster)]); + + let modifiedUsage = usage; + if (isLegacy) { + // In an effort to make telemetry more easily augmented, we need to ensure + // we can passthrough the data without every part of the process needing + // to know about the change; however, to support legacy use cases where this + // wasn't true, we need to be backwards compatible with how the legacy data + // looked and support those use cases here. + modifiedUsage = Object.keys(usage).reduce((accum, usageKey) => { + if (usageKey === 'kibana') { + accum = { + ...accum, + ...usage[usageKey], + }; + } else if (usageKey === 'reporting') { + accum = { + ...accum, + xpack: { + ...accum.xpack, + reporting: usage[usageKey], + }, + }; + } else { + // I don't think we need to it this for the above conditions, but do it for most as it will + // match the behavior done in monitoring/bulk_uploader + defaultsDeep(accum, { [usageKey]: usage[usageKey] }); + } + + return accum; + }, {} as any); + + extended = { + usage: modifiedUsage, + clusterUuid, + }; + } else { + extended = collectorSet.toApiFieldNames({ + usage: modifiedUsage, + clusterUuid, + }); + } + } + + // Guranteed to resolve immediately due to replay effect on getOpsMetrics$ + const lastMetrics = await metrics.getOpsMetrics$().pipe(first()).toPromise(); + + const overallStatus = await overallStatus$.pipe(first()).toPromise(); + const kibanaStats = collectorSet.toApiFieldNames({ + ...lastMetrics, + kibana: { + uuid: config.uuid, + name: config.server.name, + index: config.kibanaIndex, + host: config.server.hostname, + locale: i18n.getLocale(), + transport_address: `${config.server.hostname}:${config.server.port}`, + version: config.kibanaVersion.replace(SNAPSHOT_REGEX, ''), + snapshot: SNAPSHOT_REGEX.test(config.kibanaVersion), + status: ServiceStatusToLegacyState[overallStatus.level.toString()], + }, + last_updated: lastMetrics.collected_at.toISOString(), + collection_interval_in_millis: metrics.collectionInterval, + }); + + return res.ok({ + body: { + ...kibanaStats, + ...extended, + }, + }); + } + ); +} + +const ServiceStatusToLegacyState: Record = { + [ServiceStatusLevels.critical.toString()]: 'red', + [ServiceStatusLevels.unavailable.toString()]: 'red', + [ServiceStatusLevels.degraded.toString()]: 'yellow', + [ServiceStatusLevels.available.toString()]: 'green', +}; diff --git a/x-pack/legacy/plugins/xpack_main/index.js b/x-pack/legacy/plugins/xpack_main/index.js index 854fba6624719f2..52b94d060dd8ce1 100644 --- a/x-pack/legacy/plugins/xpack_main/index.js +++ b/x-pack/legacy/plugins/xpack_main/index.js @@ -5,7 +5,6 @@ */ import { resolve } from 'path'; -import { mirrorPluginStatus } from '../../server/lib/mirror_plugin_status'; import { setupXPackMain } from './server/lib/setup_xpack_main'; import { xpackInfoRoute, settingsRoute } from './server/routes/api/v1'; @@ -23,8 +22,6 @@ export const xpackMain = (kibana) => { }, init(server) { - mirrorPluginStatus(server.plugins.elasticsearch, this, 'yellow', 'red'); - setupXPackMain(server); // register routes diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js index c34e27642d2ceb4..f49f44bed97a781 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/__tests__/setup_xpack_main.js @@ -13,33 +13,35 @@ describe('setupXPackMain()', () => { const sandbox = sinon.createSandbox(); let mockServer; + let mockStatusObservable; let mockElasticsearchPlugin; - let mockXPackMainPlugin; beforeEach(() => { sandbox.useFakeTimers(); mockElasticsearchPlugin = { getCluster: sinon.stub(), - status: sinon.stub({ - on() {}, - }), }; - mockXPackMainPlugin = { - status: sinon.stub({ - green() {}, - red() {}, - }), - }; + mockStatusObservable = sinon.stub({ subscribe() {} }); mockServer = sinon.stub({ plugins: { elasticsearch: mockElasticsearchPlugin, - xpack_main: mockXPackMainPlugin, }, newPlatform: { - setup: { plugins: { features: {}, licensing: { license$: new BehaviorSubject() } } }, + setup: { + core: { + status: { + core$: { + pipe() { + return mockStatusObservable; + }, + }, + }, + }, + plugins: { features: {}, licensing: { license$: new BehaviorSubject() } }, + }, }, events: { on() {} }, log() {}, @@ -61,55 +63,6 @@ describe('setupXPackMain()', () => { setupXPackMain(mockServer); sinon.assert.calledWithExactly(mockServer.expose, 'info', sinon.match.instanceOf(XPackInfo)); - sinon.assert.calledWithExactly(mockElasticsearchPlugin.status.on, 'change', sinon.match.func); - }); - - describe('Elasticsearch plugin state changes cause XPackMain plugin state change.', () => { - let xPackInfo; - let onElasticsearchPluginStatusChange; - beforeEach(() => { - setupXPackMain(mockServer); - - onElasticsearchPluginStatusChange = mockElasticsearchPlugin.status.on.withArgs('change') - .firstCall.args[1]; - xPackInfo = mockServer.expose.firstCall.args[1]; - }); - - it('if `XPackInfo` is available status will become `green`.', async () => { - sinon.stub(xPackInfo, 'isAvailable').returns(false); - // We need this to make sure the code waits for `refreshNow` to complete before it tries - // to access its properties. - sinon.stub(xPackInfo, 'refreshNow').callsFake(() => { - return new Promise((resolve) => { - xPackInfo.isAvailable.returns(true); - resolve(); - }); - }); - - await onElasticsearchPluginStatusChange(); - - sinon.assert.calledWithExactly(mockXPackMainPlugin.status.green, 'Ready'); - sinon.assert.notCalled(mockXPackMainPlugin.status.red); - }); - - it('if `XPackInfo` is not available status will become `red`.', async () => { - sinon.stub(xPackInfo, 'isAvailable').returns(true); - sinon.stub(xPackInfo, 'unavailableReason').returns(''); - - // We need this to make sure the code waits for `refreshNow` to complete before it tries - // to access its properties. - sinon.stub(xPackInfo, 'refreshNow').callsFake(() => { - return new Promise((resolve) => { - xPackInfo.isAvailable.returns(false); - xPackInfo.unavailableReason.returns('Some weird error.'); - resolve(); - }); - }); - - await onElasticsearchPluginStatusChange(); - - sinon.assert.calledWithExactly(mockXPackMainPlugin.status.red, 'Some weird error.'); - sinon.assert.notCalled(mockXPackMainPlugin.status.green); - }); + sinon.assert.calledWithExactly(mockStatusObservable.subscribe, sinon.match.func); }); }); diff --git a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js index 33b551bbe864ff6..fd4e3c86d0ca7f6 100644 --- a/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js +++ b/x-pack/legacy/plugins/xpack_main/server/lib/setup_xpack_main.js @@ -4,6 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ +import { pairwise } from 'rxjs/operators'; import { XPackInfo } from './xpack_info'; /** @@ -19,23 +20,14 @@ export function setupXPackMain(server) { server.expose('info', info); - const setPluginStatus = () => { - if (info.isAvailable()) { - server.plugins.xpack_main.status.green('Ready'); - } else { - server.plugins.xpack_main.status.red(info.unavailableReason()); - } - }; - // trigger an xpack info refresh whenever the elasticsearch plugin status changes - server.plugins.elasticsearch.status.on('change', async () => { - await info.refreshNow(); - setPluginStatus(); - }); - - // whenever the license info is updated, regardless of the elasticsearch plugin status - // changes, reflect the change in our plugin status. See https://github.com/elastic/kibana/issues/20017 - info.onLicenseInfoChange(setPluginStatus); + server.newPlatform.setup.core.status.core$ + .pipe(pairwise()) + .subscribe(async ([coreLast, coreCurrent]) => { + if (coreLast.elasticsearch.level !== coreCurrent.elasticsearch.level) { + await info.refreshNow(); + } + }); return info; } diff --git a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js index 34fc4d97c13283b..3001f8ce301198f 100644 --- a/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js +++ b/x-pack/legacy/plugins/xpack_main/server/routes/api/v1/settings.js @@ -5,9 +5,17 @@ */ import { boomify } from 'boom'; -import { get } from 'lodash'; +import { first } from 'rxjs/operators'; +import { ServiceStatusLevels } from '../../../../../../../../src/core/server'; import { KIBANA_SETTINGS_TYPE } from '../../../../../../../plugins/monitoring/common/constants'; +const ServiceStatusToLegacyState = { + [ServiceStatusLevels.critical.toString()]: 'red', + [ServiceStatusLevels.unavailable.toString()]: 'red', + [ServiceStatusLevels.degraded.toString()]: 'yellow', + [ServiceStatusLevels.available.toString()]: 'green', +}; + const getClusterUuid = async (callCluster) => { const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid' }); return uuid; @@ -23,6 +31,7 @@ export function settingsRoute(server, kbnServer) { const callCluster = (...args) => callWithRequest(req, ...args); // All queries from HTTP API must use authentication headers from the request try { + const { status: coreStatus } = server.newPlatform.setup.core; const { usageCollection } = server.newPlatform.setup.plugins; const settingsCollector = usageCollection.getCollectorByType(KIBANA_SETTINGS_TYPE); @@ -34,7 +43,7 @@ export function settingsRoute(server, kbnServer) { const snapshotRegex = /-snapshot/i; const config = server.config(); - const status = kbnServer.status.toJSON(); + const status = await coreStatus.overall$.pipe(first()).toPromise(); const kibana = { uuid: config.get('server.uuid'), name: config.get('server.name'), @@ -45,7 +54,7 @@ export function settingsRoute(server, kbnServer) { transport_address: `${config.get('server.host')}:${config.get('server.port')}`, version: kbnServer.version.replace(snapshotRegex, ''), snapshot: snapshotRegex.test(kbnServer.version), - status: get(status, 'overall.state'), + status: ServiceStatusToLegacyState[status.level.toString()], }; return { diff --git a/x-pack/legacy/server/lib/__tests__/mirror_plugin_status.js b/x-pack/legacy/server/lib/__tests__/mirror_plugin_status.js deleted file mode 100644 index c7cae0785c9ebe6..000000000000000 --- a/x-pack/legacy/server/lib/__tests__/mirror_plugin_status.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -import EventEmitter from 'events'; -import expect from '@kbn/expect'; -import { mirrorPluginStatus } from '../mirror_plugin_status'; - -describe('mirror_plugin_status', () => { - class MockPluginStatus extends EventEmitter { - constructor() { - super(); - this.state = 'uninitialized'; - } - - _changeState(newState, newMessage) { - if (this.state === newState) { - return; - } - const prevState = this.state; - const prevMessage = this.message; - - this.state = newState; - this.message = newMessage; - - this.emit(newState, prevState, prevMessage, this.state, this.message); - this.emit('change', prevState, prevMessage, this.state, this.message); - } - - red(message) { - this._changeState('red', message); - } - yellow(message) { - this._changeState('yellow', message); - } - green(message) { - this._changeState('green', message); - } - uninitialized(message) { - this._changeState('uninitialized', message); - } - } - - class MockPlugin { - constructor() { - this.status = new MockPluginStatus(); - } - } - - let upstreamPlugin; - let downstreamPlugin; - let eventNotEmittedTimeout; - - beforeEach(() => { - upstreamPlugin = new MockPlugin(); - downstreamPlugin = new MockPlugin(); - eventNotEmittedTimeout = setTimeout(() => { - throw new Error('Event should have been emitted'); - }, 100); - }); - - it('should mirror all downstream plugin statuses to upstream plugin statuses', (done) => { - mirrorPluginStatus(upstreamPlugin, downstreamPlugin); - downstreamPlugin.status.on('change', () => { - clearTimeout(eventNotEmittedTimeout); - expect(downstreamPlugin.status.state).to.be('red'); - expect(downstreamPlugin.status.message).to.be('test message'); - done(); - }); - upstreamPlugin.status.red('test message'); - }); - - describe('should only mirror specific downstream plugin statuses to corresponding upstream plugin statuses: ', () => { - beforeEach(() => { - mirrorPluginStatus(upstreamPlugin, downstreamPlugin, 'yellow', 'red'); - }); - - it('yellow', (done) => { - downstreamPlugin.status.on('change', () => { - clearTimeout(eventNotEmittedTimeout); - expect(downstreamPlugin.status.state).to.be('yellow'); - expect(downstreamPlugin.status.message).to.be('test yellow message'); - done(); - }); - upstreamPlugin.status.yellow('test yellow message'); - }); - - it('red', (done) => { - downstreamPlugin.status.on('change', () => { - clearTimeout(eventNotEmittedTimeout); - expect(downstreamPlugin.status.state).to.be('red'); - expect(downstreamPlugin.status.message).to.be('test red message'); - done(); - }); - upstreamPlugin.status.red('test red message'); - }); - - it('not green', () => { - clearTimeout(eventNotEmittedTimeout); // because event should not be emitted in this test - downstreamPlugin.status.on('change', () => { - throw new Error('Event should NOT have been emitted'); - }); - upstreamPlugin.status.green('test green message'); - }); - }); -}); diff --git a/x-pack/legacy/server/lib/mirror_plugin_status.js b/x-pack/legacy/server/lib/mirror_plugin_status.js deleted file mode 100644 index 7b1ac215f5e4c89..000000000000000 --- a/x-pack/legacy/server/lib/mirror_plugin_status.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License; - * you may not use this file except in compliance with the Elastic License. - */ - -export function mirrorPluginStatus(upstreamPlugin, downstreamPlugin, ...statesToMirror) { - upstreamPlugin.status.setMaxListeners(21); // We need more than the default, which is 10 - - function mirror(previousState, previousMsg, newState, newMsg) { - if (newState) { - downstreamPlugin.status[newState](newMsg); - } - } - - if (statesToMirror.length === 0) { - statesToMirror.push('change'); - } - - statesToMirror.map((state) => upstreamPlugin.status.on(state, mirror)); - mirror(null, null, upstreamPlugin.status.state, upstreamPlugin.status.message); // initial mirroring -} diff --git a/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js b/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js index eabe375eefd02d8..ce6cc87cd4ebfa4 100644 --- a/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js +++ b/x-pack/legacy/server/lib/register_license_checker/register_license_checker.js @@ -4,21 +4,27 @@ * you may not use this file except in compliance with the Elastic License. */ -import { mirrorPluginStatus } from '../mirror_plugin_status'; +import { pairwise } from 'rxjs/operators'; + +import { ServiceStatusLevels } from '../../../../../src/core/server'; import { checkLicense } from '../check_license'; export function registerLicenseChecker(server, pluginId, pluginName, minimumLicenseRequired) { const xpackMainPlugin = server.plugins.xpack_main; - const thisPlugin = server.plugins[pluginId]; - - mirrorPluginStatus(xpackMainPlugin, thisPlugin); - xpackMainPlugin.status.once('green', () => { - // Register a function that is called whenever the xpack info changes, - // to re-compute the license check results for this plugin - xpackMainPlugin.info - .feature(pluginId) - .registerLicenseCheckResultsGenerator((xpackLicenseInfo) => { - return checkLicense(pluginName, minimumLicenseRequired, xpackLicenseInfo); - }); - }); + server.newPlatform.setup.core.status.core$ + .pipe(pairwise()) + .subscribe(([coreLast, coreCurrent]) => { + if ( + coreLast.elasticsearch.level !== ServiceStatusLevels.available && + coreCurrent.elasticsearch.level === ServiceStatusLevels.available + ) { + // Register a function that is called whenever the xpack info changes, + // to re-compute the license check results for this plugin + xpackMainPlugin.info + .feature(pluginId) + .registerLicenseCheckResultsGenerator((xpackLicenseInfo) => { + return checkLicense(pluginName, minimumLicenseRequired, xpackLicenseInfo); + }); + } + }); } diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 54c92d323fcff2f..7b161343fc13c98 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -2933,12 +2933,10 @@ "savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "このオブジェクトに関連付けられた保存された検索は現在存在しません。", "savedObjectsManagement.view.viewItemButtonLabel": "{title}を表示", "savedObjectsManagement.view.viewItemTitle": "{title}を表示", - "server.stats.notReadyMessage": "まだ統計が準備できていません。後程再試行してください", - "server.status.disabledTitle": "無効", - "server.status.greenTitle": "緑", - "server.status.redTitle": "赤", - "server.status.uninitializedTitle": "アンインストールしました", - "server.status.yellowTitle": "黄色", + "usageCollection.stats.notReadyMessage": "まだ統計が準備できていません。後程再試行してください", + "core.status.greenTitle": "緑", + "core.status.redTitle": "赤", + "core.status.yellowTitle": "黄色", "share.advancedSettings.csv.quoteValuesText": "csvエクスポートに値を引用するかどうかです", "share.advancedSettings.csv.quoteValuesTitle": "CSVの値を引用", "share.advancedSettings.csv.separatorText": "エクスポートされた値をこの文字列で区切ります", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index df721cb624662a2..bfca1caa6225661 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -2934,12 +2934,10 @@ "savedObjectsManagement.view.savedSearchDoesNotExistErrorMessage": "与此对象关联的已保存搜索已不存在。", "savedObjectsManagement.view.viewItemButtonLabel": "查看“{title}”", "savedObjectsManagement.view.viewItemTitle": "查看“{title}”", - "server.stats.notReadyMessage": "统计尚未就绪。请稍后重试", - "server.status.disabledTitle": "已禁用", - "server.status.greenTitle": "绿", - "server.status.redTitle": "红", - "server.status.uninitializedTitle": "未初始化", - "server.status.yellowTitle": "黄", + "usageCollection.stats.notReadyMessage": "统计尚未就绪。请稍后重试", + "core.status.greenTitle": "绿", + "core.status.redTitle": "红", + "core.status.yellowTitle": "黄", "share.advancedSettings.csv.quoteValuesText": "在 CSV 导出中是否应使用引号引起值?", "share.advancedSettings.csv.quoteValuesTitle": "使用引号引起 CSV 值", "share.advancedSettings.csv.separatorText": "使用此字符串分隔导出的值",