mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
* Complete migration of legacy status API * PR comments * Remove optional auth on stats endpoint * Regen docs * PR comments * PR nits * Set ReplaySubject buffer to 1 * Fix exclude_usage logic Co-authored-by: pgayvallet <pierre.gayvallet@elastic.co> Co-authored-by: Josh Dover <me@joshdover.com>
This commit is contained in:
parent
475ba3e636
commit
a676670218
65 changed files with 1414 additions and 2463 deletions
|
@ -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"
|
||||
|
|
|
@ -23,6 +23,7 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
|
|||
| [getStartServices](./kibana-plugin-core-server.coresetup.getstartservices.md) | <code>StartServicesAccessor<TPluginsStart, TStart></code> | [StartServicesAccessor](./kibana-plugin-core-server.startservicesaccessor.md) |
|
||||
| [http](./kibana-plugin-core-server.coresetup.http.md) | <code>HttpServiceSetup & {</code><br/><code> resources: HttpResources;</code><br/><code> }</code> | [HttpServiceSetup](./kibana-plugin-core-server.httpservicesetup.md) |
|
||||
| [logging](./kibana-plugin-core-server.coresetup.logging.md) | <code>LoggingServiceSetup</code> | [LoggingServiceSetup](./kibana-plugin-core-server.loggingservicesetup.md) |
|
||||
| [metrics](./kibana-plugin-core-server.coresetup.metrics.md) | <code>MetricsServiceSetup</code> | [MetricsServiceSetup](./kibana-plugin-core-server.metricsservicesetup.md) |
|
||||
| [savedObjects](./kibana-plugin-core-server.coresetup.savedobjects.md) | <code>SavedObjectsServiceSetup</code> | [SavedObjectsServiceSetup](./kibana-plugin-core-server.savedobjectsservicesetup.md) |
|
||||
| [status](./kibana-plugin-core-server.coresetup.status.md) | <code>StatusServiceSetup</code> | [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) |
|
||||
| [uiSettings](./kibana-plugin-core-server.coresetup.uisettings.md) | <code>UiSettingsServiceSetup</code> | [UiSettingsServiceSetup](./kibana-plugin-core-server.uisettingsservicesetup.md) |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
metrics: MetricsServiceSetup;
|
||||
```
|
|
@ -20,7 +20,7 @@ export interface CoreStart
|
|||
| [capabilities](./kibana-plugin-core-server.corestart.capabilities.md) | <code>CapabilitiesStart</code> | [CapabilitiesStart](./kibana-plugin-core-server.capabilitiesstart.md) |
|
||||
| [elasticsearch](./kibana-plugin-core-server.corestart.elasticsearch.md) | <code>ElasticsearchServiceStart</code> | [ElasticsearchServiceStart](./kibana-plugin-core-server.elasticsearchservicestart.md) |
|
||||
| [http](./kibana-plugin-core-server.corestart.http.md) | <code>HttpServiceStart</code> | [HttpServiceStart](./kibana-plugin-core-server.httpservicestart.md) |
|
||||
| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | <code>MetricsServiceStart</code> | |
|
||||
| [metrics](./kibana-plugin-core-server.corestart.metrics.md) | <code>MetricsServiceStart</code> | [MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md) |
|
||||
| [savedObjects](./kibana-plugin-core-server.corestart.savedobjects.md) | <code>SavedObjectsServiceStart</code> | [SavedObjectsServiceStart](./kibana-plugin-core-server.savedobjectsservicestart.md) |
|
||||
| [uiSettings](./kibana-plugin-core-server.corestart.uisettings.md) | <code>UiSettingsServiceStart</code> | [UiSettingsServiceStart](./kibana-plugin-core-server.uisettingsservicestart.md) |
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
## CoreStart.metrics property
|
||||
|
||||
[MetricsServiceStart](./kibana-plugin-core-server.metricsservicestart.md)
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
|
|
|
@ -253,6 +253,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 |
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[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.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
export declare type MetricsServiceStart = MetricsServiceSetup;
|
||||
```
|
|
@ -13,18 +13,22 @@ ServiceStatusLevels: Readonly<{
|
|||
available: Readonly<{
|
||||
toString: () => "available";
|
||||
valueOf: () => 0;
|
||||
toJSON: () => "available";
|
||||
}>;
|
||||
degraded: Readonly<{
|
||||
toString: () => "degraded";
|
||||
valueOf: () => 1;
|
||||
toJSON: () => "degraded";
|
||||
}>;
|
||||
unavailable: Readonly<{
|
||||
toString: () => "unavailable";
|
||||
valueOf: () => 2;
|
||||
toJSON: () => "unavailable";
|
||||
}>;
|
||||
critical: Readonly<{
|
||||
toString: () => "critical";
|
||||
valueOf: () => 3;
|
||||
toJSON: () => "critical";
|
||||
}>;
|
||||
}>
|
||||
```
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<!-- Do not edit this file. It is automatically generated by API Documenter. -->
|
||||
|
||||
[Home](./index.md) > [kibana-plugin-core-server](./kibana-plugin-core-server.md) > [StatusServiceSetup](./kibana-plugin-core-server.statusservicesetup.md) > [isStatusPageAnonymous](./kibana-plugin-core-server.statusservicesetup.isstatuspageanonymous.md)
|
||||
|
||||
## StatusServiceSetup.isStatusPageAnonymous property
|
||||
|
||||
Whether or not the status HTTP APIs are available to unauthenticated users when an authentication provider is present.
|
||||
|
||||
<b>Signature:</b>
|
||||
|
||||
```typescript
|
||||
isStatusPageAnonymous: () => boolean;
|
||||
```
|
|
@ -74,6 +74,7 @@ core.status.set(
|
|||
| [core$](./kibana-plugin-core-server.statusservicesetup.core_.md) | <code>Observable<CoreStatus></code> | Current status for all Core services. |
|
||||
| [dependencies$](./kibana-plugin-core-server.statusservicesetup.dependencies_.md) | <code>Observable<Record<string, ServiceStatus>></code> | Current status for all plugins this plugin depends on. Each key of the <code>Record</code> is a plugin id. |
|
||||
| [derivedStatus$](./kibana-plugin-core-server.statusservicesetup.derivedstatus_.md) | <code>Observable<ServiceStatus></code> | The status of this plugin as derived from its dependencies. |
|
||||
| [isStatusPageAnonymous](./kibana-plugin-core-server.statusservicesetup.isstatuspageanonymous.md) | <code>() => boolean</code> | Whether or not the status HTTP APIs are available to unauthenticated users when an authentication provider is present. |
|
||||
| [overall$](./kibana-plugin-core-server.statusservicesetup.overall_.md) | <code>Observable<ServiceStatus></code> | Overall system status for all of Kibana. |
|
||||
|
||||
## Methods
|
||||
|
|
|
@ -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.',
|
||||
|
|
|
@ -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';
|
||||
|
@ -320,6 +320,7 @@ export {
|
|||
OpsServerMetrics,
|
||||
OpsProcessMetrics,
|
||||
MetricsServiceSetup,
|
||||
MetricsServiceStart,
|
||||
} from './metrics';
|
||||
|
||||
export { DEFAULT_APP_CATEGORIES } from '../utils';
|
||||
|
@ -414,6 +415,8 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
|
|||
};
|
||||
/** {@link LoggingServiceSetup} */
|
||||
logging: LoggingServiceSetup;
|
||||
/** {@link MetricsServiceSetup} */
|
||||
metrics: MetricsServiceSetup;
|
||||
/** {@link SavedObjectsServiceSetup} */
|
||||
savedObjects: SavedObjectsServiceSetup;
|
||||
/** {@link StatusServiceSetup} */
|
||||
|
|
|
@ -33,7 +33,7 @@ import {
|
|||
} from './saved_objects';
|
||||
import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings';
|
||||
import { InternalEnvironmentServiceSetup } from './environment';
|
||||
import { InternalMetricsServiceStart } from './metrics';
|
||||
import { InternalMetricsServiceSetup, InternalMetricsServiceStart } from './metrics';
|
||||
import { InternalRenderingServiceSetup } from './rendering';
|
||||
import { InternalHttpResourcesSetup } from './http_resources';
|
||||
import { InternalStatusServiceSetup } from './status';
|
||||
|
@ -54,6 +54,7 @@ export interface InternalCoreSetup {
|
|||
httpResources: InternalHttpResourcesSetup;
|
||||
auditTrail: AuditTrailSetup;
|
||||
logging: InternalLoggingServiceSetup;
|
||||
metrics: InternalMetricsServiceSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -51,6 +51,7 @@ import { coreMock } from '../mocks';
|
|||
import { statusServiceMock } from '../status/status_service.mock';
|
||||
import { auditTrailServiceMock } from '../audit_trail/audit_trail_service.mock';
|
||||
import { loggingServiceMock } from '../logging/logging_service.mock';
|
||||
import { metricsServiceMock } from '../metrics/metrics_service.mock';
|
||||
|
||||
const MockKbnServer: jest.Mock<KbnServer> = 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: {
|
||||
|
|
|
@ -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: () => {
|
||||
|
|
|
@ -78,8 +78,8 @@ type MetricsServiceContract = PublicMethodsOf<MetricsService>;
|
|||
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<MetricsServiceContract> = {
|
||||
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;
|
||||
|
|
|
@ -52,6 +52,7 @@ 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<T>(config: T) {
|
||||
|
@ -138,6 +139,7 @@ function createCoreSetupMock({
|
|||
uiSettings: uiSettingsMock,
|
||||
auditTrail: auditTrailServiceMock.createSetupContract(),
|
||||
logging: loggingServiceMock.createSetupContract(),
|
||||
metrics: metricsServiceMock.createSetupContract(),
|
||||
getStartServices: jest
|
||||
.fn<Promise<[ReturnType<typeof createCoreStartMock>, object, any]>, []>()
|
||||
.mockResolvedValue([createCoreStartMock(), pluginStartDeps, pluginStartContract]),
|
||||
|
@ -174,6 +176,7 @@ function createInternalCoreSetupMock() {
|
|||
uiSettings: uiSettingsServiceMock.createSetupContract(),
|
||||
auditTrail: auditTrailServiceMock.createSetupContract(),
|
||||
logging: loggingServiceMock.createInternalSetupContract(),
|
||||
metrics: metricsServiceMock.createInternalSetupContract(),
|
||||
};
|
||||
return setupDeps;
|
||||
}
|
||||
|
@ -183,7 +186,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(),
|
||||
|
|
|
@ -179,6 +179,10 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
|
|||
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,
|
||||
|
@ -191,6 +195,7 @@ export function createPluginSetupContext<TPlugin, TPluginDependencies>(
|
|||
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,
|
||||
|
|
|
@ -416,6 +416,8 @@ export interface CoreSetup<TPluginsStart extends object = object, TStart = unkno
|
|||
// (undocumented)
|
||||
logging: LoggingServiceSetup;
|
||||
// (undocumented)
|
||||
metrics: MetricsServiceSetup;
|
||||
// (undocumented)
|
||||
savedObjects: SavedObjectsServiceSetup;
|
||||
// (undocumented)
|
||||
status: StatusServiceSetup;
|
||||
|
@ -433,9 +435,6 @@ export interface CoreStart {
|
|||
elasticsearch: ElasticsearchServiceStart;
|
||||
// (undocumented)
|
||||
http: HttpServiceStart;
|
||||
// Warning: (ae-forgotten-export) The symbol "MetricsServiceStart" needs to be exported by the entry point index.d.ts
|
||||
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "MetricsServiceStart"
|
||||
//
|
||||
// (undocumented)
|
||||
metrics: MetricsServiceStart;
|
||||
// (undocumented)
|
||||
|
@ -1431,6 +1430,9 @@ export interface MetricsServiceSetup {
|
|||
getOpsMetrics$: () => Observable<OpsMetrics>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type MetricsServiceStart = MetricsServiceSetup;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex';
|
||||
|
||||
|
@ -2589,18 +2591,22 @@ export const ServiceStatusLevels: Readonly<{
|
|||
available: Readonly<{
|
||||
toString: () => "available";
|
||||
valueOf: () => 0;
|
||||
toJSON: () => "available";
|
||||
}>;
|
||||
degraded: Readonly<{
|
||||
toString: () => "degraded";
|
||||
valueOf: () => 1;
|
||||
toJSON: () => "degraded";
|
||||
}>;
|
||||
unavailable: Readonly<{
|
||||
toString: () => "unavailable";
|
||||
valueOf: () => 2;
|
||||
toJSON: () => "unavailable";
|
||||
}>;
|
||||
critical: Readonly<{
|
||||
toString: () => "critical";
|
||||
valueOf: () => 3;
|
||||
toJSON: () => "critical";
|
||||
}>;
|
||||
}>;
|
||||
|
||||
|
@ -2676,6 +2682,7 @@ export interface StatusServiceSetup {
|
|||
dependencies$: Observable<Record<string, ServiceStatus>>;
|
||||
// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "kibana" does not have an export "StatusSetup"
|
||||
derivedStatus$: Observable<ServiceStatus>;
|
||||
isStatusPageAnonymous: () => boolean;
|
||||
overall$: Observable<ServiceStatus>;
|
||||
set(status$: Observable<ServiceStatus>): void;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
114
src/core/server/status/legacy_status.test.ts
Normal file
114
src/core/server/status/legacy_status.test.ts
Normal file
|
@ -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',
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
158
src/core/server/status/legacy_status.ts
Normal file
158
src/core/server/status/legacy_status.ts
Normal file
|
@ -0,0 +1,158 @@
|
|||
/*
|
||||
* 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 { deepFreeze } from '@kbn/std';
|
||||
|
||||
import { ServiceStatusLevels, ServiceStatus, CoreStatus } from './types';
|
||||
import { PluginName } from '../plugins';
|
||||
|
||||
interface Deps {
|
||||
overall: ServiceStatus;
|
||||
core: CoreStatus;
|
||||
plugins: Record<PluginName, ServiceStatus>;
|
||||
versionWithoutSnapshot: string;
|
||||
}
|
||||
|
||||
export interface LegacyStatusInfo {
|
||||
overall: LegacyStatusOverall;
|
||||
statuses: StatusComponentHttp[];
|
||||
}
|
||||
|
||||
interface LegacyStatusOverall {
|
||||
state: LegacyStatusState;
|
||||
title: string;
|
||||
nickname: string;
|
||||
uiColor: LegacyStatusUiColor;
|
||||
/** ISO-8601 date string w/o timezone */
|
||||
since: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export const calculateLegacyStatus = ({
|
||||
core,
|
||||
overall,
|
||||
plugins,
|
||||
versionWithoutSnapshot,
|
||||
}: Deps): LegacyStatusInfo => {
|
||||
const since = new Date().toISOString();
|
||||
const overallLegacy: LegacyStatusOverall = {
|
||||
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,
|
||||
...serviceStatusAttrs(status),
|
||||
});
|
||||
|
||||
const serviceStatusAttrs = (status: ServiceStatus) =>
|
||||
pick(STATUS_LEVEL_LEGACY_ATTRS[status.level.toString()], ['state', 'icon', 'uiColor']);
|
||||
|
||||
const STATUS_LEVEL_LEGACY_ATTRS = deepFreeze<Record<string, LegacyStateAttr>>({
|
||||
[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',
|
||||
},
|
||||
});
|
|
@ -17,4 +17,4 @@
|
|||
* under the License.
|
||||
*/
|
||||
|
||||
export { getKibanaInfoForStats } from './get_kibana_info_for_stats';
|
||||
export { registerStatusRoute } from './status';
|
322
src/core/server/status/routes/integration_tests/status.test.ts
Normal file
322
src/core/server/status/routes/integration_tests/status.test.ts
Normal file
|
@ -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<MetricsServiceSetup>;
|
||||
|
||||
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<Record<string, ServiceStatus>>({
|
||||
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',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
177
src/core/server/status/routes/status.ts
Normal file
177
src/core/server/status/routes/status.ts
Normal file
|
@ -0,0 +1,177 @@
|
|||
/*
|
||||
* 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<ServiceStatus>;
|
||||
core$: Observable<CoreStatus>;
|
||||
plugins$: Observable<Record<PluginName, ServiceStatus>>;
|
||||
};
|
||||
}
|
||||
|
||||
interface StatusInfo {
|
||||
overall: ServiceStatus;
|
||||
core: CoreStatus;
|
||||
plugins: Record<string, ServiceStatus>;
|
||||
}
|
||||
|
||||
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<string, number>;
|
||||
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<number, number>;
|
||||
status_codes: Record<number, number>;
|
||||
};
|
||||
concurrent_connections: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const registerStatusRoute = ({ router, config, metrics, status }: Deps) => {
|
||||
// Since the status.plugins$ observable is not subscribed to elsewhere, we need to subscribe it here to eagerly load
|
||||
// the plugins status when Kibana starts up so this endpoint responds quickly on first boot.
|
||||
const combinedStatus$ = new ReplaySubject<
|
||||
[ServiceStatus<unknown>, CoreStatus, Record<string, ServiceStatus<unknown>>]
|
||||
>(1);
|
||||
combineLatest([status.overall$, status.core$, status.plugins$]).subscribe(combinedStatus$);
|
||||
|
||||
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 combinedStatus$.pipe(first()).toPromise();
|
||||
|
||||
let statusInfo: StatusInfo | LegacyStatusInfo;
|
||||
if (req.query?.v8format) {
|
||||
statusInfo = {
|
||||
overall,
|
||||
core,
|
||||
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 });
|
||||
}
|
||||
);
|
||||
};
|
|
@ -43,6 +43,7 @@ const createSetupContractMock = () => {
|
|||
set: jest.fn(),
|
||||
dependencies$: new BehaviorSubject({}),
|
||||
derivedStatus$: new BehaviorSubject(available),
|
||||
isStatusPageAnonymous: jest.fn().mockReturnValue(false),
|
||||
};
|
||||
|
||||
return setupContract;
|
||||
|
|
|
@ -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<StatusService['setup']>[0];
|
||||
const setupDeps = (overrides: Partial<SetupDeps>): 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));
|
||||
|
|
|
@ -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<InternalElasticsearchServiceSetup, 'status$'>;
|
||||
environment: InternalEnvironmentServiceSetup;
|
||||
pluginDependencies: ReadonlyMap<PluginName, PluginName[]>;
|
||||
http: InternalHttpServiceSetup;
|
||||
metrics: InternalMetricsServiceSetup;
|
||||
savedObjects: Pick<InternalSavedObjectsServiceSetup, 'status$'>;
|
||||
}
|
||||
|
||||
|
@ -44,13 +51,21 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
|
|||
private readonly config$: Observable<StatusConfigType>;
|
||||
|
||||
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<StatusConfigType>(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<InternalStatusServiceSetup> {
|
|||
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<InternalStatusServiceSetup> {
|
|||
|
||||
public start() {}
|
||||
|
||||
public stop() {}
|
||||
public stop() {
|
||||
if (this.overallSubscription) {
|
||||
this.overallSubscription.unsubscribe();
|
||||
this.overallSubscription = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
private setupCoreStatus({
|
||||
elasticsearch,
|
||||
|
|
|
@ -71,6 +71,9 @@ export const ServiceStatusLevels = deepFreeze({
|
|||
available: {
|
||||
toString: () => 'available',
|
||||
valueOf: () => 0,
|
||||
toJSON() {
|
||||
return this.toString();
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Some features may not be working.
|
||||
|
@ -78,6 +81,9 @@ export const ServiceStatusLevels = deepFreeze({
|
|||
degraded: {
|
||||
toString: () => 'degraded',
|
||||
valueOf: () => 1,
|
||||
toJSON() {
|
||||
return this.toString();
|
||||
},
|
||||
},
|
||||
/**
|
||||
* The service is unavailable, but other functions that do not depend on this service should work.
|
||||
|
@ -85,6 +91,9 @@ export const ServiceStatusLevels = deepFreeze({
|
|||
unavailable: {
|
||||
toString: () => 'unavailable',
|
||||
valueOf: () => 2,
|
||||
toJSON() {
|
||||
return this.toString();
|
||||
},
|
||||
},
|
||||
/**
|
||||
* Block all user functions and display the status page, reserved for Core services only.
|
||||
|
@ -92,6 +101,9 @@ export const ServiceStatusLevels = deepFreeze({
|
|||
critical: {
|
||||
toString: () => 'critical',
|
||||
valueOf: () => 3,
|
||||
toJSON() {
|
||||
return this.toString();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -217,11 +229,17 @@ export interface StatusServiceSetup {
|
|||
* through the dependency tree
|
||||
*/
|
||||
derivedStatus$: Observable<ServiceStatus>;
|
||||
|
||||
/**
|
||||
* 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<StatusServiceSetup, 'core$' | 'overall$'> {
|
||||
isStatusPageAnonymous: () => boolean;
|
||||
export interface InternalStatusServiceSetup
|
||||
extends Pick<StatusServiceSetup, 'core$' | 'overall$' | 'isStatusPageAnonymous'> {
|
||||
// Namespaced under `plugins` key to improve clarity that these are APIs for plugins specifically.
|
||||
plugins: {
|
||||
set(plugin: PluginName, status$: Observable<ServiceStatus>): void;
|
||||
|
|
|
@ -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 configCompleteMixin from './config/complete';
|
||||
import { optimizeMixin } from '../../optimize';
|
||||
import * as Plugins from './plugins';
|
||||
|
@ -90,7 +89,6 @@ export default class KbnServer {
|
|||
|
||||
loggingMixin,
|
||||
warningsMixin,
|
||||
statusMixin,
|
||||
|
||||
// scan translations dirs, register locale files and initialize i18n engine.
|
||||
i18nMixin,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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<string, any>) {
|
||||
return mapKeys(object, (value, key) => snakeCase(key));
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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'),
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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('ops.cGroupOverrides.cpuPath'),
|
||||
cpuAcctPath: this.config.get('ops.cGroupOverrides.cpuAcctPath'),
|
||||
});
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
|
@ -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';
|
|
@ -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;
|
|
@ -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),
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
};
|
||||
});
|
|
@ -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');
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -49,8 +49,25 @@ export class UsageCollectionPlugin implements Plugin<CollectorSet> {
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -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<ServiceStatus>;
|
||||
}) {
|
||||
registerUiMetricRoute(router, getSavedObjects);
|
||||
registerStatsRoute({ router, ...rest });
|
||||
}
|
||||
|
|
|
@ -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<typeof createHttpServer>;
|
||||
type HttpSetup = UnwrapPromise<ReturnType<HttpService['setup']>>;
|
||||
|
||||
describe('/api/stats', () => {
|
||||
let server: HttpService;
|
||||
let httpSetup: HttpSetup;
|
||||
let overallStatus$: BehaviorSubject<ServiceStatus>;
|
||||
let metrics: MetricsServiceSetup;
|
||||
|
||||
beforeEach(async () => {
|
||||
server = createHttpServer();
|
||||
httpSetup = await server.setup({
|
||||
context: contextServiceMock.createSetupContract(),
|
||||
});
|
||||
overallStatus$ = new BehaviorSubject<ServiceStatus>({
|
||||
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),
|
||||
});
|
||||
});
|
||||
});
|
190
src/plugins/usage_collection/server/routes/stats.ts
Normal file
190
src/plugins/usage_collection/server/routes/stats.ts
Normal file
|
@ -0,0 +1,190 @@
|
|||
/*
|
||||
* 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<ServiceStatus>;
|
||||
}) {
|
||||
const getUsage = async (callCluster: LegacyAPICaller): Promise<any> => {
|
||||
const usage = await collectorSet.bulkFetchUsage(callCluster);
|
||||
return collectorSet.toObject(usage);
|
||||
};
|
||||
|
||||
const getClusterUuid = async (callCluster: LegacyAPICaller): Promise<string> => {
|
||||
const { cluster_uuid: uuid } = await callCluster('info', { filterPath: 'cluster_uuid' });
|
||||
return uuid;
|
||||
};
|
||||
|
||||
router.get(
|
||||
{
|
||||
path: '/api/stats',
|
||||
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({
|
||||
extended: schema.oneOf([schema.literal(''), schema.boolean()], { defaultValue: false }),
|
||||
legacy: schema.oneOf([schema.literal(''), schema.boolean()], { defaultValue: false }),
|
||||
exclude_usage: schema.oneOf([schema.literal(''), schema.boolean()], {
|
||||
defaultValue: false,
|
||||
}),
|
||||
}),
|
||||
},
|
||||
},
|
||||
async (context, req, res) => {
|
||||
const isExtended = req.query.extended === '' || req.query.extended;
|
||||
const isLegacy = req.query.legacy === '' || req.query.legacy;
|
||||
const shouldGetUsage = req.query.exclude_usage === false;
|
||||
|
||||
let extended;
|
||||
if (isExtended) {
|
||||
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$
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
const { collected_at, ...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: collected_at.toISOString(),
|
||||
collection_interval_in_millis: metrics.collectionInterval,
|
||||
});
|
||||
|
||||
return res.ok({
|
||||
body: {
|
||||
...kibanaStats,
|
||||
...extended,
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
const ServiceStatusToLegacyState: Record<string, string> = {
|
||||
[ServiceStatusLevels.critical.toString()]: 'red',
|
||||
[ServiceStatusLevels.unavailable.toString()]: 'red',
|
||||
[ServiceStatusLevels.degraded.toString()]: 'yellow',
|
||||
[ServiceStatusLevels.available.toString()]: 'green',
|
||||
};
|
|
@ -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 } from './server/routes/api/v1';
|
||||
|
||||
|
@ -28,8 +27,6 @@ export const xpackMain = (kibana) => {
|
|||
},
|
||||
|
||||
init(server) {
|
||||
mirrorPluginStatus(server.plugins.elasticsearch, this, 'yellow', 'red');
|
||||
|
||||
setupXPackMain(server);
|
||||
|
||||
// register routes
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,23 +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) {
|
||||
// eslint-disable-line no-unused-vars
|
||||
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
|
||||
}
|
|
@ -4,21 +4,31 @@
|
|||
* 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];
|
||||
const subscription = server.newPlatform.setup.core.status.core$
|
||||
.pipe(pairwise())
|
||||
.subscribe(([coreLast, coreCurrent]) => {
|
||||
if (
|
||||
!subscription.closed &&
|
||||
coreLast.elasticsearch.level !== ServiceStatusLevels.available &&
|
||||
coreCurrent.elasticsearch.level === ServiceStatusLevels.available
|
||||
) {
|
||||
// Unsubscribe as soon as ES becomes available so this function only runs once
|
||||
subscription.unsubscribe();
|
||||
|
||||
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);
|
||||
});
|
||||
});
|
||||
// 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -2930,12 +2930,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": "エクスポートされた値をこの文字列で区切ります",
|
||||
|
|
|
@ -2931,12 +2931,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": "使用此字符串分隔导出的值",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue