diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.events.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.events.md
new file mode 100644
index 0000000000000..5a002fc28f5db
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.events.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequest](./kibana-plugin-server.kibanarequest.md) > [events](./kibana-plugin-server.kibanarequest.events.md)
+
+## KibanaRequest.events property
+
+Request events [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md)
+
+Signature:
+
+```typescript
+readonly events: KibanaRequestEvents;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.md
index bc805fdc0b86f..6603de24494d5 100644
--- a/docs/development/core/server/kibana-plugin-server.kibanarequest.md
+++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.md
@@ -23,10 +23,11 @@ export declare class KibanaRequestBody | |
+| [events](./kibana-plugin-server.kibanarequest.events.md) | | KibanaRequestEvents
| Request events [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md) |
| [headers](./kibana-plugin-server.kibanarequest.headers.md) | | Headers
| Readonly copy of incoming request headers. |
| [params](./kibana-plugin-server.kibanarequest.params.md) | | Params
| |
| [query](./kibana-plugin-server.kibanarequest.query.md) | | Query
| |
| [route](./kibana-plugin-server.kibanarequest.route.md) | | RecursiveReadonly<KibanaRequestRoute<Method>>
| matched route details |
-| [socket](./kibana-plugin-server.kibanarequest.socket.md) | | IKibanaSocket
| |
+| [socket](./kibana-plugin-server.kibanarequest.socket.md) | | IKibanaSocket
| [IKibanaSocket](./kibana-plugin-server.ikibanasocket.md) |
| [url](./kibana-plugin-server.kibanarequest.url.md) | | Url
| a WHATWG URL standard object. |
diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequest.socket.md b/docs/development/core/server/kibana-plugin-server.kibanarequest.socket.md
index 3880428273ac9..c55f4656c993c 100644
--- a/docs/development/core/server/kibana-plugin-server.kibanarequest.socket.md
+++ b/docs/development/core/server/kibana-plugin-server.kibanarequest.socket.md
@@ -4,6 +4,8 @@
## KibanaRequest.socket property
+[IKibanaSocket](./kibana-plugin-server.ikibanasocket.md)
+
Signature:
```typescript
diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestevents.aborted_.md b/docs/development/core/server/kibana-plugin-server.kibanarequestevents.aborted_.md
new file mode 100644
index 0000000000000..d292d5d60bf5f
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.kibanarequestevents.aborted_.md
@@ -0,0 +1,13 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md) > [aborted$](./kibana-plugin-server.kibanarequestevents.aborted_.md)
+
+## KibanaRequestEvents.aborted$ property
+
+Observable that emits once if and when the request has been aborted.
+
+Signature:
+
+```typescript
+aborted$: Observable;
+```
diff --git a/docs/development/core/server/kibana-plugin-server.kibanarequestevents.md b/docs/development/core/server/kibana-plugin-server.kibanarequestevents.md
new file mode 100644
index 0000000000000..9137c4673a60c
--- /dev/null
+++ b/docs/development/core/server/kibana-plugin-server.kibanarequestevents.md
@@ -0,0 +1,20 @@
+
+
+[Home](./index.md) > [kibana-plugin-server](./kibana-plugin-server.md) > [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md)
+
+## KibanaRequestEvents interface
+
+Request events.
+
+Signature:
+
+```typescript
+export interface KibanaRequestEvents
+```
+
+## Properties
+
+| Property | Type | Description |
+| --- | --- | --- |
+| [aborted$](./kibana-plugin-server.kibanarequestevents.aborted_.md) | Observable<void>
| Observable that emits once if and when the request has been aborted. |
+
diff --git a/docs/development/core/server/kibana-plugin-server.md b/docs/development/core/server/kibana-plugin-server.md
index 00ab83123319a..cd469fe6a98c2 100644
--- a/docs/development/core/server/kibana-plugin-server.md
+++ b/docs/development/core/server/kibana-plugin-server.md
@@ -76,6 +76,7 @@ The plugin integrates with the core system via lifecycle events: `setup`
| [IRouter](./kibana-plugin-server.irouter.md) | Registers route handlers for specified resource path and method. See [RouteConfig](./kibana-plugin-server.routeconfig.md) and [RequestHandler](./kibana-plugin-server.requesthandler.md) for more information about arguments to route registrations. |
| [IScopedRenderingClient](./kibana-plugin-server.iscopedrenderingclient.md) | |
| [IUiSettingsClient](./kibana-plugin-server.iuisettingsclient.md) | Server-side client that provides access to the advanced settings stored in elasticsearch. The settings provide control over the behavior of the Kibana application. For example, a user can specify how to display numeric or date fields. Users can adjust the settings via Management UI. |
+| [KibanaRequestEvents](./kibana-plugin-server.kibanarequestevents.md) | Request events. |
| [KibanaRequestRoute](./kibana-plugin-server.kibanarequestroute.md) | Request specific route information exposed to a handler. |
| [LegacyRequest](./kibana-plugin-server.legacyrequest.md) | |
| [LegacyServiceSetupDeps](./kibana-plugin-server.legacyservicesetupdeps.md) | |
diff --git a/src/core/server/http/index.ts b/src/core/server/http/index.ts
index 55ba813484268..d31afe1670e41 100644
--- a/src/core/server/http/index.ts
+++ b/src/core/server/http/index.ts
@@ -29,6 +29,7 @@ export {
HttpResponsePayload,
ErrorHttpResponseOptions,
KibanaRequest,
+ KibanaRequestEvents,
KibanaRequestRoute,
KibanaRequestRouteOptions,
IKibanaResponse,
diff --git a/src/core/server/http/integration_tests/request.test.ts b/src/core/server/http/integration_tests/request.test.ts
new file mode 100644
index 0000000000000..bc1bbc881315a
--- /dev/null
+++ b/src/core/server/http/integration_tests/request.test.ts
@@ -0,0 +1,127 @@
+/*
+ * 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 supertest from 'supertest';
+
+import { HttpService } from '../http_service';
+
+import { contextServiceMock } from '../../context/context_service.mock';
+import { loggingServiceMock } from '../../logging/logging_service.mock';
+import { createHttpServer } from '../test_utils';
+
+let server: HttpService;
+
+let logger: ReturnType;
+const contextSetup = contextServiceMock.createSetupContract();
+
+const setupDeps = {
+ context: contextSetup,
+};
+
+beforeEach(() => {
+ logger = loggingServiceMock.create();
+
+ server = createHttpServer({ logger });
+});
+
+afterEach(async () => {
+ await server.stop();
+});
+
+const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
+describe('KibanaRequest', () => {
+ describe('events', () => {
+ describe('aborted$', () => {
+ it('emits once and completes when request aborted', async done => {
+ expect.assertions(1);
+ const { server: innerServer, createRouter } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ const nextSpy = jest.fn();
+ router.get({ path: '/', validate: false }, async (context, request, res) => {
+ request.events.aborted$.subscribe({
+ next: nextSpy,
+ complete: () => {
+ expect(nextSpy).toHaveBeenCalledTimes(1);
+ done();
+ },
+ });
+
+ // prevents the server to respond
+ await delay(30000);
+ return res.ok({ body: 'ok' });
+ });
+
+ await server.start();
+
+ const incomingRequest = supertest(innerServer.listener)
+ .get('/')
+ // end required to send request
+ .end();
+
+ setTimeout(() => incomingRequest.abort(), 50);
+ });
+
+ it('completes & does not emit when request handled', async () => {
+ const { server: innerServer, createRouter } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ const nextSpy = jest.fn();
+ const completeSpy = jest.fn();
+ router.get({ path: '/', validate: false }, async (context, request, res) => {
+ request.events.aborted$.subscribe({
+ next: nextSpy,
+ complete: completeSpy,
+ });
+
+ return res.ok({ body: 'ok' });
+ });
+
+ await server.start();
+
+ await supertest(innerServer.listener).get('/');
+
+ expect(nextSpy).toHaveBeenCalledTimes(0);
+ expect(completeSpy).toHaveBeenCalledTimes(1);
+ });
+
+ it('completes & does not emit when request rejected', async () => {
+ const { server: innerServer, createRouter } = await server.setup(setupDeps);
+ const router = createRouter('/');
+
+ const nextSpy = jest.fn();
+ const completeSpy = jest.fn();
+ router.get({ path: '/', validate: false }, async (context, request, res) => {
+ request.events.aborted$.subscribe({
+ next: nextSpy,
+ complete: completeSpy,
+ });
+
+ return res.badRequest();
+ });
+
+ await server.start();
+
+ await supertest(innerServer.listener).get('/');
+
+ expect(nextSpy).toHaveBeenCalledTimes(0);
+ expect(completeSpy).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+});
diff --git a/src/core/server/http/router/index.ts b/src/core/server/http/router/index.ts
index 084d30d694474..32663d1513f36 100644
--- a/src/core/server/http/router/index.ts
+++ b/src/core/server/http/router/index.ts
@@ -21,6 +21,7 @@ export { Headers, filterHeaders, ResponseHeaders, KnownHeaders } from './headers
export { Router, RequestHandler, IRouter, RouteRegistrar } from './router';
export {
KibanaRequest,
+ KibanaRequestEvents,
KibanaRequestRoute,
KibanaRequestRouteOptions,
isRealRequest,
diff --git a/src/core/server/http/router/request.ts b/src/core/server/http/router/request.ts
index 47b001700b015..22fb2d2643d1c 100644
--- a/src/core/server/http/router/request.ts
+++ b/src/core/server/http/router/request.ts
@@ -19,6 +19,8 @@
import { Url } from 'url';
import { Request } from 'hapi';
+import { Observable, fromEvent, merge } from 'rxjs';
+import { shareReplay, first, takeUntil } from 'rxjs/operators';
import { deepFreeze, RecursiveReadonly } from '../../../utils';
import { Headers } from './headers';
@@ -46,6 +48,17 @@ export interface KibanaRequestRoute {
options: KibanaRequestRouteOptions;
}
+/**
+ * Request events.
+ * @public
+ * */
+export interface KibanaRequestEvents {
+ /**
+ * Observable that emits once if and when the request has been aborted.
+ */
+ aborted$: Observable;
+}
+
/**
* @deprecated
* `hapi` request object, supported during migration process only for backward compatibility.
@@ -115,7 +128,10 @@ export class KibanaRequest<
*/
public readonly headers: Headers;
+ /** {@link IKibanaSocket} */
public readonly socket: IKibanaSocket;
+ /** Request events {@link KibanaRequestEvents} */
+ public readonly events: KibanaRequestEvents;
/** @internal */
protected readonly [requestSymbol]: Request;
@@ -138,12 +154,22 @@ export class KibanaRequest<
enumerable: false,
});
- this.route = deepFreeze(this.getRouteInfo());
+ this.route = deepFreeze(this.getRouteInfo(request));
this.socket = new KibanaSocket(request.raw.req.socket);
+ this.events = this.getEvents(request);
+ }
+
+ private getEvents(request: Request): KibanaRequestEvents {
+ const finish$ = merge(
+ fromEvent(request.raw.req, 'end'), // all data consumed
+ fromEvent(request.raw.req, 'close') // connection was closed
+ ).pipe(shareReplay(1), first());
+ return {
+ aborted$: fromEvent(request.raw.req, 'aborted').pipe(first(), takeUntil(finish$)),
+ } as const;
}
- private getRouteInfo(): KibanaRequestRoute {
- const request = this[requestSymbol];
+ private getRouteInfo(request: Request): KibanaRequestRoute {
const method = request.method as Method;
const { parse, maxBytes, allow, output } = request.route.settings.payload || {};
diff --git a/src/core/server/index.ts b/src/core/server/index.ts
index 3f67b9a656bb7..a97d2970dca88 100644
--- a/src/core/server/index.ts
+++ b/src/core/server/index.ts
@@ -109,6 +109,7 @@ export {
IKibanaSocket,
IsAuthenticated,
KibanaRequest,
+ KibanaRequestEvents,
KibanaRequestRoute,
KibanaRequestRouteOptions,
IKibanaResponse,
diff --git a/src/core/server/server.api.md b/src/core/server/server.api.md
index 6e41a4aefba30..dce5ec64bfa66 100644
--- a/src/core/server/server.api.md
+++ b/src/core/server/server.api.md
@@ -879,6 +879,7 @@ export class KibanaRequest;
+}
+
// @public
export interface KibanaRequestRoute {
// (undocumented)
diff --git a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts
index b19e4f48fdb3e..143fad602daea 100644
--- a/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts
+++ b/x-pack/legacy/plugins/siem/server/lib/detection_engine/signals/signal_rule_alert_type.ts
@@ -134,6 +134,27 @@ export const signalRulesAlertType = ({
logger.warn(
`Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.`
);
+ // write a failure status whenever we have a time gap
+ // this is a temporary solution until general activity
+ // monitoring is developed as a feature
+ const gapDate = new Date().toISOString();
+ await services.savedObjectsClient.create(ruleStatusSavedObjectType, {
+ alertId,
+ statusDate: gapDate,
+ status: 'failed',
+ lastFailureAt: gapDate,
+ lastSuccessAt: currentStatusSavedObject.attributes.lastSuccessAt,
+ lastFailureMessage: `Signal rule name: "${name}", id: "${alertId}", rule_id: "${ruleId}" has a time gap of ${gap.humanize()} (${gap.asMilliseconds()}ms), and could be missing signals within that time. Consider increasing your look behind time or adding more Kibana instances.`,
+ lastSuccessMessage: currentStatusSavedObject.attributes.lastSuccessMessage,
+ });
+
+ if (ruleStatusSavedObjects.saved_objects.length >= 6) {
+ // delete fifth status and prepare to insert a newer one.
+ const toDelete = ruleStatusSavedObjects.saved_objects.slice(5);
+ await toDelete.forEach(async item =>
+ services.savedObjectsClient.delete(ruleStatusSavedObjectType, item.id)
+ );
+ }
}
// set searchAfter page size to be the lesser of default page size or maxSignals.
const searchAfterSize =
diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts
index 4094daf0cd27c..6e95cf959bc9c 100644
--- a/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watch/register_delete_route.ts
@@ -24,8 +24,9 @@ export function registerDeleteRoute(deps: RouteDependencies) {
const { watchId } = request.params;
try {
- await deleteWatch(callWithRequest, watchId);
- return response.noContent();
+ return response.ok({
+ body: await deleteWatch(callWithRequest, watchId),
+ });
} catch (e) {
// Case: Error from Elasticsearch JS client
if (isEsError(e)) {
diff --git a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts
index 1c2195d57e1e1..7986424e6229a 100644
--- a/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts
+++ b/x-pack/plugins/watcher/server/routes/api/watch/register_save_route.ts
@@ -73,8 +73,9 @@ export function registerSaveRoute(deps: RouteDependencies) {
try {
// Create new watch
- await saveWatch(callWithRequest, id, serializedWatch);
- return response.noContent();
+ return response.ok({
+ body: await saveWatch(callWithRequest, id, serializedWatch),
+ });
} catch (e) {
// Case: Error from Elasticsearch JS client
if (isEsError(e)) {