From 03e344603aa0f8435e32cc3050c21707283b8475 Mon Sep 17 00:00:00 2001 From: Pierre Gayvallet Date: Tue, 3 Mar 2020 10:54:03 +0100 Subject: [PATCH] Add core metrics service (#58623) * create base service and collectors * wire the service into server, add mock * add collector tests * add main collector test * export metric types from server * add service and server tests * updates generated doc * improve doc * nits and comments * add disconnected requests test --- .../core/server/kibana-plugin-server.md | 5 + ...rver.metricsservicesetup.getopsmetrics_.md | 24 +++ ...ibana-plugin-server.metricsservicesetup.md | 20 ++ ...erver.opsmetrics.concurrent_connections.md | 13 ++ .../server/kibana-plugin-server.opsmetrics.md | 24 +++ .../kibana-plugin-server.opsmetrics.os.md | 13 ++ ...kibana-plugin-server.opsmetrics.process.md | 13 ++ ...ibana-plugin-server.opsmetrics.requests.md | 13 ++ ...plugin-server.opsmetrics.response_times.md | 13 ++ ...ibana-plugin-server.opsosmetrics.distro.md | 13 ++ ...lugin-server.opsosmetrics.distrorelease.md | 13 ++ .../kibana-plugin-server.opsosmetrics.load.md | 17 ++ .../kibana-plugin-server.opsosmetrics.md | 26 +++ ...ibana-plugin-server.opsosmetrics.memory.md | 17 ++ ...ana-plugin-server.opsosmetrics.platform.md | 13 ++ ...gin-server.opsosmetrics.platformrelease.md | 13 ++ ...in-server.opsosmetrics.uptime_in_millis.md | 13 ++ ...rver.opsprocessmetrics.event_loop_delay.md | 13 ++ .../kibana-plugin-server.opsprocessmetrics.md | 23 +++ ...-plugin-server.opsprocessmetrics.memory.md | 20 ++ ...ana-plugin-server.opsprocessmetrics.pid.md | 13 ++ ...rver.opsprocessmetrics.uptime_in_millis.md | 13 ++ ...opsservermetrics.concurrent_connections.md | 13 ++ .../kibana-plugin-server.opsservermetrics.md | 22 +++ ...plugin-server.opsservermetrics.requests.md | 17 ++ ...-server.opsservermetrics.response_times.md | 16 ++ package.json | 1 + src/core/server/index.ts | 8 + src/core/server/internal_types.ts | 2 + src/core/server/legacy/legacy_service.test.ts | 2 + src/core/server/metrics/collectors/index.ts | 23 +++ src/core/server/metrics/collectors/os.test.ts | 99 ++++++++++ src/core/server/metrics/collectors/os.ts | 60 ++++++ .../server/metrics/collectors/process.test.ts | 81 ++++++++ src/core/server/metrics/collectors/process.ts | 52 +++++ src/core/server/metrics/collectors/server.ts | 80 ++++++++ src/core/server/metrics/collectors/types.ts | 110 +++++++++++ src/core/server/metrics/index.ts | 29 +++ .../server_collector.test.ts | 183 ++++++++++++++++++ .../server/metrics/metrics_service.mock.ts | 67 +++++++ .../metrics/metrics_service.test.mocks.ts | 25 +++ .../server/metrics/metrics_service.test.ts | 134 +++++++++++++ src/core/server/metrics/metrics_service.ts | 86 ++++++++ src/core/server/metrics/ops_config.ts | 29 +++ .../ops_metrics_collector.test.mocks.ts | 39 ++++ .../metrics/ops_metrics_collector.test.ts | 59 ++++++ .../server/metrics/ops_metrics_collector.ts | 52 +++++ src/core/server/metrics/types.ts | 66 +++++++ src/core/server/mocks.ts | 5 +- src/core/server/server.api.md | 62 ++++++ src/core/server/server.test.mocks.ts | 6 + src/core/server/server.test.ts | 9 + src/core/server/server.ts | 9 + 53 files changed, 1790 insertions(+), 1 deletion(-) create mode 100644 docs/development/core/server/kibana-plugin-server.metricsservicesetup.getopsmetrics_.md create mode 100644 docs/development/core/server/kibana-plugin-server.metricsservicesetup.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsmetrics.concurrent_connections.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsmetrics.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsmetrics.os.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsmetrics.process.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsmetrics.requests.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsmetrics.response_times.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.distro.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.distrorelease.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.load.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.memory.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.platform.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.platformrelease.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsosmetrics.uptime_in_millis.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsprocessmetrics.event_loop_delay.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsprocessmetrics.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsprocessmetrics.memory.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsprocessmetrics.pid.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsservermetrics.concurrent_connections.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsservermetrics.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsservermetrics.requests.md create mode 100644 docs/development/core/server/kibana-plugin-server.opsservermetrics.response_times.md create mode 100644 src/core/server/metrics/collectors/index.ts create mode 100644 src/core/server/metrics/collectors/os.test.ts create mode 100644 src/core/server/metrics/collectors/os.ts create mode 100644 src/core/server/metrics/collectors/process.test.ts create mode 100644 src/core/server/metrics/collectors/process.ts create mode 100644 src/core/server/metrics/collectors/server.ts create mode 100644 src/core/server/metrics/collectors/types.ts create mode 100644 src/core/server/metrics/index.ts create mode 100644 src/core/server/metrics/integration_tests/server_collector.test.ts create mode 100644 src/core/server/metrics/metrics_service.mock.ts create mode 100644 src/core/server/metrics/metrics_service.test.mocks.ts create mode 100644 src/core/server/metrics/metrics_service.test.ts create mode 100644 src/core/server/metrics/metrics_service.ts create mode 100644 src/core/server/metrics/ops_config.ts create mode 100644 src/core/server/metrics/ops_metrics_collector.test.mocks.ts create mode 100644 src/core/server/metrics/ops_metrics_collector.test.ts create mode 100644 src/core/server/metrics/ops_metrics_collector.ts create mode 100644 src/core/server/metrics/types.ts diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md index 15a1fd050625..c948c8992079 100644 --- a/docs/development/core/server/kibana-plugin-server.md +++ b/docs/development/core/server/kibana-plugin-server.md @@ -88,11 +88,16 @@ The plugin integrates with the core system via lifecycle events: `setup` | [Logger](./kibana-plugin-server.logger.md) | Logger exposes all the necessary methods to log any type of information and this is the interface used by the logging consumers including plugins. | | [LoggerFactory](./kibana-plugin-server.loggerfactory.md) | The single purpose of LoggerFactory interface is to define a way to retrieve a context-based logger instance. | | [LogMeta](./kibana-plugin-server.logmeta.md) | Contextual metadata | +| [MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) | APIs to retrieves metrics gathered and exposed by the core platform. | | [OnPostAuthToolkit](./kibana-plugin-server.onpostauthtoolkit.md) | A tool set defining an outcome of OnPostAuth interceptor for incoming request. | | [OnPreAuthToolkit](./kibana-plugin-server.onpreauthtoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | | [OnPreResponseExtensions](./kibana-plugin-server.onpreresponseextensions.md) | Additional data to extend a response. | | [OnPreResponseInfo](./kibana-plugin-server.onpreresponseinfo.md) | Response status code. | | [OnPreResponseToolkit](./kibana-plugin-server.onpreresponsetoolkit.md) | A tool set defining an outcome of OnPreAuth interceptor for incoming request. | +| [OpsMetrics](./kibana-plugin-server.opsmetrics.md) | Regroups metrics gathered by all the collectors. This contains metrics about the os/runtime, the kibana process and the http server. | +| [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) | OS related metrics | +| [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) | Process related metrics | +| [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) | server related metrics | | [PackageInfo](./kibana-plugin-server.packageinfo.md) | | | [Plugin](./kibana-plugin-server.plugin.md) | The interface that should be returned by a PluginInitializer. | | [PluginConfigDescriptor](./kibana-plugin-server.pluginconfigdescriptor.md) | Describes a plugin configuration properties. | diff --git a/docs/development/core/server/kibana-plugin-server.metricsservicesetup.getopsmetrics_.md b/docs/development/core/server/kibana-plugin-server.metricsservicesetup.getopsmetrics_.md new file mode 100644 index 000000000000..454b8c905451 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.metricsservicesetup.getopsmetrics_.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) > [getOpsMetrics$](./kibana-plugin-server.metricsservicesetup.getopsmetrics_.md) + +## MetricsServiceSetup.getOpsMetrics$ property + +Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-server.opsmetrics.md) gathered. The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, based on the `opts.interval` configuration property. + +Signature: + +```typescript +getOpsMetrics$: () => Observable; +``` + +## Example + + +```ts +core.metrics.getOpsMetrics$().subscribe(metrics => { + // do something with the metrics +}) + +``` + diff --git a/docs/development/core/server/kibana-plugin-server.metricsservicesetup.md b/docs/development/core/server/kibana-plugin-server.metricsservicesetup.md new file mode 100644 index 000000000000..270c56402a39 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.metricsservicesetup.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [MetricsServiceSetup](./kibana-plugin-server.metricsservicesetup.md) + +## MetricsServiceSetup interface + +APIs to retrieves metrics gathered and exposed by the core platform. + +Signature: + +```typescript +export interface MetricsServiceSetup +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [getOpsMetrics$](./kibana-plugin-server.metricsservicesetup.getopsmetrics_.md) | () => Observable<OpsMetrics> | Retrieve an observable emitting the [OpsMetrics](./kibana-plugin-server.opsmetrics.md) gathered. The observable will emit an initial value during core's start phase, and a new value every fixed interval of time, based on the opts.interval configuration property. | + diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.concurrent_connections.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.concurrent_connections.md new file mode 100644 index 000000000000..cfd39a551ad3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.concurrent_connections.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [concurrent\_connections](./kibana-plugin-server.opsmetrics.concurrent_connections.md) + +## OpsMetrics.concurrent\_connections property + +number of current concurrent connections to the server + +Signature: + +```typescript +concurrent_connections: OpsServerMetrics['concurrent_connections']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.md new file mode 100644 index 000000000000..e23bd8d431d3 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.md @@ -0,0 +1,24 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) + +## OpsMetrics interface + +Regroups metrics gathered by all the collectors. This contains metrics about the os/runtime, the kibana process and the http server. + +Signature: + +```typescript +export interface OpsMetrics +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [concurrent\_connections](./kibana-plugin-server.opsmetrics.concurrent_connections.md) | OpsServerMetrics['concurrent_connections'] | number of current concurrent connections to the server | +| [os](./kibana-plugin-server.opsmetrics.os.md) | OpsOsMetrics | OS related metrics | +| [process](./kibana-plugin-server.opsmetrics.process.md) | OpsProcessMetrics | Process related metrics | +| [requests](./kibana-plugin-server.opsmetrics.requests.md) | OpsServerMetrics['requests'] | server requests stats | +| [response\_times](./kibana-plugin-server.opsmetrics.response_times.md) | OpsServerMetrics['response_times'] | server response time stats | + diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.os.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.os.md new file mode 100644 index 000000000000..993a1d7a2d7b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.os.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [os](./kibana-plugin-server.opsmetrics.os.md) + +## OpsMetrics.os property + +OS related metrics + +Signature: + +```typescript +os: OpsOsMetrics; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.process.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.process.md new file mode 100644 index 000000000000..53d3a33d66e0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.process.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [process](./kibana-plugin-server.opsmetrics.process.md) + +## OpsMetrics.process property + +Process related metrics + +Signature: + +```typescript +process: OpsProcessMetrics; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.requests.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.requests.md new file mode 100644 index 000000000000..9cd6b85e507f --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.requests.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [requests](./kibana-plugin-server.opsmetrics.requests.md) + +## OpsMetrics.requests property + +server requests stats + +Signature: + +```typescript +requests: OpsServerMetrics['requests']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsmetrics.response_times.md b/docs/development/core/server/kibana-plugin-server.opsmetrics.response_times.md new file mode 100644 index 000000000000..358699071b1c --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsmetrics.response_times.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsMetrics](./kibana-plugin-server.opsmetrics.md) > [response\_times](./kibana-plugin-server.opsmetrics.response_times.md) + +## OpsMetrics.response\_times property + +server response time stats + +Signature: + +```typescript +response_times: OpsServerMetrics['response_times']; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.distro.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.distro.md new file mode 100644 index 000000000000..338164f173d0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.distro.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [distro](./kibana-plugin-server.opsosmetrics.distro.md) + +## OpsOsMetrics.distro property + +The os distrib. Only present for linux platforms + +Signature: + +```typescript +distro?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.distrorelease.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.distrorelease.md new file mode 100644 index 000000000000..24c5a1f00b64 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.distrorelease.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [distroRelease](./kibana-plugin-server.opsosmetrics.distrorelease.md) + +## OpsOsMetrics.distroRelease property + +The os distrib release, prefixed by the os distrib. Only present for linux platforms + +Signature: + +```typescript +distroRelease?: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.load.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.load.md new file mode 100644 index 000000000000..0bf17502ce34 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.load.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [load](./kibana-plugin-server.opsosmetrics.load.md) + +## OpsOsMetrics.load property + +cpu load metrics + +Signature: + +```typescript +load: { + '1m': number; + '5m': number; + '15m': number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.md new file mode 100644 index 000000000000..0fb4e59fdf53 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.md @@ -0,0 +1,26 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) + +## OpsOsMetrics interface + +OS related metrics + +Signature: + +```typescript +export interface OpsOsMetrics +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [distro](./kibana-plugin-server.opsosmetrics.distro.md) | string | The os distrib. Only present for linux platforms | +| [distroRelease](./kibana-plugin-server.opsosmetrics.distrorelease.md) | string | The os distrib release, prefixed by the os distrib. Only present for linux platforms | +| [load](./kibana-plugin-server.opsosmetrics.load.md) | {
'1m': number;
'5m': number;
'15m': number;
} | cpu load metrics | +| [memory](./kibana-plugin-server.opsosmetrics.memory.md) | {
total_in_bytes: number;
free_in_bytes: number;
used_in_bytes: number;
} | system memory usage metrics | +| [platform](./kibana-plugin-server.opsosmetrics.platform.md) | NodeJS.Platform | The os platform | +| [platformRelease](./kibana-plugin-server.opsosmetrics.platformrelease.md) | string | The os platform release, prefixed by the platform name | +| [uptime\_in\_millis](./kibana-plugin-server.opsosmetrics.uptime_in_millis.md) | number | the OS uptime | + diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.memory.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.memory.md new file mode 100644 index 000000000000..4a1becaeeaec --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.memory.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [memory](./kibana-plugin-server.opsosmetrics.memory.md) + +## OpsOsMetrics.memory property + +system memory usage metrics + +Signature: + +```typescript +memory: { + total_in_bytes: number; + free_in_bytes: number; + used_in_bytes: number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.platform.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.platform.md new file mode 100644 index 000000000000..411d0fc546dc --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.platform.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [platform](./kibana-plugin-server.opsosmetrics.platform.md) + +## OpsOsMetrics.platform property + +The os platform + +Signature: + +```typescript +platform: NodeJS.Platform; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.platformrelease.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.platformrelease.md new file mode 100644 index 000000000000..1071b4a38f58 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.platformrelease.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [platformRelease](./kibana-plugin-server.opsosmetrics.platformrelease.md) + +## OpsOsMetrics.platformRelease property + +The os platform release, prefixed by the platform name + +Signature: + +```typescript +platformRelease: string; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsosmetrics.uptime_in_millis.md b/docs/development/core/server/kibana-plugin-server.opsosmetrics.uptime_in_millis.md new file mode 100644 index 000000000000..dfff1a1f1da0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsosmetrics.uptime_in_millis.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsOsMetrics](./kibana-plugin-server.opsosmetrics.md) > [uptime\_in\_millis](./kibana-plugin-server.opsosmetrics.uptime_in_millis.md) + +## OpsOsMetrics.uptime\_in\_millis property + +the OS uptime + +Signature: + +```typescript +uptime_in_millis: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.event_loop_delay.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.event_loop_delay.md new file mode 100644 index 000000000000..f61c8b099532 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.event_loop_delay.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) > [event\_loop\_delay](./kibana-plugin-server.opsprocessmetrics.event_loop_delay.md) + +## OpsProcessMetrics.event\_loop\_delay property + +node event loop delay + +Signature: + +```typescript +event_loop_delay: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.md new file mode 100644 index 000000000000..92fd8471cce7 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.md @@ -0,0 +1,23 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) + +## OpsProcessMetrics interface + +Process related metrics + +Signature: + +```typescript +export interface OpsProcessMetrics +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [event\_loop\_delay](./kibana-plugin-server.opsprocessmetrics.event_loop_delay.md) | number | node event loop delay | +| [memory](./kibana-plugin-server.opsprocessmetrics.memory.md) | {
heap: {
total_in_bytes: number;
used_in_bytes: number;
size_limit: number;
};
resident_set_size_in_bytes: number;
} | process memory usage | +| [pid](./kibana-plugin-server.opsprocessmetrics.pid.md) | number | pid of the kibana process | +| [uptime\_in\_millis](./kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md) | number | uptime of the kibana process | + diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.memory.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.memory.md new file mode 100644 index 000000000000..5c1a8de70dc0 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.memory.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) > [memory](./kibana-plugin-server.opsprocessmetrics.memory.md) + +## OpsProcessMetrics.memory property + +process memory usage + +Signature: + +```typescript +memory: { + heap: { + total_in_bytes: number; + used_in_bytes: number; + size_limit: number; + }; + resident_set_size_in_bytes: number; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.pid.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.pid.md new file mode 100644 index 000000000000..a34187f37201 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.pid.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) > [pid](./kibana-plugin-server.opsprocessmetrics.pid.md) + +## OpsProcessMetrics.pid property + +pid of the kibana process + +Signature: + +```typescript +pid: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md new file mode 100644 index 000000000000..24db2f017a66 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsProcessMetrics](./kibana-plugin-server.opsprocessmetrics.md) > [uptime\_in\_millis](./kibana-plugin-server.opsprocessmetrics.uptime_in_millis.md) + +## OpsProcessMetrics.uptime\_in\_millis property + +uptime of the kibana process + +Signature: + +```typescript +uptime_in_millis: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsservermetrics.concurrent_connections.md b/docs/development/core/server/kibana-plugin-server.opsservermetrics.concurrent_connections.md new file mode 100644 index 000000000000..ade79fedfa1b --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsservermetrics.concurrent_connections.md @@ -0,0 +1,13 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) > [concurrent\_connections](./kibana-plugin-server.opsservermetrics.concurrent_connections.md) + +## OpsServerMetrics.concurrent\_connections property + +number of current concurrent connections to the server + +Signature: + +```typescript +concurrent_connections: number; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsservermetrics.md b/docs/development/core/server/kibana-plugin-server.opsservermetrics.md new file mode 100644 index 000000000000..4e35c02bd9f2 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsservermetrics.md @@ -0,0 +1,22 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) + +## OpsServerMetrics interface + +server related metrics + +Signature: + +```typescript +export interface OpsServerMetrics +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [concurrent\_connections](./kibana-plugin-server.opsservermetrics.concurrent_connections.md) | number | number of current concurrent connections to the server | +| [requests](./kibana-plugin-server.opsservermetrics.requests.md) | {
disconnects: number;
total: number;
statusCodes: Record<number, number>;
} | server requests stats | +| [response\_times](./kibana-plugin-server.opsservermetrics.response_times.md) | {
avg_in_millis: number;
max_in_millis: number;
} | server response time stats | + diff --git a/docs/development/core/server/kibana-plugin-server.opsservermetrics.requests.md b/docs/development/core/server/kibana-plugin-server.opsservermetrics.requests.md new file mode 100644 index 000000000000..5ad2abc86955 --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsservermetrics.requests.md @@ -0,0 +1,17 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) > [requests](./kibana-plugin-server.opsservermetrics.requests.md) + +## OpsServerMetrics.requests property + +server requests stats + +Signature: + +```typescript +requests: { + disconnects: number; + total: number; + statusCodes: Record; + }; +``` diff --git a/docs/development/core/server/kibana-plugin-server.opsservermetrics.response_times.md b/docs/development/core/server/kibana-plugin-server.opsservermetrics.response_times.md new file mode 100644 index 000000000000..5008efc6ad4d --- /dev/null +++ b/docs/development/core/server/kibana-plugin-server.opsservermetrics.response_times.md @@ -0,0 +1,16 @@ + + +[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [OpsServerMetrics](./kibana-plugin-server.opsservermetrics.md) > [response\_times](./kibana-plugin-server.opsservermetrics.response_times.md) + +## OpsServerMetrics.response\_times property + +server response time stats + +Signature: + +```typescript +response_times: { + avg_in_millis: number; + max_in_millis: number; + }; +``` diff --git a/package.json b/package.json index 6c44c34996c6..0c2a50b158c8 100644 --- a/package.json +++ b/package.json @@ -326,6 +326,7 @@ "@types/fetch-mock": "^7.3.1", "@types/flot": "^0.0.31", "@types/getopts": "^2.0.1", + "@types/getos": "^3.0.0", "@types/glob": "^7.1.1", "@types/globby": "^8.0.0", "@types/hapi": "^17.0.18", diff --git a/src/core/server/index.ts b/src/core/server/index.ts index e45d4f28edcc..de6cdb2d7acd 100644 --- a/src/core/server/index.ts +++ b/src/core/server/index.ts @@ -245,6 +245,14 @@ export { StringValidationRegexString, } from './ui_settings'; +export { + OpsMetrics, + OpsOsMetrics, + OpsServerMetrics, + OpsProcessMetrics, + MetricsServiceSetup, +} from './metrics'; + export { RecursiveReadonly } from '../utils'; export { diff --git a/src/core/server/internal_types.ts b/src/core/server/internal_types.ts index ff68d1544d11..37d1061dc618 100644 --- a/src/core/server/internal_types.ts +++ b/src/core/server/internal_types.ts @@ -30,6 +30,7 @@ import { } from './saved_objects'; import { InternalUiSettingsServiceSetup, InternalUiSettingsServiceStart } from './ui_settings'; import { UuidServiceSetup } from './uuid'; +import { InternalMetricsServiceSetup } from './metrics'; /** @internal */ export interface InternalCoreSetup { @@ -40,6 +41,7 @@ export interface InternalCoreSetup { uiSettings: InternalUiSettingsServiceSetup; savedObjects: InternalSavedObjectsServiceSetup; uuid: UuidServiceSetup; + metrics: InternalMetricsServiceSetup; } /** diff --git a/src/core/server/legacy/legacy_service.test.ts b/src/core/server/legacy/legacy_service.test.ts index 46436461505c..50468db8a504 100644 --- a/src/core/server/legacy/legacy_service.test.ts +++ b/src/core/server/legacy/legacy_service.test.ts @@ -43,6 +43,7 @@ import { savedObjectsServiceMock } from '../saved_objects/saved_objects_service. import { capabilitiesServiceMock } from '../capabilities/capabilities_service.mock'; import { setupMock as renderingServiceMock } from '../rendering/__mocks__/rendering_service'; import { uuidServiceMock } from '../uuid/uuid_service.mock'; +import { metricsServiceMock } from '../metrics/metrics_service.mock'; import { findLegacyPluginSpecs } from './plugins'; import { LegacyVars, LegacyServiceSetupDeps, LegacyServiceStartDeps } from './types'; import { LegacyService } from './legacy_service'; @@ -93,6 +94,7 @@ beforeEach(() => { }, }, rendering: renderingServiceMock, + metrics: metricsServiceMock.createInternalSetupContract(), uuid: uuidSetup, }, plugins: { 'plugin-id': 'plugin-value' }, diff --git a/src/core/server/metrics/collectors/index.ts b/src/core/server/metrics/collectors/index.ts new file mode 100644 index 000000000000..f58ab02e6388 --- /dev/null +++ b/src/core/server/metrics/collectors/index.ts @@ -0,0 +1,23 @@ +/* + * 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 { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics, MetricsCollector } from './types'; +export { OsMetricsCollector } from './os'; +export { ProcessMetricsCollector } from './process'; +export { ServerMetricsCollector } from './server'; diff --git a/src/core/server/metrics/collectors/os.test.ts b/src/core/server/metrics/collectors/os.test.ts new file mode 100644 index 000000000000..7d5a6da90b7d --- /dev/null +++ b/src/core/server/metrics/collectors/os.test.ts @@ -0,0 +1,99 @@ +/* + * 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('getos', () => (cb: Function) => cb(null, { dist: 'distrib', release: 'release' })); + +import os from 'os'; +import { OsMetricsCollector } from './os'; + +describe('OsMetricsCollector', () => { + let collector: OsMetricsCollector; + + beforeEach(() => { + collector = new OsMetricsCollector(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('collects platform info from the os package', async () => { + const platform = 'darwin'; + const release = '10.14.1'; + + jest.spyOn(os, 'platform').mockImplementation(() => platform); + jest.spyOn(os, 'release').mockImplementation(() => release); + + const metrics = await collector.collect(); + + expect(metrics.platform).toBe(platform); + expect(metrics.platformRelease).toBe(`${platform}-${release}`); + }); + + it('collects distribution info when platform is linux', async () => { + const platform = 'linux'; + + jest.spyOn(os, 'platform').mockImplementation(() => platform); + + const metrics = await collector.collect(); + + expect(metrics.distro).toBe('distrib'); + expect(metrics.distroRelease).toBe('distrib-release'); + }); + + it('collects memory info from the os package', async () => { + const totalMemory = 1457886; + const freeMemory = 456786; + + jest.spyOn(os, 'totalmem').mockImplementation(() => totalMemory); + jest.spyOn(os, 'freemem').mockImplementation(() => freeMemory); + + const metrics = await collector.collect(); + + expect(metrics.memory.total_in_bytes).toBe(totalMemory); + expect(metrics.memory.free_in_bytes).toBe(freeMemory); + expect(metrics.memory.used_in_bytes).toBe(totalMemory - freeMemory); + }); + + it('collects uptime info from the os package', async () => { + const uptime = 325; + + jest.spyOn(os, 'uptime').mockImplementation(() => uptime); + + const metrics = await collector.collect(); + + expect(metrics.uptime_in_millis).toBe(uptime * 1000); + }); + + it('collects load info from the os package', async () => { + const oneMinLoad = 1; + const fiveMinLoad = 2; + const fifteenMinLoad = 3; + + jest.spyOn(os, 'loadavg').mockImplementation(() => [oneMinLoad, fiveMinLoad, fifteenMinLoad]); + + const metrics = await collector.collect(); + + expect(metrics.load).toEqual({ + '1m': oneMinLoad, + '5m': fiveMinLoad, + '15m': fifteenMinLoad, + }); + }); +}); diff --git a/src/core/server/metrics/collectors/os.ts b/src/core/server/metrics/collectors/os.ts new file mode 100644 index 000000000000..d3d9bb0be86f --- /dev/null +++ b/src/core/server/metrics/collectors/os.ts @@ -0,0 +1,60 @@ +/* + * 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 getosAsync, { LinuxOs } from 'getos'; +import { promisify } from 'util'; +import { OpsOsMetrics, MetricsCollector } from './types'; + +const getos = promisify(getosAsync); + +export class OsMetricsCollector implements MetricsCollector { + public async collect(): Promise { + const platform = os.platform(); + const load = os.loadavg(); + + const metrics: OpsOsMetrics = { + platform, + platformRelease: `${platform}-${os.release()}`, + load: { + '1m': load[0], + '5m': load[1], + '15m': load[2], + }, + memory: { + total_in_bytes: os.totalmem(), + free_in_bytes: os.freemem(), + used_in_bytes: os.totalmem() - os.freemem(), + }, + uptime_in_millis: os.uptime() * 1000, + }; + + if (platform === 'linux') { + try { + const distro = (await getos()) as LinuxOs; + metrics.distro = distro.dist; + metrics.distroRelease = `${distro.dist}-${distro.release}`; + } catch (e) { + // ignore errors + } + } + + return metrics; + } +} diff --git a/src/core/server/metrics/collectors/process.test.ts b/src/core/server/metrics/collectors/process.test.ts new file mode 100644 index 000000000000..a437d799371f --- /dev/null +++ b/src/core/server/metrics/collectors/process.test.ts @@ -0,0 +1,81 @@ +/* + * 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 v8, { HeapInfo } from 'v8'; +import { ProcessMetricsCollector } from './process'; + +describe('ProcessMetricsCollector', () => { + let collector: ProcessMetricsCollector; + + beforeEach(() => { + collector = new ProcessMetricsCollector(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('collects pid from the process', async () => { + const metrics = await collector.collect(); + + expect(metrics.pid).toEqual(process.pid); + }); + + it('collects event loop delay', async () => { + const metrics = await collector.collect(); + + expect(metrics.event_loop_delay).toBeGreaterThan(0); + }); + + it('collects uptime info from the process', async () => { + const uptime = 58986; + jest.spyOn(process, 'uptime').mockImplementation(() => uptime); + + const metrics = await collector.collect(); + + expect(metrics.uptime_in_millis).toEqual(uptime * 1000); + }); + + it('collects memory info from the process', async () => { + const heapTotal = 58986; + const heapUsed = 4688; + const heapSizeLimit = 5788; + const rss = 5865; + jest.spyOn(process, 'memoryUsage').mockImplementation(() => ({ + rss, + heapTotal, + heapUsed, + external: 0, + })); + + jest.spyOn(v8, 'getHeapStatistics').mockImplementation( + () => + ({ + heap_size_limit: heapSizeLimit, + } as HeapInfo) + ); + + const metrics = await collector.collect(); + + expect(metrics.memory.heap.total_in_bytes).toEqual(heapTotal); + expect(metrics.memory.heap.used_in_bytes).toEqual(heapUsed); + expect(metrics.memory.heap.size_limit).toEqual(heapSizeLimit); + expect(metrics.memory.resident_set_size_in_bytes).toEqual(rss); + }); +}); diff --git a/src/core/server/metrics/collectors/process.ts b/src/core/server/metrics/collectors/process.ts new file mode 100644 index 000000000000..aa68abaf74e4 --- /dev/null +++ b/src/core/server/metrics/collectors/process.ts @@ -0,0 +1,52 @@ +/* + * 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 v8 from 'v8'; +import { Bench } from 'hoek'; +import { OpsProcessMetrics, MetricsCollector } from './types'; + +export class ProcessMetricsCollector implements MetricsCollector { + public async collect(): Promise { + const heapStats = v8.getHeapStatistics(); + const memoryUsage = process.memoryUsage(); + const [eventLoopDelay] = await Promise.all([getEventLoopDelay()]); + return { + memory: { + heap: { + total_in_bytes: memoryUsage.heapTotal, + used_in_bytes: memoryUsage.heapUsed, + size_limit: heapStats.heap_size_limit, + }, + resident_set_size_in_bytes: memoryUsage.rss, + }, + pid: process.pid, + event_loop_delay: eventLoopDelay, + uptime_in_millis: process.uptime() * 1000, + }; + } +} + +const getEventLoopDelay = (): Promise => { + const bench = new Bench(); + return new Promise(resolve => { + setImmediate(() => { + return resolve(bench.elapsed()); + }); + }); +}; diff --git a/src/core/server/metrics/collectors/server.ts b/src/core/server/metrics/collectors/server.ts new file mode 100644 index 000000000000..e46ac2f653df --- /dev/null +++ b/src/core/server/metrics/collectors/server.ts @@ -0,0 +1,80 @@ +/* + * 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 { ResponseObject, Server as HapiServer } from 'hapi'; +import { OpsServerMetrics, MetricsCollector } from './types'; + +interface ServerResponseTime { + count: number; + total: number; + max: number; +} + +export class ServerMetricsCollector implements MetricsCollector { + private readonly requests: OpsServerMetrics['requests'] = { + disconnects: 0, + total: 0, + statusCodes: {}, + }; + private readonly responseTimes: ServerResponseTime = { + count: 0, + total: 0, + max: 0, + }; + + constructor(private readonly server: HapiServer) { + this.server.ext('onRequest', (request, h) => { + this.requests.total++; + request.events.once('disconnect', () => { + this.requests.disconnects++; + }); + return h.continue; + }); + this.server.events.on('response', request => { + const statusCode = (request.response as ResponseObject)?.statusCode; + if (statusCode) { + if (!this.requests.statusCodes[statusCode]) { + this.requests.statusCodes[statusCode] = 0; + } + this.requests.statusCodes[statusCode]++; + } + + const duration = Date.now() - request.info.received; + this.responseTimes.count++; + this.responseTimes.total += duration; + this.responseTimes.max = Math.max(this.responseTimes.max, duration); + }); + } + + public async collect(): Promise { + const connections = await new Promise(resolve => { + this.server.listener.getConnections((_, count) => { + resolve(count); + }); + }); + + return { + requests: this.requests, + response_times: { + avg_in_millis: this.responseTimes.total / Math.max(this.responseTimes.count, 1), + max_in_millis: this.responseTimes.max, + }, + concurrent_connections: connections, + }; + } +} diff --git a/src/core/server/metrics/collectors/types.ts b/src/core/server/metrics/collectors/types.ts new file mode 100644 index 000000000000..5a83bc70af3c --- /dev/null +++ b/src/core/server/metrics/collectors/types.ts @@ -0,0 +1,110 @@ +/* + * 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. + */ + +/** Base interface for all metrics gatherers */ +export interface MetricsCollector { + collect(): Promise; +} + +/** + * Process related metrics + * @public + */ +export interface OpsProcessMetrics { + /** process memory usage */ + memory: { + /** heap memory usage */ + heap: { + /** total heap available */ + total_in_bytes: number; + /** used heap */ + used_in_bytes: number; + /** v8 heap size limit */ + size_limit: number; + }; + /** node rss */ + resident_set_size_in_bytes: number; + }; + /** node event loop delay */ + event_loop_delay: number; + /** pid of the kibana process */ + pid: number; + /** uptime of the kibana process */ + uptime_in_millis: number; +} + +/** + * OS related metrics + * @public + */ +export interface OpsOsMetrics { + /** The os platform */ + platform: NodeJS.Platform; + /** The os platform release, prefixed by the platform name */ + platformRelease: string; + /** The os distrib. Only present for linux platforms */ + distro?: string; + /** The os distrib release, prefixed by the os distrib. Only present for linux platforms */ + distroRelease?: string; + /** cpu load metrics */ + load: { + /** load for last minute */ + '1m': number; + /** load for last 5 minutes */ + '5m': number; + /** load for last 15 minutes */ + '15m': number; + }; + /** system memory usage metrics */ + memory: { + /** total memory available */ + total_in_bytes: number; + /** current free memory */ + free_in_bytes: number; + /** current used memory */ + used_in_bytes: number; + }; + /** the OS uptime */ + uptime_in_millis: number; +} + +/** + * server related metrics + * @public + */ +export interface OpsServerMetrics { + /** server response time stats */ + response_times: { + /** average response time */ + avg_in_millis: number; + /** maximum response time */ + max_in_millis: number; + }; + /** server requests stats */ + requests: { + /** number of disconnected requests since startup */ + disconnects: number; + /** total number of requests handled since startup */ + total: number; + /** number of request handled per response status code */ + statusCodes: Record; + }; + /** number of current concurrent connections to the server */ + concurrent_connections: number; +} diff --git a/src/core/server/metrics/index.ts b/src/core/server/metrics/index.ts new file mode 100644 index 000000000000..fdcf637c0cd7 --- /dev/null +++ b/src/core/server/metrics/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { + InternalMetricsServiceStart, + InternalMetricsServiceSetup, + MetricsServiceSetup, + MetricsServiceStart, + OpsMetrics, +} from './types'; +export { OpsProcessMetrics, OpsServerMetrics, OpsOsMetrics } from './collectors'; +export { MetricsService } from './metrics_service'; +export { opsConfig } from './ops_config'; diff --git a/src/core/server/metrics/integration_tests/server_collector.test.ts b/src/core/server/metrics/integration_tests/server_collector.test.ts new file mode 100644 index 000000000000..a387de80212d --- /dev/null +++ b/src/core/server/metrics/integration_tests/server_collector.test.ts @@ -0,0 +1,183 @@ +/* + * 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 { Subject } from 'rxjs'; +import { take } from 'rxjs/operators'; +import supertest from 'supertest'; +import { Server as HapiServer } from 'hapi'; +import { createHttpServer } from '../../http/test_utils'; +import { HttpService, IRouter } from '../../http'; +import { contextServiceMock } from '../../context/context_service.mock'; +import { ServerMetricsCollector } from '../collectors/server'; + +describe('ServerMetricsCollector', () => { + let server: HttpService; + let collector: ServerMetricsCollector; + let hapiServer: HapiServer; + let router: IRouter; + + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)); + const sendGet = (path: string) => supertest(hapiServer.listener).get(path); + + beforeEach(async () => { + server = createHttpServer(); + const contextSetup = contextServiceMock.createSetupContract(); + const httpSetup = await server.setup({ context: contextSetup }); + hapiServer = httpSetup.server; + router = httpSetup.createRouter('/'); + collector = new ServerMetricsCollector(hapiServer); + }); + + afterEach(async () => { + await server.stop(); + }); + + it('collect requests infos', async () => { + router.get({ path: '/', validate: false }, async (ctx, req, res) => { + return res.ok({ body: '' }); + }); + await server.start(); + + let metrics = await collector.collect(); + + expect(metrics.requests).toEqual({ + total: 0, + disconnects: 0, + statusCodes: {}, + }); + + await sendGet('/'); + await sendGet('/'); + await sendGet('/not-found'); + + metrics = await collector.collect(); + + expect(metrics.requests).toEqual({ + total: 3, + disconnects: 0, + statusCodes: { + '200': 2, + '404': 1, + }, + }); + }); + + it('collect disconnects requests infos', async () => { + const never = new Promise(resolve => undefined); + + router.get({ path: '/', validate: false }, async (ctx, req, res) => { + return res.ok({ body: '' }); + }); + router.get({ path: '/disconnect', validate: false }, async (ctx, req, res) => { + await never; + return res.ok({ body: '' }); + }); + await server.start(); + + await sendGet('/'); + const discoReq1 = sendGet('/disconnect').end(); + const discoReq2 = sendGet('/disconnect').end(); + await delay(20); + + let metrics = await collector.collect(); + expect(metrics.requests).toEqual( + expect.objectContaining({ + total: 3, + disconnects: 0, + }) + ); + + discoReq1.abort(); + await delay(20); + + metrics = await collector.collect(); + expect(metrics.requests).toEqual( + expect.objectContaining({ + total: 3, + disconnects: 1, + }) + ); + + discoReq2.abort(); + await delay(20); + + metrics = await collector.collect(); + expect(metrics.requests).toEqual( + expect.objectContaining({ + total: 3, + disconnects: 2, + }) + ); + }); + + it('collect response times', async () => { + router.get({ path: '/no-delay', validate: false }, async (ctx, req, res) => { + return res.ok({ body: '' }); + }); + router.get({ path: '/500-ms', validate: false }, async (ctx, req, res) => { + await delay(500); + return res.ok({ body: '' }); + }); + router.get({ path: '/250-ms', validate: false }, async (ctx, req, res) => { + await delay(250); + return res.ok({ body: '' }); + }); + await server.start(); + + await Promise.all([sendGet('/no-delay'), sendGet('/250-ms')]); + let metrics = await collector.collect(); + + expect(metrics.response_times.avg_in_millis).toBeGreaterThanOrEqual(125); + expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(250); + + await Promise.all([sendGet('/500-ms'), sendGet('/500-ms')]); + metrics = await collector.collect(); + + expect(metrics.response_times.avg_in_millis).toBeGreaterThanOrEqual(250); + expect(metrics.response_times.max_in_millis).toBeGreaterThanOrEqual(500); + }); + + it('collect connection count', async () => { + const waitSubject = new Subject(); + + router.get({ path: '/', validate: false }, async (ctx, req, res) => { + await waitSubject.pipe(take(1)).toPromise(); + return res.ok({ body: '' }); + }); + await server.start(); + + let metrics = await collector.collect(); + expect(metrics.concurrent_connections).toEqual(0); + + sendGet('/').end(() => null); + await delay(20); + metrics = await collector.collect(); + expect(metrics.concurrent_connections).toEqual(1); + + sendGet('/').end(() => null); + await delay(20); + metrics = await collector.collect(); + expect(metrics.concurrent_connections).toEqual(2); + + waitSubject.next('go'); + await delay(20); + metrics = await collector.collect(); + expect(metrics.concurrent_connections).toEqual(0); + }); +}); diff --git a/src/core/server/metrics/metrics_service.mock.ts b/src/core/server/metrics/metrics_service.mock.ts new file mode 100644 index 000000000000..cc53a4e27d57 --- /dev/null +++ b/src/core/server/metrics/metrics_service.mock.ts @@ -0,0 +1,67 @@ +/* + * 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 { MetricsService } from './metrics_service'; +import { + InternalMetricsServiceSetup, + InternalMetricsServiceStart, + MetricsServiceSetup, + MetricsServiceStart, +} from './types'; + +const createSetupContractMock = () => { + const setupContract: jest.Mocked = { + getOpsMetrics$: jest.fn(), + }; + return setupContract; +}; + +const createInternalSetupContractMock = () => { + const setupContract: jest.Mocked = createSetupContractMock(); + return setupContract; +}; + +const createStartContractMock = () => { + const startContract: jest.Mocked = {}; + return startContract; +}; + +const createInternalStartContractMock = () => { + const startContract: jest.Mocked = createStartContractMock(); + return startContract; +}; + +type MetricsServiceContract = PublicMethodsOf; + +const createMock = () => { + const mocked: jest.Mocked = { + setup: jest.fn().mockReturnValue(createInternalSetupContractMock()), + start: jest.fn().mockReturnValue(createInternalStartContractMock()), + stop: jest.fn(), + }; + return mocked; +}; + +export const metricsServiceMock = { + create: createMock, + createSetupContract: createSetupContractMock, + createStartContract: createStartContractMock, + createInternalSetupContract: createInternalSetupContractMock, + createInternalStartContract: createInternalStartContractMock, +}; diff --git a/src/core/server/metrics/metrics_service.test.mocks.ts b/src/core/server/metrics/metrics_service.test.mocks.ts new file mode 100644 index 000000000000..8e9177528304 --- /dev/null +++ b/src/core/server/metrics/metrics_service.test.mocks.ts @@ -0,0 +1,25 @@ +/* + * 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 const mockOpsCollector = { + collect: jest.fn(), +}; +jest.doMock('./ops_metrics_collector', () => ({ + OpsMetricsCollector: jest.fn().mockImplementation(() => mockOpsCollector), +})); diff --git a/src/core/server/metrics/metrics_service.test.ts b/src/core/server/metrics/metrics_service.test.ts new file mode 100644 index 000000000000..10d6761adbe7 --- /dev/null +++ b/src/core/server/metrics/metrics_service.test.ts @@ -0,0 +1,134 @@ +/* + * 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 moment from 'moment'; +import { mockOpsCollector } from './metrics_service.test.mocks'; +import { MetricsService } from './metrics_service'; +import { mockCoreContext } from '../core_context.mock'; +import { configServiceMock } from '../config/config_service.mock'; +import { httpServiceMock } from '../http/http_service.mock'; +import { take } from 'rxjs/operators'; + +const testInterval = 100; + +const dummyMetrics = { metricA: 'value', metricB: 'otherValue' }; + +describe('MetricsService', () => { + const httpMock = httpServiceMock.createSetupContract(); + let metricsService: MetricsService; + + beforeEach(() => { + jest.useFakeTimers(); + + const configService = configServiceMock.create({ + atPath: { interval: moment.duration(testInterval) }, + }); + const coreContext = mockCoreContext.create({ configService }); + metricsService = new MetricsService(coreContext); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.clearAllTimers(); + }); + + describe('#start', () => { + it('invokes setInterval with the configured interval', async () => { + await metricsService.setup({ http: httpMock }); + await metricsService.start(); + + expect(setInterval).toHaveBeenCalledTimes(1); + expect(setInterval).toHaveBeenCalledWith(expect.any(Function), testInterval); + }); + + it('emits the metrics at start', async () => { + mockOpsCollector.collect.mockResolvedValue(dummyMetrics); + + const { getOpsMetrics$ } = await metricsService.setup({ + http: httpMock, + }); + + await metricsService.start(); + + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); + expect( + await getOpsMetrics$() + .pipe(take(1)) + .toPromise() + ).toEqual(dummyMetrics); + }); + + it('collects the metrics at every interval', async () => { + mockOpsCollector.collect.mockResolvedValue(dummyMetrics); + + await metricsService.setup({ http: httpMock }); + + await metricsService.start(); + + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2); + + jest.advanceTimersByTime(testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(3); + }); + + it('throws when called before setup', async () => { + await expect(metricsService.start()).rejects.toThrowErrorMatchingInlineSnapshot( + `"#setup() needs to be run first"` + ); + }); + }); + + describe('#stop', () => { + it('stops the metrics interval', async () => { + const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); + await metricsService.start(); + + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(1); + + jest.advanceTimersByTime(testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2); + + await metricsService.stop(); + jest.advanceTimersByTime(10 * testInterval); + expect(mockOpsCollector.collect).toHaveBeenCalledTimes(2); + + getOpsMetrics$().subscribe({ complete: () => {} }); + }); + + it('completes the metrics observable', async () => { + const { getOpsMetrics$ } = await metricsService.setup({ http: httpMock }); + await metricsService.start(); + + let completed = false; + + getOpsMetrics$().subscribe({ + complete: () => { + completed = true; + }, + }); + + await metricsService.stop(); + + expect(completed).toEqual(true); + }); + }); +}); diff --git a/src/core/server/metrics/metrics_service.ts b/src/core/server/metrics/metrics_service.ts new file mode 100644 index 000000000000..1aed89a4aad6 --- /dev/null +++ b/src/core/server/metrics/metrics_service.ts @@ -0,0 +1,86 @@ +/* + * 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 { ReplaySubject } from 'rxjs'; +import { first, shareReplay } from 'rxjs/operators'; +import { CoreService } from '../../types'; +import { CoreContext } from '../core_context'; +import { Logger } from '../logging'; +import { InternalHttpServiceSetup } from '../http'; +import { InternalMetricsServiceSetup, InternalMetricsServiceStart, OpsMetrics } from './types'; +import { OpsMetricsCollector } from './ops_metrics_collector'; +import { opsConfig, OpsConfigType } from './ops_config'; + +interface MetricsServiceSetupDeps { + http: InternalHttpServiceSetup; +} + +/** @internal */ +export class MetricsService + implements CoreService { + private readonly logger: Logger; + private metricsCollector?: OpsMetricsCollector; + private collectInterval?: NodeJS.Timeout; + private metrics$ = new ReplaySubject(1); + + constructor(private readonly coreContext: CoreContext) { + this.logger = coreContext.logger.get('metrics'); + } + + public async setup({ http }: MetricsServiceSetupDeps): Promise { + this.metricsCollector = new OpsMetricsCollector(http.server); + + const metricsObservable = this.metrics$.pipe(shareReplay(1)); + + return { + getOpsMetrics$: () => metricsObservable, + }; + } + + public async start(): Promise { + if (!this.metricsCollector) { + throw new Error('#setup() needs to be run first'); + } + const config = await this.coreContext.configService + .atPath(opsConfig.path) + .pipe(first()) + .toPromise(); + + await this.refreshMetrics(); + + this.collectInterval = setInterval(() => { + this.refreshMetrics(); + }, config.interval.asMilliseconds()); + + return {}; + } + + private async refreshMetrics() { + this.logger.debug('Refreshing metrics'); + const metrics = await this.metricsCollector!.collect(); + this.metrics$.next(metrics); + } + + public async stop() { + if (this.collectInterval) { + clearInterval(this.collectInterval); + } + this.metrics$.complete(); + } +} diff --git a/src/core/server/metrics/ops_config.ts b/src/core/server/metrics/ops_config.ts new file mode 100644 index 000000000000..bd6ae5cc5474 --- /dev/null +++ b/src/core/server/metrics/ops_config.ts @@ -0,0 +1,29 @@ +/* + * 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, TypeOf } from '@kbn/config-schema'; + +export const opsConfig = { + path: 'ops', + schema: schema.object({ + interval: schema.duration({ defaultValue: '5s' }), + }), +}; + +export type OpsConfigType = TypeOf; diff --git a/src/core/server/metrics/ops_metrics_collector.test.mocks.ts b/src/core/server/metrics/ops_metrics_collector.test.mocks.ts new file mode 100644 index 000000000000..8265796d5797 --- /dev/null +++ b/src/core/server/metrics/ops_metrics_collector.test.mocks.ts @@ -0,0 +1,39 @@ +/* + * 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 const mockOsCollector = { + collect: jest.fn(), +}; +jest.doMock('./collectors/os', () => ({ + OsMetricsCollector: jest.fn().mockImplementation(() => mockOsCollector), +})); + +export const mockProcessCollector = { + collect: jest.fn(), +}; +jest.doMock('./collectors/process', () => ({ + ProcessMetricsCollector: jest.fn().mockImplementation(() => mockProcessCollector), +})); + +export const mockServerCollector = { + collect: jest.fn(), +}; +jest.doMock('./collectors/server', () => ({ + ServerMetricsCollector: jest.fn().mockImplementation(() => mockServerCollector), +})); diff --git a/src/core/server/metrics/ops_metrics_collector.test.ts b/src/core/server/metrics/ops_metrics_collector.test.ts new file mode 100644 index 000000000000..04302a195fb6 --- /dev/null +++ b/src/core/server/metrics/ops_metrics_collector.test.ts @@ -0,0 +1,59 @@ +/* + * 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 { + mockOsCollector, + mockProcessCollector, + mockServerCollector, +} from './ops_metrics_collector.test.mocks'; +import { httpServiceMock } from '../http/http_service.mock'; +import { OpsMetricsCollector } from './ops_metrics_collector'; + +describe('OpsMetricsCollector', () => { + let collector: OpsMetricsCollector; + + beforeEach(() => { + const hapiServer = httpServiceMock.createSetupContract().server; + collector = new OpsMetricsCollector(hapiServer); + + mockOsCollector.collect.mockResolvedValue('osMetrics'); + }); + + it('gathers metrics from the underlying collectors', async () => { + mockOsCollector.collect.mockResolvedValue('osMetrics'); + mockProcessCollector.collect.mockResolvedValue('processMetrics'); + mockServerCollector.collect.mockResolvedValue({ + requests: 'serverRequestsMetrics', + response_times: 'serverTimingMetrics', + }); + + const metrics = await collector.collect(); + + expect(mockOsCollector.collect).toHaveBeenCalledTimes(1); + expect(mockProcessCollector.collect).toHaveBeenCalledTimes(1); + expect(mockServerCollector.collect).toHaveBeenCalledTimes(1); + + expect(metrics).toEqual({ + process: 'processMetrics', + os: 'osMetrics', + requests: 'serverRequestsMetrics', + response_times: 'serverTimingMetrics', + }); + }); +}); diff --git a/src/core/server/metrics/ops_metrics_collector.ts b/src/core/server/metrics/ops_metrics_collector.ts new file mode 100644 index 000000000000..04344f21f57f --- /dev/null +++ b/src/core/server/metrics/ops_metrics_collector.ts @@ -0,0 +1,52 @@ +/* + * 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 { Server as HapiServer } from 'hapi'; +import { + ProcessMetricsCollector, + OsMetricsCollector, + ServerMetricsCollector, + MetricsCollector, +} from './collectors'; +import { OpsMetrics } from './types'; + +export class OpsMetricsCollector implements MetricsCollector { + private readonly processCollector: ProcessMetricsCollector; + private readonly osCollector: OsMetricsCollector; + private readonly serverCollector: ServerMetricsCollector; + + constructor(server: HapiServer) { + this.processCollector = new ProcessMetricsCollector(); + this.osCollector = new OsMetricsCollector(); + this.serverCollector = new ServerMetricsCollector(server); + } + + public async collect(): Promise { + const [process, os, server] = await Promise.all([ + this.processCollector.collect(), + this.osCollector.collect(), + this.serverCollector.collect(), + ]); + return { + process, + os, + ...server, + }; + } +} diff --git a/src/core/server/metrics/types.ts b/src/core/server/metrics/types.ts new file mode 100644 index 000000000000..5c8f18fff380 --- /dev/null +++ b/src/core/server/metrics/types.ts @@ -0,0 +1,66 @@ +/* + * 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 } from 'rxjs'; +import { OpsProcessMetrics, OpsOsMetrics, OpsServerMetrics } from './collectors'; + +/** + * APIs to retrieves metrics gathered and exposed by the core platform. + * + * @public + */ +export interface MetricsServiceSetup { + /** + * Retrieve an observable emitting the {@link OpsMetrics} gathered. + * The observable will emit an initial value during core's `start` phase, and a new value every fixed interval of time, + * based on the `opts.interval` configuration property. + * + * @example + * ```ts + * core.metrics.getOpsMetrics$().subscribe(metrics => { + * // do something with the metrics + * }) + * ``` + */ + getOpsMetrics$: () => Observable; +} +// eslint-disable-next-line @typescript-eslint/no-empty-interface +export interface MetricsServiceStart {} + +export type InternalMetricsServiceSetup = MetricsServiceSetup; +export type InternalMetricsServiceStart = MetricsServiceStart; + +/** + * Regroups metrics gathered by all the collectors. + * This contains metrics about the os/runtime, the kibana process and the http server. + * + * @public + */ +export interface OpsMetrics { + /** Process related metrics */ + process: OpsProcessMetrics; + /** OS related metrics */ + os: OpsOsMetrics; + /** server response time stats */ + response_times: OpsServerMetrics['response_times']; + /** server requests stats */ + requests: OpsServerMetrics['requests']; + /** number of current concurrent connections to the server */ + concurrent_connections: OpsServerMetrics['concurrent_connections']; +} diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 96b28ab5827e..037f3bbed67e 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -30,6 +30,8 @@ import { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; import { SharedGlobalConfig } from './plugins'; import { InternalCoreSetup, InternalCoreStart } from './internal_types'; import { capabilitiesServiceMock } from './capabilities/capabilities_service.mock'; +import { metricsServiceMock } from './metrics/metrics_service.mock'; +import { uuidServiceMock } from './uuid/uuid_service.mock'; export { httpServerMock } from './http/http_server.mocks'; export { sessionStorageMock } from './http/cookie_session_storage.mocks'; @@ -40,7 +42,7 @@ export { loggingServiceMock } from './logging/logging_service.mock'; export { savedObjectsRepositoryMock } from './saved_objects/service/lib/repository.mock'; export { typeRegistryMock as savedObjectsTypeRegistryMock } from './saved_objects/saved_objects_type_registry.mock'; export { uiSettingsServiceMock } from './ui_settings/ui_settings_service.mock'; -import { uuidServiceMock } from './uuid/uuid_service.mock'; +export { metricsServiceMock } from './metrics/metrics_service.mock'; export function pluginInitializerContextConfigMock(config: T) { const globalConfig: SharedGlobalConfig = { @@ -153,6 +155,7 @@ function createInternalCoreSetupMock() { uiSettings: uiSettingsServiceMock.createSetupContract(), savedObjects: savedObjectsServiceMock.createInternalSetupContract(), uuid: uuidServiceMock.createSetupContract(), + metrics: metricsServiceMock.createInternalSetupContract(), }; return setupDeps; } diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md index 42bc1ce214b1..445ed16ec782 100644 --- a/src/core/server/server.api.md +++ b/src/core/server/server.api.md @@ -1176,6 +1176,11 @@ export interface LogRecord { timestamp: Date; } +// @public +export interface MetricsServiceSetup { + getOpsMetrics$: () => Observable; +} + // @public (undocumented) export type MIGRATION_ASSISTANCE_INDEX_ACTION = 'upgrade' | 'reindex'; @@ -1227,6 +1232,63 @@ export interface OnPreResponseToolkit { next: (responseExtensions?: OnPreResponseExtensions) => OnPreResponseResult; } +// @public +export interface OpsMetrics { + concurrent_connections: OpsServerMetrics['concurrent_connections']; + os: OpsOsMetrics; + process: OpsProcessMetrics; + requests: OpsServerMetrics['requests']; + response_times: OpsServerMetrics['response_times']; +} + +// @public +export interface OpsOsMetrics { + distro?: string; + distroRelease?: string; + load: { + '1m': number; + '5m': number; + '15m': number; + }; + memory: { + total_in_bytes: number; + free_in_bytes: number; + used_in_bytes: number; + }; + platform: NodeJS.Platform; + platformRelease: string; + uptime_in_millis: number; +} + +// @public +export interface OpsProcessMetrics { + event_loop_delay: number; + memory: { + heap: { + total_in_bytes: number; + used_in_bytes: number; + size_limit: number; + }; + resident_set_size_in_bytes: number; + }; + pid: number; + uptime_in_millis: number; +} + +// @public +export interface OpsServerMetrics { + concurrent_connections: number; + requests: { + disconnects: number; + total: number; + statusCodes: Record; + }; + response_times: { + avg_in_millis: number; + max_in_millis: number; + }; +} + // @public (undocumented) export interface PackageInfo { // (undocumented) diff --git a/src/core/server/server.test.mocks.ts b/src/core/server/server.test.mocks.ts index 038c4651ff5a..53d1b742a649 100644 --- a/src/core/server/server.test.mocks.ts +++ b/src/core/server/server.test.mocks.ts @@ -79,3 +79,9 @@ export const mockUuidService = uuidServiceMock.create(); jest.doMock('./uuid/uuid_service', () => ({ UuidService: jest.fn(() => mockUuidService), })); + +import { metricsServiceMock } from './metrics/metrics_service.mock'; +export const mockMetricsService = metricsServiceMock.create(); +jest.doMock('./metrics/metrics_service', () => ({ + MetricsService: jest.fn(() => mockMetricsService), +})); diff --git a/src/core/server/server.test.ts b/src/core/server/server.test.ts index 161dd3759a21..a4b5a9d81df2 100644 --- a/src/core/server/server.test.ts +++ b/src/core/server/server.test.ts @@ -28,6 +28,7 @@ import { mockEnsureValidConfiguration, mockUiSettingsService, mockRenderingService, + mockMetricsService, } from './server.test.mocks'; import { BehaviorSubject } from 'rxjs'; @@ -61,6 +62,7 @@ test('sets up services on "setup"', async () => { expect(mockSavedObjectsService.setup).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockRenderingService.setup).not.toHaveBeenCalled(); + expect(mockMetricsService.setup).not.toHaveBeenCalled(); await server.setup(); @@ -71,6 +73,7 @@ test('sets up services on "setup"', async () => { expect(mockSavedObjectsService.setup).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.setup).toHaveBeenCalledTimes(1); expect(mockRenderingService.setup).toHaveBeenCalledTimes(1); + expect(mockMetricsService.setup).toHaveBeenCalledTimes(1); }); test('injects legacy dependency to context#setup()', async () => { @@ -107,6 +110,7 @@ test('runs services on "start"', async () => { expect(mockLegacyService.start).not.toHaveBeenCalled(); expect(mockSavedObjectsService.start).not.toHaveBeenCalled(); expect(mockUiSettingsService.start).not.toHaveBeenCalled(); + expect(mockMetricsService.start).not.toHaveBeenCalled(); await server.start(); @@ -114,6 +118,7 @@ test('runs services on "start"', async () => { expect(mockLegacyService.start).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.start).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.start).toHaveBeenCalledTimes(1); + expect(mockMetricsService.start).toHaveBeenCalledTimes(1); }); test('does not fail on "setup" if there are unused paths detected', async () => { @@ -135,6 +140,7 @@ test('stops services on "stop"', async () => { expect(mockLegacyService.stop).not.toHaveBeenCalled(); expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); expect(mockUiSettingsService.stop).not.toHaveBeenCalled(); + expect(mockMetricsService.stop).not.toHaveBeenCalled(); await server.stop(); @@ -144,6 +150,7 @@ test('stops services on "stop"', async () => { expect(mockLegacyService.stop).toHaveBeenCalledTimes(1); expect(mockSavedObjectsService.stop).toHaveBeenCalledTimes(1); expect(mockUiSettingsService.stop).toHaveBeenCalledTimes(1); + expect(mockMetricsService.stop).toHaveBeenCalledTimes(1); }); test(`doesn't setup core services if config validation fails`, async () => { @@ -159,6 +166,7 @@ test(`doesn't setup core services if config validation fails`, async () => { expect(mockLegacyService.setup).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); expect(mockRenderingService.setup).not.toHaveBeenCalled(); + expect(mockMetricsService.setup).not.toHaveBeenCalled(); }); test(`doesn't setup core services if legacy config validation fails`, async () => { @@ -178,4 +186,5 @@ test(`doesn't setup core services if legacy config validation fails`, async () = expect(mockLegacyService.setup).not.toHaveBeenCalled(); expect(mockSavedObjectsService.stop).not.toHaveBeenCalled(); expect(mockUiSettingsService.setup).not.toHaveBeenCalled(); + expect(mockMetricsService.setup).not.toHaveBeenCalled(); }); diff --git a/src/core/server/server.ts b/src/core/server/server.ts index db2493b38d6e..8603f5fba1da 100644 --- a/src/core/server/server.ts +++ b/src/core/server/server.ts @@ -34,6 +34,7 @@ import { Logger, LoggerFactory } from './logging'; import { UiSettingsService } from './ui_settings'; import { PluginsService, config as pluginsConfig } from './plugins'; import { SavedObjectsService } from '../server/saved_objects'; +import { MetricsService, opsConfig } from './metrics'; import { config as cspConfig } from './csp'; import { config as elasticsearchConfig } from './elasticsearch'; @@ -67,6 +68,7 @@ export class Server { private readonly savedObjects: SavedObjectsService; private readonly uiSettings: UiSettingsService; private readonly uuid: UuidService; + private readonly metrics: MetricsService; private coreStart?: InternalCoreStart; @@ -89,6 +91,7 @@ export class Server { this.uiSettings = new UiSettingsService(core); this.capabilities = new CapabilitiesService(core); this.uuid = new UuidService(core); + this.metrics = new MetricsService(core); } public async setup() { @@ -137,6 +140,8 @@ export class Server { legacyPlugins, }); + const metricsSetup = await this.metrics.setup({ http: httpSetup }); + const coreSetup: InternalCoreSetup = { capabilities: capabilitiesSetup, context: contextServiceSetup, @@ -145,6 +150,7 @@ export class Server { uiSettings: uiSettingsSetup, savedObjects: savedObjectsSetup, uuid: uuidSetup, + metrics: metricsSetup, }; const pluginsSetup = await this.plugins.setup(coreSetup); @@ -193,6 +199,7 @@ export class Server { await this.http.start(); await this.rendering.start(); + await this.metrics.start(); return this.coreStart; } @@ -207,6 +214,7 @@ export class Server { await this.http.stop(); await this.uiSettings.stop(); await this.rendering.stop(); + await this.metrics.stop(); } private registerDefaultRoute(httpSetup: InternalHttpServiceSetup) { @@ -260,6 +268,7 @@ export class Server { [savedObjectsConfig.path, savedObjectsConfig.schema], [savedObjectsMigrationConfig.path, savedObjectsMigrationConfig.schema], [uiSettingsConfig.path, uiSettingsConfig.schema], + [opsConfig.path, opsConfig.schema], ]; this.configService.addDeprecationProvider(rootConfigPath, coreDeprecationProvider);