diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md
index 15a1fd05062568..c948c899207968 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 00000000000000..454b8c905451ee
--- /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 00000000000000..270c56402a390d
--- /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 00000000000000..cfd39a551ad349
--- /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 00000000000000..e23bd8d431d3fb
--- /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 00000000000000..993a1d7a2d7b79
--- /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 00000000000000..53d3a33d66e06a
--- /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 00000000000000..9cd6b85e507f0c
--- /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 00000000000000..358699071b1c3b
--- /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 00000000000000..338164f173d025
--- /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 00000000000000..24c5a1f00b64c9
--- /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 00000000000000..0bf17502ce34e4
--- /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 00000000000000..0fb4e59fdf539a
--- /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 00000000000000..4a1becaeeaec71
--- /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 00000000000000..411d0fc546dc0b
--- /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 00000000000000..1071b4a38f5880
--- /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 00000000000000..dfff1a1f1da0ba
--- /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 00000000000000..f61c8b0995324d
--- /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 00000000000000..92fd8471cce7da
--- /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 00000000000000..5c1a8de70dc019
--- /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 00000000000000..a34187f3720188
--- /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 00000000000000..24db2f017a663e
--- /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 00000000000000..ade79fedfa1b5c
--- /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 00000000000000..4e35c02bd9f28a
--- /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 00000000000000..5ad2abc8695577
--- /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 00000000000000..5008efc6ad4da2
--- /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 e727d87a83c537..2c401724c72cd0 100644
--- a/package.json
+++ b/package.json
@@ -323,6 +323,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/graphql": "^0.13.2",
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index e45d4f28edcc37..de6cdb2d7acd78 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 ff68d1544d119e..37d1061dc618dc 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 46436461505c06..50468db8a504de 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 00000000000000..f58ab02e638813
--- /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 00000000000000..7d5a6da90b7d62
--- /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 00000000000000..d3d9bb0be86fa2
--- /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 00000000000000..a437d799371f11
--- /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 00000000000000..aa68abaf74e41a
--- /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 00000000000000..e46ac2f653df66
--- /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 00000000000000..5a83bc70af3c11
--- /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 00000000000000..fdcf637c0cd7b9
--- /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 00000000000000..a387de80212d91
--- /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 00000000000000..cc53a4e27d5710
--- /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 00000000000000..8e91775283042b
--- /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 00000000000000..10d6761adbe7d5
--- /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 00000000000000..1aed89a4aad600
--- /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 00000000000000..bd6ae5cc5474d7
--- /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 00000000000000..8265796d57970e
--- /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 00000000000000..04302a195fb6cd
--- /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 00000000000000..04344f21f57f72
--- /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 00000000000000..5c8f18fff380de
--- /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 96b28ab5827e1b..037f3bbed67e06 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 42bc1ce214b191..445ed16ec7829b 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 038c4651ff5a7d..53d1b742a64944 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 161dd3759a218d..a4b5a9d81df203 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 db2493b38d6e0a..8603f5fba1da87 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);