From 3e69ea5f01781d9658045169070c15c467e77ca7 Mon Sep 17 00:00:00 2001 From: Mikhail Shustov Date: Wed, 22 Jan 2020 13:34:35 +0100 Subject: [PATCH 1/3] [NP] KibanaRequest provides request abortion event (#55061) * add aborted$ observable to KibanaRequest * complete observable on request end * update docs * update test suit names * always finish subscription * address comments --- ...bana-plugin-server.kibanarequest.events.md | 13 ++ .../kibana-plugin-server.kibanarequest.md | 3 +- ...bana-plugin-server.kibanarequest.socket.md | 2 + ...gin-server.kibanarequestevents.aborted_.md | 13 ++ ...ibana-plugin-server.kibanarequestevents.md | 20 +++ .../core/server/kibana-plugin-server.md | 1 + src/core/server/http/index.ts | 1 + .../http/integration_tests/request.test.ts | 127 ++++++++++++++++++ src/core/server/http/router/index.ts | 1 + src/core/server/http/router/request.ts | 32 ++++- src/core/server/index.ts | 1 + src/core/server/server.api.md | 6 + 12 files changed, 216 insertions(+), 4 deletions(-) create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequest.events.md create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequestevents.aborted_.md create mode 100644 docs/development/core/server/kibana-plugin-server.kibanarequestevents.md create mode 100644 src/core/server/http/integration_tests/request.test.ts 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) From e828a1295492659c446d4dc894e46bba40b35c68 Mon Sep 17 00:00:00 2001 From: "Devin W. Hurley" Date: Wed, 22 Jan 2020 07:50:32 -0500 Subject: [PATCH 2/3] [SIEM] [Detection Engine] Log time gaps as failures for now (#55515) * log a failure to failure history if time gap is detected. stop-gap solution until a feature is fully fleshed out to report this and future messaging / monitoring. * write date the gap warning occurred in the last_failure_at field, along with the status_date field. --- .../signals/signal_rule_alert_type.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) 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 = From d23d8f0afd36ac97fdf944f8a885ec0605556c46 Mon Sep 17 00:00:00 2001 From: Jean-Louis Leysens Date: Wed, 22 Jan 2020 13:52:53 +0100 Subject: [PATCH 3/3] Restore 200 OK + response payload for save and delete endpoints (#55535) --- .../np_ready/routes/api/watch/register_delete_route.ts | 5 +++-- .../server/np_ready/routes/api/watch/register_save_route.ts | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_delete_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_delete_route.ts index 3402cc283dba0..26b6f96f6cb8c 100644 --- a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_delete_route.ts +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_delete_route.ts @@ -24,8 +24,9 @@ export function registerDeleteRoute(deps: RouteDependencies, legacy: ServerShim) 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/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_save_route.ts b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_save_route.ts index 5d22392d49ed8..df4117dee2bfd 100644 --- a/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_save_route.ts +++ b/x-pack/legacy/plugins/watcher/server/np_ready/routes/api/watch/register_save_route.ts @@ -76,8 +76,9 @@ export function registerSaveRoute(deps: RouteDependencies, legacy: ServerShim) { 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)) {