mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[eventLog] search for actions/alerts as hidden saved objects (#70395)
resolves https://github.com/elastic/kibana/issues/70086 Configures the saved object client for the event log to access the recently hidden action and alert saved objects. We didn't have tests for action/alert event log activity, so added some now. Also found a buglet that was preventing access to event log data from actions and alerts in non-default spaces.
This commit is contained in:
parent
fbf54f0023
commit
b167d77e3e
40 changed files with 1248 additions and 393 deletions
|
@ -3,6 +3,7 @@
|
|||
"version": "0.0.1",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "eventLog"],
|
||||
"optionalPlugins": ["spaces"],
|
||||
"server": true,
|
||||
"ui": false
|
||||
}
|
||||
|
|
|
@ -4,10 +4,9 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { LegacyClusterClient, Logger } from '../../../../../src/core/server';
|
||||
import { elasticsearchServiceMock, loggingSystemMock } from '../../../../../src/core/server/mocks';
|
||||
import { LegacyClusterClient, Logger } from 'src/core/server';
|
||||
import { elasticsearchServiceMock, loggingSystemMock } from 'src/core/server/mocks';
|
||||
import { ClusterClientAdapter, IClusterClientAdapter } from './cluster_client_adapter';
|
||||
import moment from 'moment';
|
||||
import { findOptionsSchema } from '../event_log_client';
|
||||
|
||||
type EsClusterClient = Pick<jest.Mocked<LegacyClusterClient>, 'callAsInternalUser' | 'asScoped'>;
|
||||
|
@ -205,7 +204,7 @@ describe('createIndex', () => {
|
|||
describe('queryEventsBySavedObject', () => {
|
||||
const DEFAULT_OPTIONS = findOptionsSchema.validate({});
|
||||
|
||||
test('should call cluster with proper arguments', async () => {
|
||||
test('should call cluster with proper arguments with non-default namespace', async () => {
|
||||
clusterClient.callAsInternalUser.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [],
|
||||
|
@ -214,6 +213,7 @@ describe('queryEventsBySavedObject', () => {
|
|||
});
|
||||
await clusterClientAdapter.queryEventsBySavedObject(
|
||||
'index-name',
|
||||
'namespace',
|
||||
'saved-object-type',
|
||||
'saved-object-id',
|
||||
DEFAULT_OPTIONS
|
||||
|
@ -221,52 +221,147 @@ describe('queryEventsBySavedObject', () => {
|
|||
|
||||
const [method, query] = clusterClient.callAsInternalUser.mock.calls[0];
|
||||
expect(method).toEqual('search');
|
||||
expect(query).toMatchObject({
|
||||
index: 'index-name',
|
||||
body: {
|
||||
from: 0,
|
||||
size: 10,
|
||||
sort: { '@timestamp': { order: 'asc' } },
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: 'primary',
|
||||
expect(query).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"from": 0,
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"must": Array [
|
||||
Object {
|
||||
"nested": Object {
|
||||
"path": "kibana.saved_objects",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"must": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.rel": Object {
|
||||
"value": "primary",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: 'saved-object-type',
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.type": Object {
|
||||
"value": "saved-object-type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.id': {
|
||||
value: 'saved-object-id',
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.id": Object {
|
||||
"value": "saved-object-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.namespace": Object {
|
||||
"value": "namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 10,
|
||||
"sort": Object {
|
||||
"@timestamp": Object {
|
||||
"order": "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
"index": "index-name",
|
||||
"rest_total_hits_as_int": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('should call cluster with proper arguments with default namespace', async () => {
|
||||
clusterClient.callAsInternalUser.mockResolvedValue({
|
||||
hits: {
|
||||
hits: [],
|
||||
total: { value: 0 },
|
||||
},
|
||||
});
|
||||
await clusterClientAdapter.queryEventsBySavedObject(
|
||||
'index-name',
|
||||
undefined,
|
||||
'saved-object-type',
|
||||
'saved-object-id',
|
||||
DEFAULT_OPTIONS
|
||||
);
|
||||
|
||||
const [method, query] = clusterClient.callAsInternalUser.mock.calls[0];
|
||||
expect(method).toEqual('search');
|
||||
expect(query).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"from": 0,
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"must": Array [
|
||||
Object {
|
||||
"nested": Object {
|
||||
"path": "kibana.saved_objects",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"must": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.rel": Object {
|
||||
"value": "primary",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.type": Object {
|
||||
"value": "saved-object-type",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.id": Object {
|
||||
"value": "saved-object-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"bool": Object {
|
||||
"must_not": Object {
|
||||
"exists": Object {
|
||||
"field": "kibana.saved_objects.namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 10,
|
||||
"sort": Object {
|
||||
"@timestamp": Object {
|
||||
"order": "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
"index": "index-name",
|
||||
"rest_total_hits_as_int": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('should call cluster with sort', async () => {
|
||||
|
@ -278,6 +373,7 @@ describe('queryEventsBySavedObject', () => {
|
|||
});
|
||||
await clusterClientAdapter.queryEventsBySavedObject(
|
||||
'index-name',
|
||||
'namespace',
|
||||
'saved-object-type',
|
||||
'saved-object-id',
|
||||
{ ...DEFAULT_OPTIONS, sort_field: 'event.end', sort_order: 'desc' }
|
||||
|
@ -301,10 +397,11 @@ describe('queryEventsBySavedObject', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const start = moment().subtract(1, 'days').toISOString();
|
||||
const start = '2020-07-08T00:52:28.350Z';
|
||||
|
||||
await clusterClientAdapter.queryEventsBySavedObject(
|
||||
'index-name',
|
||||
'namespace',
|
||||
'saved-object-type',
|
||||
'saved-object-id',
|
||||
{ ...DEFAULT_OPTIONS, start }
|
||||
|
@ -312,56 +409,73 @@ describe('queryEventsBySavedObject', () => {
|
|||
|
||||
const [method, query] = clusterClient.callAsInternalUser.mock.calls[0];
|
||||
expect(method).toEqual('search');
|
||||
expect(query).toMatchObject({
|
||||
index: 'index-name',
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: 'primary',
|
||||
expect(query).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"from": 0,
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"must": Array [
|
||||
Object {
|
||||
"nested": Object {
|
||||
"path": "kibana.saved_objects",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"must": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.rel": Object {
|
||||
"value": "primary",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: 'saved-object-type',
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.type": Object {
|
||||
"value": "saved-object-type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.id': {
|
||||
value: 'saved-object-id',
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.id": Object {
|
||||
"value": "saved-object-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.namespace": Object {
|
||||
"value": "namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: start,
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "2020-07-08T00:52:28.350Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 10,
|
||||
"sort": Object {
|
||||
"@timestamp": Object {
|
||||
"order": "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
"index": "index-name",
|
||||
"rest_total_hits_as_int": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
|
||||
test('supports optional date range', async () => {
|
||||
|
@ -372,11 +486,12 @@ describe('queryEventsBySavedObject', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const start = moment().subtract(1, 'days').toISOString();
|
||||
const end = moment().add(1, 'days').toISOString();
|
||||
const start = '2020-07-08T00:52:28.350Z';
|
||||
const end = '2020-07-08T00:00:00.000Z';
|
||||
|
||||
await clusterClientAdapter.queryEventsBySavedObject(
|
||||
'index-name',
|
||||
'namespace',
|
||||
'saved-object-type',
|
||||
'saved-object-id',
|
||||
{ ...DEFAULT_OPTIONS, start, end }
|
||||
|
@ -384,62 +499,79 @@ describe('queryEventsBySavedObject', () => {
|
|||
|
||||
const [method, query] = clusterClient.callAsInternalUser.mock.calls[0];
|
||||
expect(method).toEqual('search');
|
||||
expect(query).toMatchObject({
|
||||
index: 'index-name',
|
||||
body: {
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: 'primary',
|
||||
expect(query).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"body": Object {
|
||||
"from": 0,
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"must": Array [
|
||||
Object {
|
||||
"nested": Object {
|
||||
"path": "kibana.saved_objects",
|
||||
"query": Object {
|
||||
"bool": Object {
|
||||
"must": Array [
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.rel": Object {
|
||||
"value": "primary",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: 'saved-object-type',
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.type": Object {
|
||||
"value": "saved-object-type",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.id': {
|
||||
value: 'saved-object-id',
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.id": Object {
|
||||
"value": "saved-object-id",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
Object {
|
||||
"term": Object {
|
||||
"kibana.saved_objects.namespace": Object {
|
||||
"value": "namespace",
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: start,
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"gte": "2020-07-08T00:52:28.350Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
lte: end,
|
||||
Object {
|
||||
"range": Object {
|
||||
"@timestamp": Object {
|
||||
"lte": "2020-07-08T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
},
|
||||
"size": 10,
|
||||
"sort": Object {
|
||||
"@timestamp": Object {
|
||||
"order": "asc",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
"index": "index-name",
|
||||
"rest_total_hits_as_int": true,
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,8 +6,9 @@
|
|||
|
||||
import { reject, isUndefined } from 'lodash';
|
||||
import { SearchResponse, Client } from 'elasticsearch';
|
||||
import { Logger, LegacyClusterClient } from '../../../../../src/core/server';
|
||||
import { IEvent, SAVED_OBJECT_REL_PRIMARY } from '../types';
|
||||
import { Logger, LegacyClusterClient } from 'src/core/server';
|
||||
|
||||
import { IValidatedEvent, SAVED_OBJECT_REL_PRIMARY } from '../types';
|
||||
import { FindOptionsType } from '../event_log_client';
|
||||
|
||||
export type EsClusterClient = Pick<LegacyClusterClient, 'callAsInternalUser' | 'asScoped'>;
|
||||
|
@ -22,7 +23,7 @@ export interface QueryEventsBySavedObjectResult {
|
|||
page: number;
|
||||
per_page: number;
|
||||
total: number;
|
||||
data: IEvent[];
|
||||
data: IValidatedEvent[];
|
||||
}
|
||||
|
||||
export class ClusterClientAdapter {
|
||||
|
@ -129,10 +130,91 @@ export class ClusterClientAdapter {
|
|||
|
||||
public async queryEventsBySavedObject(
|
||||
index: string,
|
||||
namespace: string | undefined,
|
||||
type: string,
|
||||
id: string,
|
||||
{ page, per_page: perPage, start, end, sort_field, sort_order }: FindOptionsType
|
||||
): Promise<QueryEventsBySavedObjectResult> {
|
||||
const defaultNamespaceQuery = {
|
||||
bool: {
|
||||
must_not: {
|
||||
exists: {
|
||||
field: 'kibana.saved_objects.namespace',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const namedNamespaceQuery = {
|
||||
term: {
|
||||
'kibana.saved_objects.namespace': {
|
||||
value: namespace,
|
||||
},
|
||||
},
|
||||
};
|
||||
const namespaceQuery = namespace === undefined ? defaultNamespaceQuery : namedNamespaceQuery;
|
||||
|
||||
const body = {
|
||||
size: perPage,
|
||||
from: (page - 1) * perPage,
|
||||
sort: { [sort_field]: { order: sort_order } },
|
||||
query: {
|
||||
bool: {
|
||||
must: reject(
|
||||
[
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: SAVED_OBJECT_REL_PRIMARY,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: type,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.id': {
|
||||
value: id,
|
||||
},
|
||||
},
|
||||
},
|
||||
namespaceQuery,
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
start && {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: start,
|
||||
},
|
||||
},
|
||||
},
|
||||
end && {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
lte: end,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
isUndefined
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const {
|
||||
hits: { hits, total },
|
||||
|
@ -141,72 +223,13 @@ export class ClusterClientAdapter {
|
|||
// The SearchResponse type only supports total as an int,
|
||||
// so we're forced to explicitly request that it return as an int
|
||||
rest_total_hits_as_int: true,
|
||||
body: {
|
||||
size: perPage,
|
||||
from: (page - 1) * perPage,
|
||||
sort: { [sort_field]: { order: sort_order } },
|
||||
query: {
|
||||
bool: {
|
||||
must: reject(
|
||||
[
|
||||
{
|
||||
nested: {
|
||||
path: 'kibana.saved_objects',
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.rel': {
|
||||
value: SAVED_OBJECT_REL_PRIMARY,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.type': {
|
||||
value: type,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
'kibana.saved_objects.id': {
|
||||
value: id,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
start && {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: start,
|
||||
},
|
||||
},
|
||||
},
|
||||
end && {
|
||||
range: {
|
||||
'@timestamp': {
|
||||
lte: end,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
isUndefined
|
||||
),
|
||||
},
|
||||
},
|
||||
},
|
||||
body,
|
||||
});
|
||||
return {
|
||||
page,
|
||||
per_page: perPage,
|
||||
total,
|
||||
data: hits.map((hit) => hit._source) as IEvent[],
|
||||
data: hits.map((hit) => hit._source) as IValidatedEvent[],
|
||||
};
|
||||
} catch (err) {
|
||||
throw new Error(
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { loggingSystemMock } from 'src/core/server/mocks';
|
||||
|
||||
import { EsContext } from './context';
|
||||
import { namesMock } from './names.mock';
|
||||
import { IClusterClientAdapter } from './cluster_client_adapter';
|
||||
import { loggingSystemMock } from '../../../../../src/core/server/mocks';
|
||||
import { clusterClientAdapterMock } from './cluster_client_adapter.mock';
|
||||
|
||||
const createContextMock = () => {
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import { EventLogClient } from './event_log_client';
|
||||
import { contextMock } from './es/context.mock';
|
||||
import { savedObjectsClientMock } from 'src/core/server/mocks';
|
||||
|
@ -18,6 +19,7 @@ describe('EventLogStart', () => {
|
|||
const eventLogClient = new EventLogClient({
|
||||
esContext,
|
||||
savedObjectsClient,
|
||||
request: FakeRequest(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
|
@ -38,6 +40,7 @@ describe('EventLogStart', () => {
|
|||
const eventLogClient = new EventLogClient({
|
||||
esContext,
|
||||
savedObjectsClient,
|
||||
request: FakeRequest(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockRejectedValue(new Error('Fail'));
|
||||
|
@ -53,6 +56,7 @@ describe('EventLogStart', () => {
|
|||
const eventLogClient = new EventLogClient({
|
||||
esContext,
|
||||
savedObjectsClient,
|
||||
request: FakeRequest(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
|
@ -107,6 +111,7 @@ describe('EventLogStart', () => {
|
|||
|
||||
expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith(
|
||||
esContext.esNames.alias,
|
||||
undefined,
|
||||
'saved-object-type',
|
||||
'saved-object-id',
|
||||
{
|
||||
|
@ -124,6 +129,7 @@ describe('EventLogStart', () => {
|
|||
const eventLogClient = new EventLogClient({
|
||||
esContext,
|
||||
savedObjectsClient,
|
||||
request: FakeRequest(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
|
@ -184,6 +190,7 @@ describe('EventLogStart', () => {
|
|||
|
||||
expect(esContext.esAdapter.queryEventsBySavedObject).toHaveBeenCalledWith(
|
||||
esContext.esNames.alias,
|
||||
undefined,
|
||||
'saved-object-type',
|
||||
'saved-object-id',
|
||||
{
|
||||
|
@ -203,6 +210,7 @@ describe('EventLogStart', () => {
|
|||
const eventLogClient = new EventLogClient({
|
||||
esContext,
|
||||
savedObjectsClient,
|
||||
request: FakeRequest(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
|
@ -232,6 +240,7 @@ describe('EventLogStart', () => {
|
|||
const eventLogClient = new EventLogClient({
|
||||
esContext,
|
||||
savedObjectsClient,
|
||||
request: FakeRequest(),
|
||||
});
|
||||
|
||||
savedObjectsClient.get.mockResolvedValueOnce({
|
||||
|
@ -286,3 +295,22 @@ function fakeEvent(overrides = {}) {
|
|||
overrides
|
||||
);
|
||||
}
|
||||
|
||||
function FakeRequest(): KibanaRequest {
|
||||
const savedObjectsClient = savedObjectsClientMock.create();
|
||||
return ({
|
||||
headers: {},
|
||||
getBasePath: () => '',
|
||||
path: '/',
|
||||
route: { settings: {} },
|
||||
url: {
|
||||
href: '/',
|
||||
},
|
||||
raw: {
|
||||
req: {
|
||||
url: '/',
|
||||
},
|
||||
},
|
||||
getSavedObjectsClient: () => savedObjectsClient,
|
||||
} as unknown) as KibanaRequest;
|
||||
}
|
||||
|
|
|
@ -5,20 +5,16 @@
|
|||
*/
|
||||
|
||||
import { Observable } from 'rxjs';
|
||||
import { LegacyClusterClient, SavedObjectsClientContract } from 'src/core/server';
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { LegacyClusterClient, SavedObjectsClientContract, KibanaRequest } from 'src/core/server';
|
||||
import { SpacesServiceSetup } from '../../spaces/server';
|
||||
|
||||
import { EsContext } from './es';
|
||||
import { IEventLogClient } from './types';
|
||||
import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter';
|
||||
export type PluginClusterClient = Pick<LegacyClusterClient, 'callAsInternalUser' | 'asScoped'>;
|
||||
export type AdminClusterClient$ = Observable<PluginClusterClient>;
|
||||
|
||||
interface EventLogServiceCtorParams {
|
||||
esContext: EsContext;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
}
|
||||
|
||||
const optionalDateFieldSchema = schema.maybe(
|
||||
schema.string({
|
||||
validate(value) {
|
||||
|
@ -60,14 +56,30 @@ export type FindOptionsType = Pick<
|
|||
> &
|
||||
Partial<TypeOf<typeof findOptionsSchema>>;
|
||||
|
||||
interface EventLogServiceCtorParams {
|
||||
esContext: EsContext;
|
||||
savedObjectsClient: SavedObjectsClientContract;
|
||||
spacesService?: SpacesServiceSetup;
|
||||
request: KibanaRequest;
|
||||
}
|
||||
|
||||
// note that clusterClient may be null, indicating we can't write to ES
|
||||
export class EventLogClient implements IEventLogClient {
|
||||
private esContext: EsContext;
|
||||
private savedObjectsClient: SavedObjectsClientContract;
|
||||
private spacesService?: SpacesServiceSetup;
|
||||
private request: KibanaRequest;
|
||||
|
||||
constructor({ esContext, savedObjectsClient }: EventLogServiceCtorParams) {
|
||||
constructor({
|
||||
esContext,
|
||||
savedObjectsClient,
|
||||
spacesService,
|
||||
request,
|
||||
}: EventLogServiceCtorParams) {
|
||||
this.esContext = esContext;
|
||||
this.savedObjectsClient = savedObjectsClient;
|
||||
this.spacesService = spacesService;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
async findEventsBySavedObject(
|
||||
|
@ -75,13 +87,20 @@ export class EventLogClient implements IEventLogClient {
|
|||
id: string,
|
||||
options?: Partial<FindOptionsType>
|
||||
): Promise<QueryEventsBySavedObjectResult> {
|
||||
const findOptions = findOptionsSchema.validate(options ?? {});
|
||||
|
||||
const space = await this.spacesService?.getActiveSpace(this.request);
|
||||
const namespace = space && this.spacesService?.spaceIdToNamespace(space.id);
|
||||
|
||||
// verify the user has the required permissions to view this saved object
|
||||
await this.savedObjectsClient.get(type, id);
|
||||
|
||||
return await this.esContext.esAdapter.queryEventsBySavedObject(
|
||||
this.esContext.esNames.alias,
|
||||
namespace,
|
||||
type,
|
||||
id,
|
||||
findOptionsSchema.validate(options ?? {})
|
||||
findOptions
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,10 +4,11 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
import { savedObjectsClientMock, savedObjectsServiceMock } from 'src/core/server/mocks';
|
||||
|
||||
import { EventLogClientService } from './event_log_start_service';
|
||||
import { contextMock } from './es/context.mock';
|
||||
import { KibanaRequest } from 'kibana/server';
|
||||
import { savedObjectsClientMock, savedObjectsServiceMock } from 'src/core/server/mocks';
|
||||
|
||||
jest.mock('./event_log_client');
|
||||
|
||||
|
@ -26,13 +27,8 @@ describe('EventLogClientService', () => {
|
|||
|
||||
eventLogStartService.getClient(request);
|
||||
|
||||
expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request);
|
||||
|
||||
const [{ value: savedObjectsClient }] = savedObjectsService.getScopedClient.mock.results;
|
||||
|
||||
expect(jest.requireMock('./event_log_client').EventLogClient).toHaveBeenCalledWith({
|
||||
esContext,
|
||||
savedObjectsClient,
|
||||
expect(savedObjectsService.getScopedClient).toHaveBeenCalledWith(request, {
|
||||
includedHiddenTypes: ['action', 'alert'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
SavedObjectsServiceStart,
|
||||
SavedObjectsClientContract,
|
||||
} from 'src/core/server';
|
||||
import { SpacesServiceSetup } from '../../spaces/server';
|
||||
|
||||
import { EsContext } from './es';
|
||||
import { IEventLogClientService } from './types';
|
||||
|
@ -18,30 +19,37 @@ import { EventLogClient } from './event_log_client';
|
|||
export type PluginClusterClient = Pick<LegacyClusterClient, 'callAsInternalUser' | 'asScoped'>;
|
||||
export type AdminClusterClient$ = Observable<PluginClusterClient>;
|
||||
|
||||
const includedHiddenTypes = ['action', 'alert'];
|
||||
|
||||
interface EventLogServiceCtorParams {
|
||||
esContext: EsContext;
|
||||
savedObjectsService: SavedObjectsServiceStart;
|
||||
spacesService?: SpacesServiceSetup;
|
||||
}
|
||||
|
||||
// note that clusterClient may be null, indicating we can't write to ES
|
||||
export class EventLogClientService implements IEventLogClientService {
|
||||
private esContext: EsContext;
|
||||
private savedObjectsService: SavedObjectsServiceStart;
|
||||
private spacesService?: SpacesServiceSetup;
|
||||
|
||||
constructor({ esContext, savedObjectsService }: EventLogServiceCtorParams) {
|
||||
constructor({ esContext, savedObjectsService, spacesService }: EventLogServiceCtorParams) {
|
||||
this.esContext = esContext;
|
||||
this.savedObjectsService = savedObjectsService;
|
||||
this.spacesService = spacesService;
|
||||
}
|
||||
|
||||
getClient(
|
||||
request: KibanaRequest,
|
||||
savedObjectsClient: SavedObjectsClientContract = this.savedObjectsService.getScopedClient(
|
||||
request
|
||||
)
|
||||
) {
|
||||
getClient(request: KibanaRequest) {
|
||||
const savedObjectsClient: SavedObjectsClientContract = this.savedObjectsService.getScopedClient(
|
||||
request,
|
||||
{ includedHiddenTypes }
|
||||
);
|
||||
|
||||
return new EventLogClient({
|
||||
esContext: this.esContext,
|
||||
savedObjectsClient,
|
||||
spacesService: this.spacesService,
|
||||
request,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ export {
|
|||
IEventLogger,
|
||||
IEventLogClientService,
|
||||
IEvent,
|
||||
IValidatedEvent,
|
||||
SAVED_OBJECT_REL_PRIMARY,
|
||||
} from './types';
|
||||
export const config = { schema: ConfigSchema };
|
||||
|
|
|
@ -17,6 +17,7 @@ import {
|
|||
IContextProvider,
|
||||
RequestHandler,
|
||||
} from 'src/core/server';
|
||||
import { SpacesPluginSetup, SpacesServiceSetup } from '../../spaces/server';
|
||||
|
||||
import {
|
||||
IEventLogConfig,
|
||||
|
@ -39,14 +40,19 @@ const ACTIONS = {
|
|||
stopping: 'stopping',
|
||||
};
|
||||
|
||||
interface PluginSetupDeps {
|
||||
spaces?: SpacesPluginSetup;
|
||||
}
|
||||
|
||||
export class Plugin implements CorePlugin<IEventLogService, IEventLogClientService> {
|
||||
private readonly config$: IEventLogConfig$;
|
||||
private systemLogger: Logger;
|
||||
private eventLogService?: IEventLogService;
|
||||
private eventLogService?: EventLogService;
|
||||
private esContext?: EsContext;
|
||||
private eventLogger?: IEventLogger;
|
||||
private globalConfig$: Observable<SharedGlobalConfig>;
|
||||
private eventLogClientService?: EventLogClientService;
|
||||
private spacesService?: SpacesServiceSetup;
|
||||
|
||||
constructor(private readonly context: PluginInitializerContext) {
|
||||
this.systemLogger = this.context.logger.get();
|
||||
|
@ -54,13 +60,14 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi
|
|||
this.globalConfig$ = this.context.config.legacy.globalConfig$;
|
||||
}
|
||||
|
||||
async setup(core: CoreSetup): Promise<IEventLogService> {
|
||||
async setup(core: CoreSetup, { spaces }: PluginSetupDeps): Promise<IEventLogService> {
|
||||
const globalConfig = await this.globalConfig$.pipe(first()).toPromise();
|
||||
const kibanaIndex = globalConfig.kibana.index;
|
||||
|
||||
this.systemLogger.debug('setting up plugin');
|
||||
|
||||
const config = await this.config$.pipe(first()).toPromise();
|
||||
this.spacesService = spaces?.spacesService;
|
||||
|
||||
this.esContext = createEsContext({
|
||||
logger: this.systemLogger,
|
||||
|
@ -89,7 +96,7 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi
|
|||
// Routes
|
||||
const router = core.http.createRouter();
|
||||
// Register routes
|
||||
findRoute(router);
|
||||
findRoute(router, this.systemLogger);
|
||||
|
||||
return this.eventLogService;
|
||||
}
|
||||
|
@ -115,6 +122,7 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi
|
|||
this.eventLogClientService = new EventLogClientService({
|
||||
esContext: this.esContext,
|
||||
savedObjectsService: core.savedObjects,
|
||||
spacesService: this.spacesService,
|
||||
});
|
||||
return this.eventLogClientService;
|
||||
}
|
||||
|
@ -125,8 +133,7 @@ export class Plugin implements CorePlugin<IEventLogService, IEventLogClientServi
|
|||
> => {
|
||||
return async (context, request) => {
|
||||
return {
|
||||
getEventLogClient: () =>
|
||||
this.eventLogClientService!.getClient(request, context.core.savedObjects.client),
|
||||
getEventLogClient: () => this.eventLogClientService!.getClient(request),
|
||||
};
|
||||
};
|
||||
};
|
||||
|
|
|
@ -4,9 +4,10 @@
|
|||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'kibana/server';
|
||||
import { identity, merge } from 'lodash';
|
||||
import { httpServerMock } from '../../../../../src/core/server/mocks';
|
||||
import { RequestHandlerContext, KibanaRequest, KibanaResponseFactory } from 'src/core/server';
|
||||
|
||||
import { httpServerMock } from 'src/core/server/mocks';
|
||||
import { IEventLogClient } from '../types';
|
||||
|
||||
export function mockHandlerArguments(
|
||||
|
|
|
@ -8,8 +8,10 @@ import { findRoute } from './find';
|
|||
import { httpServiceMock } from 'src/core/server/mocks';
|
||||
import { mockHandlerArguments, fakeEvent } from './_mock_handler_arguments';
|
||||
import { eventLogClientMock } from '../event_log_client.mock';
|
||||
import { loggingSystemMock } from 'src/core/server/mocks';
|
||||
|
||||
const eventLogClient = eventLogClientMock.create();
|
||||
const systemLogger = loggingSystemMock.createLogger();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
@ -19,7 +21,7 @@ describe('find', () => {
|
|||
it('finds events with proper parameters', async () => {
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
findRoute(router);
|
||||
findRoute(router, systemLogger);
|
||||
|
||||
const [config, handler] = router.get.mock.calls[0];
|
||||
|
||||
|
@ -58,7 +60,7 @@ describe('find', () => {
|
|||
it('supports optional pagination parameters', async () => {
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
findRoute(router);
|
||||
findRoute(router, systemLogger);
|
||||
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
eventLogClient.findEventsBySavedObject.mockResolvedValueOnce({
|
||||
|
@ -95,4 +97,29 @@ describe('find', () => {
|
|||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('logs a warning when the query throws an error', async () => {
|
||||
const router = httpServiceMock.createRouter();
|
||||
|
||||
findRoute(router, systemLogger);
|
||||
|
||||
const [, handler] = router.get.mock.calls[0];
|
||||
eventLogClient.findEventsBySavedObject.mockRejectedValueOnce(new Error('oof!'));
|
||||
|
||||
const [context, req, res] = mockHandlerArguments(
|
||||
eventLogClient,
|
||||
{
|
||||
params: { id: '1', type: 'action' },
|
||||
query: { page: 3, per_page: 10 },
|
||||
},
|
||||
['ok']
|
||||
);
|
||||
|
||||
await handler(context, req, res);
|
||||
|
||||
expect(systemLogger.debug).toHaveBeenCalledTimes(1);
|
||||
expect(systemLogger.debug).toHaveBeenCalledWith(
|
||||
'error calling eventLog findEventsBySavedObject(action, 1, {"page":3,"per_page":10}): oof!'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -11,7 +11,9 @@ import {
|
|||
KibanaRequest,
|
||||
IKibanaResponse,
|
||||
KibanaResponseFactory,
|
||||
} from 'kibana/server';
|
||||
Logger,
|
||||
} from 'src/core/server';
|
||||
|
||||
import { BASE_EVENT_LOG_API_PATH } from '../../common';
|
||||
import { findOptionsSchema, FindOptionsType } from '../event_log_client';
|
||||
|
||||
|
@ -20,7 +22,7 @@ const paramSchema = schema.object({
|
|||
id: schema.string(),
|
||||
});
|
||||
|
||||
export const findRoute = (router: IRouter) => {
|
||||
export const findRoute = (router: IRouter, systemLogger: Logger) => {
|
||||
router.get(
|
||||
{
|
||||
path: `${BASE_EVENT_LOG_API_PATH}/{type}/{id}/_find`,
|
||||
|
@ -42,9 +44,16 @@ export const findRoute = (router: IRouter) => {
|
|||
params: { id, type },
|
||||
query,
|
||||
} = req;
|
||||
return res.ok({
|
||||
body: await eventLogClient.findEventsBySavedObject(type, id, query),
|
||||
});
|
||||
|
||||
try {
|
||||
return res.ok({
|
||||
body: await eventLogClient.findEventsBySavedObject(type, id, query),
|
||||
});
|
||||
} catch (err) {
|
||||
const call = `findEventsBySavedObject(${type}, ${id}, ${JSON.stringify(query)})`;
|
||||
systemLogger.debug(`error calling eventLog ${call}: ${err.message}`);
|
||||
return res.notFound();
|
||||
}
|
||||
})
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
|
||||
import { Observable } from 'rxjs';
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { KibanaRequest } from 'src/core/server';
|
||||
|
||||
export { IEvent, IValidatedEvent, EventSchema, ECS_VERSION } from '../generated/schemas';
|
||||
import { KibanaRequest } from 'kibana/server';
|
||||
import { IEvent } from '../generated/schemas';
|
||||
import { FindOptionsType } from './event_log_client';
|
||||
import { QueryEventsBySavedObjectResult } from './es/cluster_client_adapter';
|
||||
|
|
|
@ -9,11 +9,8 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function serverLogTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('server-log action', () => {
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
it('should return 200 when creating a server-log action', async () => {
|
||||
await supertest
|
||||
.post('/api/actions/action')
|
||||
|
|
|
@ -33,6 +33,7 @@ const enabledActionTypes = [
|
|||
'test.index-record',
|
||||
'test.noop',
|
||||
'test.rate-limit',
|
||||
'test.throw',
|
||||
];
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
|
|
|
@ -24,6 +24,14 @@ export function defineActionTypes(
|
|||
return { status: 'ok', actionId: '' };
|
||||
},
|
||||
};
|
||||
const throwActionType: ActionType = {
|
||||
id: 'test.throw',
|
||||
name: 'Test: Throw',
|
||||
minimumLicenseRequired: 'gold',
|
||||
async executor() {
|
||||
throw new Error('this action is intended to fail');
|
||||
},
|
||||
};
|
||||
const indexRecordActionType: ActionType = {
|
||||
id: 'test.index-record',
|
||||
name: 'Test: Index Record',
|
||||
|
@ -193,6 +201,7 @@ export function defineActionTypes(
|
|||
},
|
||||
};
|
||||
actions.registerType(noopActionType);
|
||||
actions.registerType(throwActionType);
|
||||
actions.registerType(indexRecordActionType);
|
||||
actions.registerType(failingActionType);
|
||||
actions.registerType(rateLimitedActionType);
|
||||
|
|
|
@ -286,6 +286,50 @@ export function defineAlertTypes(
|
|||
},
|
||||
async executor(opts: AlertExecutorOptions) {},
|
||||
};
|
||||
const throwAlertType: AlertType = {
|
||||
id: 'test.throw',
|
||||
name: 'Test: Throw',
|
||||
actionGroups: [
|
||||
{
|
||||
id: 'default',
|
||||
name: 'Default',
|
||||
},
|
||||
],
|
||||
producer: 'alerting',
|
||||
defaultActionGroupId: 'default',
|
||||
async executor({ services, params, state }: AlertExecutorOptions) {
|
||||
throw new Error('this alert is intended to fail');
|
||||
},
|
||||
};
|
||||
const patternFiringAlertType: AlertType = {
|
||||
id: 'test.patternFiring',
|
||||
name: 'Test: Firing on a Pattern',
|
||||
actionGroups: [{ id: 'default', name: 'Default' }],
|
||||
producer: 'alerting',
|
||||
defaultActionGroupId: 'default',
|
||||
async executor(alertExecutorOptions: AlertExecutorOptions) {
|
||||
const { services, state, params } = alertExecutorOptions;
|
||||
const pattern = params.pattern;
|
||||
if (!Array.isArray(pattern)) throw new Error('pattern is not an array');
|
||||
if (pattern.length === 0) throw new Error('pattern is empty');
|
||||
|
||||
// get the pattern index, return if past it
|
||||
const patternIndex = state.patternIndex ?? 0;
|
||||
if (patternIndex > pattern.length) {
|
||||
return { patternIndex };
|
||||
}
|
||||
|
||||
// fire if pattern says to
|
||||
if (pattern[patternIndex]) {
|
||||
services.alertInstanceFactory('instance').scheduleActions('default');
|
||||
}
|
||||
|
||||
return {
|
||||
patternIndex: (patternIndex + 1) % pattern.length,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
alerts.registerType(alwaysFiringAlertType);
|
||||
alerts.registerType(cumulativeFiringAlertType);
|
||||
alerts.registerType(neverFiringAlertType);
|
||||
|
@ -295,4 +339,6 @@ export function defineAlertTypes(
|
|||
alerts.registerType(noopAlertType);
|
||||
alerts.registerType(onlyContextVariablesAlertType);
|
||||
alerts.registerType(onlyStateVariablesAlertType);
|
||||
alerts.registerType(patternFiringAlertType);
|
||||
alerts.registerType(throwAlertType);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import { IValidatedEvent } from '../../../../plugins/event_log/server';
|
||||
import { getUrlPrefix } from '.';
|
||||
import { FtrProviderContext } from '../ftr_provider_context';
|
||||
|
||||
interface GetEventLogParams {
|
||||
getService: FtrProviderContext['getService'];
|
||||
spaceId: string;
|
||||
type: string;
|
||||
id: string;
|
||||
provider: string;
|
||||
actions: string[];
|
||||
}
|
||||
|
||||
// Return event log entries given the specified parameters; for the `actions`
|
||||
// parameter, at least one event of each action must be in the returned entries.
|
||||
export async function getEventLog(params: GetEventLogParams): Promise<IValidatedEvent[]> {
|
||||
const { getService, spaceId, type, id, provider, actions } = params;
|
||||
const supertest = getService('supertest');
|
||||
|
||||
const spacePrefix = getUrlPrefix(spaceId);
|
||||
const url = `${spacePrefix}/api/event_log/${type}/${id}/_find`;
|
||||
|
||||
const { body: result } = await supertest.get(url).expect(200);
|
||||
if (!result.total) {
|
||||
throw new Error('no events found yet');
|
||||
}
|
||||
|
||||
const events: IValidatedEvent[] = (result.data as IValidatedEvent[]).filter(
|
||||
(event) => event?.event?.provider === provider
|
||||
);
|
||||
const foundActions = new Set(
|
||||
events.map((event) => event?.event?.action).filter((event) => !!event)
|
||||
);
|
||||
|
||||
for (const action of actions) {
|
||||
if (!foundActions.has(action)) {
|
||||
throw new Error(`no event found with action "${action}"`);
|
||||
}
|
||||
}
|
||||
|
||||
return events;
|
||||
}
|
|
@ -12,3 +12,4 @@ export { AlertUtils } from './alert_utils';
|
|||
export { TaskManagerUtils } from './task_manager_utils';
|
||||
export * from './test_assertions';
|
||||
export { checkAAD } from './check_aad';
|
||||
export { getEventLog } from './get_event_log';
|
||||
|
|
|
@ -11,11 +11,8 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function emailTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('create email action', () => {
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
let createdActionId = '';
|
||||
|
||||
it('should return 200 when creating an email action successfully', async () => {
|
||||
|
|
|
@ -14,10 +14,8 @@ const ES_TEST_INDEX_NAME = 'functional-test-actions-index';
|
|||
export default function indexTest({ getService }: FtrProviderContext) {
|
||||
const es = getService('legacyEs');
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('index action', () => {
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
beforeEach(() => clearTestIndex(es));
|
||||
|
||||
let createdActionID: string;
|
||||
|
|
|
@ -16,10 +16,8 @@ const ES_TEST_INDEX_NAME = 'functional-test-actions-index-preconfigured';
|
|||
export default function indexTest({ getService }: FtrProviderContext) {
|
||||
const es = getService('legacyEs');
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('preconfigured index action', () => {
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
beforeEach(() => clearTestIndex(es));
|
||||
|
||||
it('should execute successfully when expected for a single body', async () => {
|
||||
|
|
|
@ -34,7 +34,6 @@ const mapping = [
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function jiraTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
const mockJira = {
|
||||
|
@ -82,8 +81,6 @@ export default function jiraTest({ getService }: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
describe('Jira - Action Creation', () => {
|
||||
it('should return 200 when creating a jira action successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function pagerdutyTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('pagerduty action', () => {
|
||||
|
@ -30,8 +29,6 @@ export default function pagerdutyTest({ getService }: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
it('should return successfully when passed valid create parameters', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
|
|
|
@ -34,7 +34,6 @@ const mapping = [
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function resilientTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
const mockResilient = {
|
||||
|
@ -82,8 +81,6 @@ export default function resilientTest({ getService }: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
describe('IBM Resilient - Action Creation', () => {
|
||||
it('should return 200 when creating a ibm resilient action successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
|
|
|
@ -11,11 +11,8 @@ import { FtrProviderContext } from '../../../../common/ftr_provider_context';
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function serverLogTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('server-log action', () => {
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
let serverLogActionId: string;
|
||||
|
||||
it('should return 200 when creating a builtin server-log action', async () => {
|
||||
|
|
|
@ -34,7 +34,6 @@ const mapping = [
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function servicenowTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
const mockServiceNow = {
|
||||
|
@ -81,8 +80,6 @@ export default function servicenowTest({ getService }: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
describe('ServiceNow - Action Creation', () => {
|
||||
it('should return 200 when creating a servicenow action successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
|
|
|
@ -16,7 +16,6 @@ import {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function slackTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
describe('slack action', () => {
|
||||
|
@ -30,8 +29,6 @@ export default function slackTest({ getService }: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
it('should return 200 when creating a slack action successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
|
|
|
@ -27,7 +27,6 @@ function parsePort(url: Record<string, string>): Record<string, string | null |
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function webhookTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
async function createWebhookAction(
|
||||
|
@ -71,8 +70,6 @@ export default function webhookTest({ getService }: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
it('should return 200 when creating a webhook action successfully', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post('/api/actions/action')
|
||||
|
|
|
@ -11,8 +11,12 @@ import {
|
|||
ES_TEST_INDEX_NAME,
|
||||
getUrlPrefix,
|
||||
ObjectRemover,
|
||||
getEventLog,
|
||||
} from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { IValidatedEvent } from '../../../../../plugins/event_log/server';
|
||||
|
||||
const NANOS_IN_MILLIS = 1000 * 1000;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
|
@ -107,6 +111,13 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
reference,
|
||||
source: 'action:test.index-record',
|
||||
});
|
||||
|
||||
await validateEventLog({
|
||||
spaceId: space.id,
|
||||
actionId: createdAction.id,
|
||||
outcome: 'success',
|
||||
message: `action executed: test.index-record:${createdAction.id}: My action`,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
|
@ -480,4 +491,66 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
interface ValidateEventLogParams {
|
||||
spaceId: string;
|
||||
actionId: string;
|
||||
outcome: string;
|
||||
message: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
async function validateEventLog(params: ValidateEventLogParams): Promise<void> {
|
||||
const { spaceId, actionId, outcome, message, errorMessage } = params;
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId,
|
||||
type: 'action',
|
||||
id: actionId,
|
||||
provider: 'actions',
|
||||
actions: ['execute'],
|
||||
});
|
||||
});
|
||||
|
||||
expect(events.length).to.equal(1);
|
||||
|
||||
const event = events[0];
|
||||
|
||||
const duration = event?.event?.duration;
|
||||
const eventStart = Date.parse(event?.event?.start || 'undefined');
|
||||
const eventEnd = Date.parse(event?.event?.end || 'undefined');
|
||||
const dateNow = Date.now();
|
||||
|
||||
expect(typeof duration).to.be('number');
|
||||
expect(eventStart).to.be.ok();
|
||||
expect(eventEnd).to.be.ok();
|
||||
|
||||
const durationDiff = Math.abs(
|
||||
Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart)
|
||||
);
|
||||
|
||||
// account for rounding errors
|
||||
expect(durationDiff < 1).to.equal(true);
|
||||
expect(eventStart <= eventEnd).to.equal(true);
|
||||
expect(eventEnd <= dateNow).to.equal(true);
|
||||
|
||||
expect(event?.event?.outcome).to.equal(outcome);
|
||||
|
||||
expect(event?.kibana?.saved_objects).to.eql([
|
||||
{
|
||||
rel: 'primary',
|
||||
type: 'action',
|
||||
id: actionId,
|
||||
namespace: spaceId,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(event?.message).to.eql(message);
|
||||
|
||||
if (errorMessage) {
|
||||
expect(event?.error?.message).to.eql(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,11 @@ import {
|
|||
ObjectRemover,
|
||||
AlertUtils,
|
||||
TaskManagerUtils,
|
||||
getEventLog,
|
||||
} from '../../../common/lib';
|
||||
import { IValidatedEvent } from '../../../../../plugins/event_log/server';
|
||||
|
||||
const NANOS_IN_MILLIS = 1000 * 1000;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function alertTests({ getService }: FtrProviderContext) {
|
||||
|
@ -159,6 +163,13 @@ instanceStateValue: true
|
|||
});
|
||||
|
||||
await taskManagerUtils.waitForActionTaskParamsToBeCleanedUp(testStart);
|
||||
|
||||
await validateEventLog({
|
||||
spaceId: space.id,
|
||||
alertId,
|
||||
outcome: 'success',
|
||||
message: `alert executed: test.always-firing:${alertId}: 'abc'`,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Scenario untested: ${JSON.stringify(scenario)}`);
|
||||
|
@ -927,4 +938,66 @@ instanceStateValue: true
|
|||
});
|
||||
}
|
||||
});
|
||||
|
||||
interface ValidateEventLogParams {
|
||||
spaceId: string;
|
||||
alertId: string;
|
||||
outcome: string;
|
||||
message: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
async function validateEventLog(params: ValidateEventLogParams): Promise<void> {
|
||||
const { spaceId, alertId, outcome, message, errorMessage } = params;
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId,
|
||||
type: 'alert',
|
||||
id: alertId,
|
||||
provider: 'alerting',
|
||||
actions: ['execute'],
|
||||
});
|
||||
});
|
||||
|
||||
expect(events.length).to.be.greaterThan(0);
|
||||
|
||||
const event = events[0];
|
||||
|
||||
const duration = event?.event?.duration;
|
||||
const eventStart = Date.parse(event?.event?.start || 'undefined');
|
||||
const eventEnd = Date.parse(event?.event?.end || 'undefined');
|
||||
const dateNow = Date.now();
|
||||
|
||||
expect(typeof duration).to.be('number');
|
||||
expect(eventStart).to.be.ok();
|
||||
expect(eventEnd).to.be.ok();
|
||||
|
||||
const durationDiff = Math.abs(
|
||||
Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart)
|
||||
);
|
||||
|
||||
// account for rounding errors
|
||||
expect(durationDiff < 1).to.equal(true);
|
||||
expect(eventStart <= eventEnd).to.equal(true);
|
||||
expect(eventEnd <= dateNow).to.equal(true);
|
||||
|
||||
expect(event?.event?.outcome).to.equal(outcome);
|
||||
|
||||
expect(event?.kibana?.saved_objects).to.eql([
|
||||
{
|
||||
rel: 'primary',
|
||||
type: 'alert',
|
||||
id: alertId,
|
||||
namespace: spaceId,
|
||||
},
|
||||
]);
|
||||
|
||||
expect(event?.message).to.eql(message);
|
||||
|
||||
if (errorMessage) {
|
||||
expect(event?.error?.message).to.eql(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,10 +14,8 @@ const ES_TEST_INDEX_NAME = 'functional-test-actions-index';
|
|||
export default function indexTest({ getService }: FtrProviderContext) {
|
||||
const es = getService('legacyEs');
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('index action', () => {
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
beforeEach(() => clearTestIndex(es));
|
||||
|
||||
let createdActionID: string;
|
||||
|
|
|
@ -15,7 +15,6 @@ import {
|
|||
// eslint-disable-next-line import/no-default-export
|
||||
export default function webhookTest({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const esArchiver = getService('esArchiver');
|
||||
const kibanaServer = getService('kibanaServer');
|
||||
|
||||
async function createWebhookAction(
|
||||
|
@ -55,8 +54,6 @@ export default function webhookTest({ getService }: FtrProviderContext) {
|
|||
);
|
||||
});
|
||||
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
|
||||
it('webhook can be executed without username and password', async () => {
|
||||
const webhookActionId = await createWebhookAction(webhookSimulatorURL);
|
||||
const { body: result } = await supertest
|
||||
|
|
|
@ -11,8 +11,12 @@ import {
|
|||
ES_TEST_INDEX_NAME,
|
||||
getUrlPrefix,
|
||||
ObjectRemover,
|
||||
getEventLog,
|
||||
} from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { IValidatedEvent } from '../../../../../plugins/event_log/server';
|
||||
|
||||
const NANOS_IN_MILLIS = 1000 * 1000;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
|
@ -86,6 +90,13 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
reference,
|
||||
source: 'action:test.index-record',
|
||||
});
|
||||
|
||||
await validateEventLog({
|
||||
spaceId: Spaces.space1.id,
|
||||
actionId: createdAction.id,
|
||||
outcome: 'success',
|
||||
message: `action executed: test.index-record:${createdAction.id}: My action`,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle failed executions', async () => {
|
||||
|
@ -118,6 +129,14 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
serviceMessage: `expected failure for ${ES_TEST_INDEX_NAME} ${reference}`,
|
||||
retry: false,
|
||||
});
|
||||
|
||||
await validateEventLog({
|
||||
spaceId: Spaces.space1.id,
|
||||
actionId: createdAction.id,
|
||||
outcome: 'failure',
|
||||
message: `action execution failure: test.failing:${createdAction.id}: failing action`,
|
||||
errorMessage: `an error occurred while running the action executor: expected failure for .kibana-alerting-test-data actions-failure-1:space1`,
|
||||
});
|
||||
});
|
||||
|
||||
it(`shouldn't execute an action from another space`, async () => {
|
||||
|
@ -198,4 +217,66 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface ValidateEventLogParams {
|
||||
spaceId: string;
|
||||
actionId: string;
|
||||
outcome: string;
|
||||
message: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
async function validateEventLog(params: ValidateEventLogParams): Promise<void> {
|
||||
const { spaceId, actionId, outcome, message, errorMessage } = params;
|
||||
|
||||
const events: IValidatedEvent[] = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId,
|
||||
type: 'action',
|
||||
id: actionId,
|
||||
provider: 'actions',
|
||||
actions: ['execute'],
|
||||
});
|
||||
});
|
||||
|
||||
expect(events.length).to.equal(1);
|
||||
|
||||
const event = events[0];
|
||||
|
||||
const duration = event?.event?.duration;
|
||||
const eventStart = Date.parse(event?.event?.start || 'undefined');
|
||||
const eventEnd = Date.parse(event?.event?.end || 'undefined');
|
||||
const dateNow = Date.now();
|
||||
|
||||
expect(typeof duration).to.be('number');
|
||||
expect(eventStart).to.be.ok();
|
||||
expect(eventEnd).to.be.ok();
|
||||
|
||||
const durationDiff = Math.abs(
|
||||
Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart)
|
||||
);
|
||||
|
||||
// account for rounding errors
|
||||
expect(durationDiff < 1).to.equal(true);
|
||||
expect(eventStart <= eventEnd).to.equal(true);
|
||||
expect(eventEnd <= dateNow).to.equal(true);
|
||||
|
||||
expect(event?.event?.outcome).to.equal(outcome);
|
||||
|
||||
expect(event?.kibana?.saved_objects).to.eql([
|
||||
{
|
||||
rel: 'primary',
|
||||
type: 'action',
|
||||
id: actionId,
|
||||
namespace: 'space1',
|
||||
},
|
||||
]);
|
||||
|
||||
expect(event?.message).to.eql(message);
|
||||
|
||||
if (errorMessage) {
|
||||
expect(event?.error?.message).to.eql(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,265 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License;
|
||||
* you may not use this file except in compliance with the Elastic License.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
import { Spaces } from '../../scenarios';
|
||||
import { getUrlPrefix, getTestAlertData, ObjectRemover, getEventLog } from '../../../common/lib';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
import { IValidatedEvent } from '../../../../../plugins/event_log/server';
|
||||
|
||||
const NANOS_IN_MILLIS = 1000 * 1000;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
export default function eventLogTests({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const retry = getService('retry');
|
||||
|
||||
describe('eventLog', () => {
|
||||
const objectRemover = new ObjectRemover(supertest);
|
||||
|
||||
after(() => objectRemover.removeAll());
|
||||
|
||||
it('should generate expected events for normal operation', async () => {
|
||||
const { body: createdAction } = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/actions/action`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send({
|
||||
name: 'MY action',
|
||||
actionTypeId: 'test.noop',
|
||||
config: {},
|
||||
secrets: {},
|
||||
})
|
||||
.expect(200);
|
||||
|
||||
// pattern of when the alert should fire
|
||||
const pattern = [false, true, true];
|
||||
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
alertTypeId: 'test.patternFiring',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
params: {
|
||||
pattern,
|
||||
},
|
||||
actions: [
|
||||
{
|
||||
id: createdAction.id,
|
||||
group: 'default',
|
||||
params: {},
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
const alertId = response.body.id;
|
||||
objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts');
|
||||
|
||||
// get the events we're expecting
|
||||
const events = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: Spaces.space1.id,
|
||||
type: 'alert',
|
||||
id: alertId,
|
||||
provider: 'alerting',
|
||||
actions: ['execute', 'execute-action', 'new-instance', 'resolved-instance'],
|
||||
});
|
||||
});
|
||||
|
||||
// make sure the counts of the # of events per type are as expected
|
||||
const executeEvents = getEventsByAction(events, 'execute');
|
||||
const executeActionEvents = getEventsByAction(events, 'execute-action');
|
||||
const newInstanceEvents = getEventsByAction(events, 'new-instance');
|
||||
const resolvedInstanceEvents = getEventsByAction(events, 'resolved-instance');
|
||||
|
||||
expect(executeEvents.length >= 4).to.be(true);
|
||||
expect(executeActionEvents.length).to.be(2);
|
||||
expect(newInstanceEvents.length).to.be(1);
|
||||
expect(resolvedInstanceEvents.length).to.be(1);
|
||||
|
||||
// make sure the events are in the right temporal order
|
||||
const executeTimes = getTimestamps(executeEvents);
|
||||
const executeActionTimes = getTimestamps(executeActionEvents);
|
||||
const newInstanceTimes = getTimestamps(newInstanceEvents);
|
||||
const resolvedInstanceTimes = getTimestamps(resolvedInstanceEvents);
|
||||
|
||||
expect(executeTimes[0] < newInstanceTimes[0]).to.be(true);
|
||||
expect(executeTimes[1] <= newInstanceTimes[0]).to.be(true);
|
||||
expect(executeTimes[2] > newInstanceTimes[0]).to.be(true);
|
||||
expect(executeTimes[1] <= executeActionTimes[0]).to.be(true);
|
||||
expect(executeTimes[2] > executeActionTimes[0]).to.be(true);
|
||||
expect(resolvedInstanceTimes[0] > newInstanceTimes[0]).to.be(true);
|
||||
|
||||
// validate each event
|
||||
for (const event of events) {
|
||||
switch (event?.event?.action) {
|
||||
case 'execute':
|
||||
validateEvent(event, {
|
||||
spaceId: Spaces.space1.id,
|
||||
savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }],
|
||||
outcome: 'success',
|
||||
message: `alert executed: test.patternFiring:${alertId}: 'abc'`,
|
||||
});
|
||||
break;
|
||||
case 'execute-action':
|
||||
validateEvent(event, {
|
||||
spaceId: Spaces.space1.id,
|
||||
savedObjects: [
|
||||
{ type: 'alert', id: alertId, rel: 'primary' },
|
||||
{ type: 'action', id: createdAction.id },
|
||||
],
|
||||
message: `alert: test.patternFiring:${alertId}: 'abc' instanceId: 'instance' scheduled actionGroup: 'default' action: test.noop:${createdAction.id}`,
|
||||
});
|
||||
break;
|
||||
case 'new-instance':
|
||||
validateEvent(event, {
|
||||
spaceId: Spaces.space1.id,
|
||||
savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }],
|
||||
message: `test.patternFiring:${alertId}: 'abc' created new instance: 'instance'`,
|
||||
});
|
||||
break;
|
||||
case 'resolved-instance':
|
||||
validateEvent(event, {
|
||||
spaceId: Spaces.space1.id,
|
||||
savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }],
|
||||
message: `test.patternFiring:${alertId}: 'abc' resolved instance: 'instance'`,
|
||||
});
|
||||
break;
|
||||
// this will get triggered as we add new event actions
|
||||
default:
|
||||
throw new Error(`unexpected event action "${event?.event?.action}"`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should generate events for execution errors', async () => {
|
||||
const response = await supertest
|
||||
.post(`${getUrlPrefix(Spaces.space1.id)}/api/alerts/alert`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(
|
||||
getTestAlertData({
|
||||
alertTypeId: 'test.throw',
|
||||
schedule: { interval: '1s' },
|
||||
throttle: null,
|
||||
})
|
||||
);
|
||||
|
||||
expect(response.status).to.eql(200);
|
||||
const alertId = response.body.id;
|
||||
objectRemover.add(Spaces.space1.id, alertId, 'alert', 'alerts');
|
||||
|
||||
const events = await retry.try(async () => {
|
||||
return await getEventLog({
|
||||
getService,
|
||||
spaceId: Spaces.space1.id,
|
||||
type: 'alert',
|
||||
id: alertId,
|
||||
provider: 'alerting',
|
||||
actions: ['execute'],
|
||||
});
|
||||
});
|
||||
|
||||
const event = events[0];
|
||||
expect(event).to.be.ok();
|
||||
|
||||
validateEvent(event, {
|
||||
spaceId: Spaces.space1.id,
|
||||
savedObjects: [{ type: 'alert', id: alertId, rel: 'primary' }],
|
||||
outcome: 'failure',
|
||||
message: `alert execution failure: test.throw:${alertId}: 'abc'`,
|
||||
errorMessage: 'this alert is intended to fail',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
interface SavedObject {
|
||||
type: string;
|
||||
id: string;
|
||||
rel?: string;
|
||||
}
|
||||
|
||||
interface ValidateEventLogParams {
|
||||
spaceId: string;
|
||||
savedObjects: SavedObject[];
|
||||
outcome?: string;
|
||||
message: string;
|
||||
errorMessage?: string;
|
||||
}
|
||||
|
||||
function validateEvent(event: IValidatedEvent, params: ValidateEventLogParams): void {
|
||||
const { spaceId, savedObjects, outcome, message, errorMessage } = params;
|
||||
|
||||
const duration = event?.event?.duration;
|
||||
const eventStart = Date.parse(event?.event?.start || 'undefined');
|
||||
const eventEnd = Date.parse(event?.event?.end || 'undefined');
|
||||
const dateNow = Date.now();
|
||||
|
||||
if (duration !== undefined) {
|
||||
expect(typeof duration).to.be('number');
|
||||
expect(eventStart).to.be.ok();
|
||||
expect(eventEnd).to.be.ok();
|
||||
|
||||
const durationDiff = Math.abs(
|
||||
Math.round(duration! / NANOS_IN_MILLIS) - (eventEnd - eventStart)
|
||||
);
|
||||
|
||||
// account for rounding errors
|
||||
expect(durationDiff < 1).to.equal(true);
|
||||
expect(eventStart <= eventEnd).to.equal(true);
|
||||
expect(eventEnd <= dateNow).to.equal(true);
|
||||
}
|
||||
|
||||
expect(event?.event?.outcome).to.equal(outcome);
|
||||
|
||||
for (const savedObject of savedObjects) {
|
||||
expect(
|
||||
isSavedObjectInEvent(event, spaceId, savedObject.type, savedObject.id, savedObject.rel)
|
||||
).to.be(true);
|
||||
}
|
||||
|
||||
expect(event?.message).to.eql(message);
|
||||
|
||||
if (errorMessage) {
|
||||
expect(event?.error?.message).to.eql(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getEventsByAction(events: IValidatedEvent[], action: string) {
|
||||
return events.filter((event) => event?.event?.action === action);
|
||||
}
|
||||
|
||||
function getTimestamps(events: IValidatedEvent[]) {
|
||||
return events.map((event) => event?.['@timestamp'] ?? 'missing timestamp');
|
||||
}
|
||||
|
||||
function isSavedObjectInEvent(
|
||||
event: IValidatedEvent,
|
||||
namespace: string,
|
||||
type: string,
|
||||
id: string,
|
||||
rel?: string
|
||||
): boolean {
|
||||
const savedObjects = event?.kibana?.saved_objects ?? [];
|
||||
|
||||
for (const savedObject of savedObjects) {
|
||||
if (
|
||||
savedObject.namespace === namespace &&
|
||||
savedObject.type === type &&
|
||||
savedObject.id === id &&
|
||||
savedObject.rel === rel
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
|
@ -17,6 +17,7 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./get'));
|
||||
loadTestFile(require.resolve('./get_alert_state'));
|
||||
loadTestFile(require.resolve('./list_alert_types'));
|
||||
loadTestFile(require.resolve('./event_log'));
|
||||
loadTestFile(require.resolve('./mute_all'));
|
||||
loadTestFile(require.resolve('./mute_instance'));
|
||||
loadTestFile(require.resolve('./unmute_all'));
|
||||
|
@ -26,6 +27,8 @@ export default function alertingTests({ loadTestFile }: FtrProviderContext) {
|
|||
loadTestFile(require.resolve('./alerts_space1'));
|
||||
loadTestFile(require.resolve('./alerts_default_space'));
|
||||
loadTestFile(require.resolve('./builtin_alert_types'));
|
||||
|
||||
// note that this test will destroy existing spaces
|
||||
loadTestFile(require.resolve('./migrations'));
|
||||
});
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ export default function alertingApiIntegrationTests({
|
|||
}
|
||||
});
|
||||
|
||||
after(() => esArchiver.unload('empty_kibana'));
|
||||
after(async () => await esArchiver.unload('empty_kibana'));
|
||||
|
||||
loadTestFile(require.resolve('./actions'));
|
||||
loadTestFile(require.resolve('./alerting'));
|
||||
|
|
|
@ -44,7 +44,7 @@ export class EventLogFixturePlugin
|
|||
core.savedObjects.registerType({
|
||||
name: 'event_log_test',
|
||||
hidden: false,
|
||||
namespaceType: 'agnostic',
|
||||
namespaceType: 'single',
|
||||
mappings: {
|
||||
properties: {},
|
||||
},
|
||||
|
|
|
@ -18,137 +18,162 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
const supertest = getService('supertest');
|
||||
const log = getService('log');
|
||||
const retry = getService('retry');
|
||||
const spacesService = getService('spaces');
|
||||
const esArchiver = getService('esArchiver');
|
||||
|
||||
describe('Event Log public API', () => {
|
||||
it('should allow querying for events by Saved Object', async () => {
|
||||
const id = uuid.v4();
|
||||
|
||||
const expectedEvents = [fakeEvent(id), fakeEvent(id)];
|
||||
|
||||
await logTestEvent(id, expectedEvents[0]);
|
||||
await logTestEvent(id, expectedEvents[1]);
|
||||
|
||||
await retry.try(async () => {
|
||||
const {
|
||||
body: { data, total },
|
||||
} = await findEvents(id, {});
|
||||
|
||||
expect(data.length).to.be(2);
|
||||
expect(total).to.be(2);
|
||||
|
||||
assertEventsFromApiMatchCreatedEvents(data, expectedEvents);
|
||||
before(async () => {
|
||||
await spacesService.create({
|
||||
id: 'namespace-a',
|
||||
name: 'Space A',
|
||||
disabledFeatures: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('should support pagination for events', async () => {
|
||||
const id = uuid.v4();
|
||||
|
||||
const expectedEvents = await logFakeEvents(id, 6);
|
||||
|
||||
await retry.try(async () => {
|
||||
const {
|
||||
body: { data: foundEvents },
|
||||
} = await findEvents(id, {});
|
||||
|
||||
expect(foundEvents.length).to.be(6);
|
||||
});
|
||||
|
||||
const [expectedFirstPage, expectedSecondPage] = chunk(expectedEvents, 3);
|
||||
|
||||
const {
|
||||
body: { data: firstPage },
|
||||
} = await findEvents(id, { per_page: 3 });
|
||||
|
||||
expect(firstPage.length).to.be(3);
|
||||
assertEventsFromApiMatchCreatedEvents(firstPage, expectedFirstPage);
|
||||
|
||||
const {
|
||||
body: { data: secondPage },
|
||||
} = await findEvents(id, { per_page: 3, page: 2 });
|
||||
|
||||
expect(secondPage.length).to.be(3);
|
||||
assertEventsFromApiMatchCreatedEvents(secondPage, expectedSecondPage);
|
||||
after(async () => {
|
||||
await esArchiver.unload('empty_kibana');
|
||||
});
|
||||
|
||||
it('should support sorting by event end', async () => {
|
||||
const id = uuid.v4();
|
||||
for (const namespace of [undefined, 'namespace-a']) {
|
||||
const namespaceName = namespace === undefined ? 'default' : namespace;
|
||||
|
||||
const expectedEvents = await logFakeEvents(id, 6);
|
||||
describe(`namespace: ${namespaceName}`, () => {
|
||||
it('should allow querying for events by Saved Object', async () => {
|
||||
const id = uuid.v4();
|
||||
|
||||
await retry.try(async () => {
|
||||
const {
|
||||
body: { data: foundEvents },
|
||||
} = await findEvents(id, { sort_field: 'event.end', sort_order: 'desc' });
|
||||
const expectedEvents = [fakeEvent(namespace, id), fakeEvent(namespace, id)];
|
||||
|
||||
expect(foundEvents.length).to.be(expectedEvents.length);
|
||||
assertEventsFromApiMatchCreatedEvents(foundEvents, expectedEvents.reverse());
|
||||
await logTestEvent(namespace, id, expectedEvents[0]);
|
||||
await logTestEvent(namespace, id, expectedEvents[1]);
|
||||
|
||||
await retry.try(async () => {
|
||||
const {
|
||||
body: { data, total },
|
||||
} = await findEvents(namespace, id, {});
|
||||
|
||||
expect(data.length).to.be(2);
|
||||
expect(total).to.be(2);
|
||||
|
||||
assertEventsFromApiMatchCreatedEvents(data, expectedEvents);
|
||||
});
|
||||
});
|
||||
|
||||
it('should support pagination for events', async () => {
|
||||
const id = uuid.v4();
|
||||
|
||||
const expectedEvents = await logFakeEvents(namespace, id, 6);
|
||||
|
||||
await retry.try(async () => {
|
||||
const {
|
||||
body: { data: foundEvents },
|
||||
} = await findEvents(namespace, id, {});
|
||||
|
||||
expect(foundEvents.length).to.be(6);
|
||||
});
|
||||
|
||||
const [expectedFirstPage, expectedSecondPage] = chunk(expectedEvents, 3);
|
||||
|
||||
const {
|
||||
body: { data: firstPage },
|
||||
} = await findEvents(namespace, id, { per_page: 3 });
|
||||
|
||||
expect(firstPage.length).to.be(3);
|
||||
assertEventsFromApiMatchCreatedEvents(firstPage, expectedFirstPage);
|
||||
|
||||
const {
|
||||
body: { data: secondPage },
|
||||
} = await findEvents(namespace, id, { per_page: 3, page: 2 });
|
||||
|
||||
expect(secondPage.length).to.be(3);
|
||||
assertEventsFromApiMatchCreatedEvents(secondPage, expectedSecondPage);
|
||||
});
|
||||
|
||||
it('should support sorting by event end', async () => {
|
||||
const id = uuid.v4();
|
||||
|
||||
const expectedEvents = await logFakeEvents(namespace, id, 6);
|
||||
|
||||
await retry.try(async () => {
|
||||
const {
|
||||
body: { data: foundEvents },
|
||||
} = await findEvents(namespace, id, { sort_field: 'event.end', sort_order: 'desc' });
|
||||
|
||||
expect(foundEvents.length).to.be(expectedEvents.length);
|
||||
assertEventsFromApiMatchCreatedEvents(foundEvents, expectedEvents.reverse());
|
||||
});
|
||||
});
|
||||
|
||||
it('should support date ranges for events', async () => {
|
||||
const id = uuid.v4();
|
||||
|
||||
// write a document that shouldn't be found in the inclusive date range search
|
||||
const firstEvent = fakeEvent(namespace, id);
|
||||
await logTestEvent(namespace, id, firstEvent);
|
||||
|
||||
// wait a second, get the start time for the date range search
|
||||
await delay(1000);
|
||||
const start = new Date().toISOString();
|
||||
|
||||
// write the documents that we should be found in the date range searches
|
||||
const expectedEvents = await logFakeEvents(namespace, id, 6);
|
||||
|
||||
// get the end time for the date range search
|
||||
const end = new Date().toISOString();
|
||||
|
||||
// write a document that shouldn't be found in the inclusive date range search
|
||||
await delay(1000);
|
||||
const lastEvent = fakeEvent(namespace, id);
|
||||
await logTestEvent(namespace, id, lastEvent);
|
||||
|
||||
await retry.try(async () => {
|
||||
const {
|
||||
body: { data: foundEvents, total },
|
||||
} = await findEvents(namespace, id, {});
|
||||
|
||||
expect(foundEvents.length).to.be(8);
|
||||
expect(total).to.be(8);
|
||||
});
|
||||
|
||||
const {
|
||||
body: { data: eventsWithinRange },
|
||||
} = await findEvents(namespace, id, { start, end });
|
||||
|
||||
expect(eventsWithinRange.length).to.be(expectedEvents.length);
|
||||
assertEventsFromApiMatchCreatedEvents(eventsWithinRange, expectedEvents);
|
||||
|
||||
const {
|
||||
body: { data: eventsFrom },
|
||||
} = await findEvents(namespace, id, { start });
|
||||
|
||||
expect(eventsFrom.length).to.be(expectedEvents.length + 1);
|
||||
assertEventsFromApiMatchCreatedEvents(eventsFrom, [...expectedEvents, lastEvent]);
|
||||
|
||||
const {
|
||||
body: { data: eventsUntil },
|
||||
} = await findEvents(namespace, id, { end });
|
||||
|
||||
expect(eventsUntil.length).to.be(expectedEvents.length + 1);
|
||||
assertEventsFromApiMatchCreatedEvents(eventsUntil, [firstEvent, ...expectedEvents]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should support date ranges for events', async () => {
|
||||
const id = uuid.v4();
|
||||
|
||||
// write a document that shouldn't be found in the inclusive date range search
|
||||
const firstEvent = fakeEvent(id);
|
||||
await logTestEvent(id, firstEvent);
|
||||
|
||||
// wait a second, get the start time for the date range search
|
||||
await delay(1000);
|
||||
const start = new Date().toISOString();
|
||||
|
||||
// write the documents that we should be found in the date range searches
|
||||
const expectedEvents = await logFakeEvents(id, 6);
|
||||
|
||||
// get the end time for the date range search
|
||||
const end = new Date().toISOString();
|
||||
|
||||
// write a document that shouldn't be found in the inclusive date range search
|
||||
await delay(1000);
|
||||
const lastEvent = fakeEvent(id);
|
||||
await logTestEvent(id, lastEvent);
|
||||
|
||||
await retry.try(async () => {
|
||||
const {
|
||||
body: { data: foundEvents, total },
|
||||
} = await findEvents(id, {});
|
||||
|
||||
expect(foundEvents.length).to.be(8);
|
||||
expect(total).to.be(8);
|
||||
});
|
||||
|
||||
const {
|
||||
body: { data: eventsWithinRange },
|
||||
} = await findEvents(id, { start, end });
|
||||
|
||||
expect(eventsWithinRange.length).to.be(expectedEvents.length);
|
||||
assertEventsFromApiMatchCreatedEvents(eventsWithinRange, expectedEvents);
|
||||
|
||||
const {
|
||||
body: { data: eventsFrom },
|
||||
} = await findEvents(id, { start });
|
||||
|
||||
expect(eventsFrom.length).to.be(expectedEvents.length + 1);
|
||||
assertEventsFromApiMatchCreatedEvents(eventsFrom, [...expectedEvents, lastEvent]);
|
||||
|
||||
const {
|
||||
body: { data: eventsUntil },
|
||||
} = await findEvents(id, { end });
|
||||
|
||||
expect(eventsUntil.length).to.be(expectedEvents.length + 1);
|
||||
assertEventsFromApiMatchCreatedEvents(eventsUntil, [firstEvent, ...expectedEvents]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
async function findEvents(id: string, query: Record<string, any> = {}) {
|
||||
const uri = `/api/event_log/event_log_test/${id}/_find${
|
||||
async function findEvents(
|
||||
namespace: string | undefined,
|
||||
id: string,
|
||||
query: Record<string, any> = {}
|
||||
) {
|
||||
const urlPrefix = urlPrefixFromNamespace(namespace);
|
||||
const url = `${urlPrefix}/api/event_log/event_log_test/${id}/_find${
|
||||
isEmpty(query)
|
||||
? ''
|
||||
: `?${Object.entries(query)
|
||||
.map(([key, val]) => `${key}=${val}`)
|
||||
.join('&')}`
|
||||
}`;
|
||||
log.debug(`calling ${uri}`);
|
||||
return await supertest.get(uri).set('kbn-xsrf', 'foo').expect(200);
|
||||
log.debug(`Finding Events for Saved Object with ${url}`);
|
||||
return await supertest.get(url).set('kbn-xsrf', 'foo').expect(200);
|
||||
}
|
||||
|
||||
function assertEventsFromApiMatchCreatedEvents(
|
||||
|
@ -169,16 +194,27 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
}
|
||||
}
|
||||
|
||||
async function logTestEvent(id: string, event: IEvent) {
|
||||
log.debug(`Logging Event for Saved Object ${id}`);
|
||||
return await supertest
|
||||
.post(`/api/log_event_fixture/${id}/_log`)
|
||||
.set('kbn-xsrf', 'foo')
|
||||
.send(event)
|
||||
.expect(200);
|
||||
async function logTestEvent(namespace: string | undefined, id: string, event: IEvent) {
|
||||
const urlPrefix = urlPrefixFromNamespace(namespace);
|
||||
const url = `${urlPrefix}/api/log_event_fixture/${id}/_log`;
|
||||
log.debug(`Logging Event for Saved Object with ${url} - ${JSON.stringify(event)}`);
|
||||
return await supertest.post(url).set('kbn-xsrf', 'foo').send(event).expect(200);
|
||||
}
|
||||
|
||||
function fakeEvent(id: string, overrides: Partial<IEvent> = {}): IEvent {
|
||||
function fakeEvent(
|
||||
namespace: string | undefined,
|
||||
id: string,
|
||||
overrides: Partial<IEvent> = {}
|
||||
): IEvent {
|
||||
const savedObject: any = {
|
||||
rel: 'primary',
|
||||
type: 'event_log_test',
|
||||
id,
|
||||
};
|
||||
if (namespace !== undefined) {
|
||||
savedObject.namespace = namespace;
|
||||
}
|
||||
|
||||
return merge(
|
||||
{
|
||||
event: {
|
||||
|
@ -186,14 +222,7 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
action: 'test',
|
||||
},
|
||||
kibana: {
|
||||
saved_objects: [
|
||||
{
|
||||
rel: 'primary',
|
||||
namespace: 'default',
|
||||
type: 'event_log_test',
|
||||
id,
|
||||
},
|
||||
],
|
||||
saved_objects: [savedObject],
|
||||
},
|
||||
message: `test ${moment().toISOString()}`,
|
||||
},
|
||||
|
@ -201,13 +230,22 @@ export default function ({ getService }: FtrProviderContext) {
|
|||
);
|
||||
}
|
||||
|
||||
async function logFakeEvents(savedObjectId: string, eventsToLog: number): Promise<IEvent[]> {
|
||||
async function logFakeEvents(
|
||||
namespace: string | undefined,
|
||||
savedObjectId: string,
|
||||
eventsToLog: number
|
||||
): Promise<IEvent[]> {
|
||||
const expectedEvents: IEvent[] = [];
|
||||
for (let index = 0; index < eventsToLog; index++) {
|
||||
const event = fakeEvent(savedObjectId);
|
||||
await logTestEvent(savedObjectId, event);
|
||||
const event = fakeEvent(namespace, savedObjectId);
|
||||
await logTestEvent(namespace, savedObjectId, event);
|
||||
expectedEvents.push(event);
|
||||
}
|
||||
return expectedEvents;
|
||||
}
|
||||
}
|
||||
|
||||
function urlPrefixFromNamespace(namespace: string | undefined): string {
|
||||
if (namespace === undefined) return '';
|
||||
return `/s/${namespace}`;
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue