mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
**Needed for:** rule execution log for Security https://github.com/elastic/kibana/pull/94143 **Related to:** - alerts-as-data: https://github.com/elastic/kibana/issues/93728, https://github.com/elastic/kibana/issues/93729, https://github.com/elastic/kibana/issues/93730 - RFC for index naming https://github.com/elastic/kibana/issues/98912 ## Summary This PR adds a mechanism for writing to / reading from / bootstrapping indices for RAC project into the `rule_registry` plugin. Particularly, indices for alerts-as-data and rule execution events. This implementation is similar to existing implementations like `event_log` plugin (see https://github.com/elastic/kibana/pull/98353#issuecomment-833045980 for historical perspective), but we're going to converge all of them into 1 or 2 implementations. At least we should have a single one in `rule_registry` itself. In this PR I tried to incorporate most of the feedback received in the RFC (https://github.com/elastic/kibana/issues/98912), but if you notice I missed/forgot something, please let me know in the comments. Done in this PR: - [x] Schema-agnostic APIs for working with Elasticsearch. - [x] Schema-aware log definition and bootstrapping API (creating hierarchical logs). - [x] Schema-aware write API (logging events). - [x] Schema-aware read API (searching logs, filtering, sorting, pagination, aggregation). - [x] Support for Kibana spaces, space-aware index bootstrapping (either at rule creation or rule execution time). As for reviewing this PR, perhaps it might be easier to start with: - checking description of https://github.com/elastic/kibana/issues/98912 - checking usage examples https://github.com/elastic/kibana/pull/98353/files#diff-c049ff2198cc69bd50a69e92d29e88da7e10b9a152bdaceaf3d41826e712c12b - checking public api https://github.com/elastic/kibana/pull/98353/files#diff-8e9ef0dbcbc60b1861d492a03865b2ae76a56ec38ada61898c991d3a74bd6268 ## Next steps Next steps towards rule execution log in Security (https://github.com/elastic/kibana/pull/94143): - define actual schema for rule execution events - inject instance of rule execution log into Security rule executors and route handlers - implement actual execution logging in rule executors - update route handlers to start fetching execution events and metrics from the log instead of custom saved objects Next steps in the context of RAC and unified implementation: - converge this implementation with `RuleDataService` implementation - implement robust index bootstrapping - reconsider using FieldMap as a generic type parameter - implement validation for documents being indexed - cover the final implementation with tests - write comprehensive docs: update plugin README, add JSDoc comments to all public interfaces
This commit is contained in:
parent
3f455e8ef6
commit
d3fbb91a22
41 changed files with 1810 additions and 41 deletions
|
@ -130,19 +130,20 @@ export class APMPlugin
|
|||
|
||||
registerFeaturesUsage({ licensingPlugin: plugins.licensing });
|
||||
|
||||
const { ruleDataService } = plugins.ruleRegistry;
|
||||
const getCoreStart = () =>
|
||||
core.getStartServices().then(([coreStart]) => coreStart);
|
||||
|
||||
const ready = once(async () => {
|
||||
const componentTemplateName = plugins.ruleRegistry.getFullAssetName(
|
||||
const componentTemplateName = ruleDataService.getFullAssetName(
|
||||
'apm-mappings'
|
||||
);
|
||||
|
||||
if (!plugins.ruleRegistry.isWriteEnabled()) {
|
||||
if (!ruleDataService.isWriteEnabled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await plugins.ruleRegistry.createOrUpdateComponentTemplate({
|
||||
await ruleDataService.createOrUpdateComponentTemplate({
|
||||
name: componentTemplateName,
|
||||
body: {
|
||||
template: {
|
||||
|
@ -167,16 +168,14 @@ export class APMPlugin
|
|||
},
|
||||
});
|
||||
|
||||
await plugins.ruleRegistry.createOrUpdateIndexTemplate({
|
||||
name: plugins.ruleRegistry.getFullAssetName('apm-index-template'),
|
||||
await ruleDataService.createOrUpdateIndexTemplate({
|
||||
name: ruleDataService.getFullAssetName('apm-index-template'),
|
||||
body: {
|
||||
index_patterns: [
|
||||
plugins.ruleRegistry.getFullAssetName('observability-apm*'),
|
||||
ruleDataService.getFullAssetName('observability-apm*'),
|
||||
],
|
||||
composed_of: [
|
||||
plugins.ruleRegistry.getFullAssetName(
|
||||
TECHNICAL_COMPONENT_TEMPLATE_NAME
|
||||
),
|
||||
ruleDataService.getFullAssetName(TECHNICAL_COMPONENT_TEMPLATE_NAME),
|
||||
componentTemplateName,
|
||||
],
|
||||
},
|
||||
|
@ -188,7 +187,7 @@ export class APMPlugin
|
|||
});
|
||||
|
||||
const ruleDataClient = new RuleDataClient({
|
||||
alias: plugins.ruleRegistry.getFullAssetName('observability-apm'),
|
||||
alias: ruleDataService.getFullAssetName('observability-apm'),
|
||||
getClusterClient: async () => {
|
||||
const coreStart = await getCoreStart();
|
||||
return coreStart.elasticsearch.client.asInternalUser;
|
||||
|
|
|
@ -57,7 +57,7 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
|
|||
return coreStart.elasticsearch.client.asInternalUser;
|
||||
},
|
||||
ready: () => Promise.resolve(),
|
||||
alias: plugins.ruleRegistry.getFullAssetName(),
|
||||
alias: plugins.ruleRegistry.ruleDataService.getFullAssetName(),
|
||||
});
|
||||
|
||||
registerRoutes({
|
||||
|
|
|
@ -8,6 +8,8 @@
|
|||
],
|
||||
"requiredPlugins": [
|
||||
"alerting",
|
||||
"data",
|
||||
"spaces",
|
||||
"triggersActionsUi"
|
||||
],
|
||||
"server": true
|
||||
|
|
20
x-pack/plugins/rule_registry/server/config.ts
Normal file
20
x-pack/plugins/rule_registry/server/config.ts
Normal file
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
|
||||
export const config = {
|
||||
schema: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
write: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
}),
|
||||
index: schema.string({ defaultValue: '.alerts' }),
|
||||
}),
|
||||
};
|
||||
|
||||
export type RuleRegistryPluginConfig = TypeOf<typeof config.schema>;
|
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './index_bootstrapper';
|
||||
export * from './index_management_gateway';
|
||||
export * from './index_reader';
|
||||
export * from './index_writer';
|
||||
export * from './resources/ilm_policy';
|
||||
export * from './resources/index_mappings';
|
||||
export * from './resources/index_names';
|
|
@ -0,0 +1,107 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { Logger } from 'src/core/server';
|
||||
|
||||
import { IndexNames } from './resources/index_names';
|
||||
import { IndexMappings } from './resources/index_mappings';
|
||||
import { createIndexTemplate } from './resources/index_template';
|
||||
import { IlmPolicy, defaultIlmPolicy } from './resources/ilm_policy';
|
||||
import { IIndexManagementGateway } from './index_management_gateway';
|
||||
|
||||
interface ConstructorParams {
|
||||
gateway: IIndexManagementGateway;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface IndexSpecification {
|
||||
indexNames: IndexNames;
|
||||
indexMappings: IndexMappings;
|
||||
ilmPolicy?: IlmPolicy;
|
||||
}
|
||||
|
||||
export type IIndexBootstrapper = PublicMethodsOf<IndexBootstrapper>;
|
||||
|
||||
// TODO: Converge with the logic of .siem-signals index bootstrapping
|
||||
// x-pack/plugins/security_solution/server/lib/detection_engine/routes/index/create_index_route.ts
|
||||
|
||||
// TODO: Handle race conditions and potential errors between multiple instances of Kibana
|
||||
// trying to bootstrap the same index. Possible options:
|
||||
// - robust idempotent logic with error handling
|
||||
// - leveraging task_manager to make sure bootstrapping is run only once at a time
|
||||
// - using some sort of distributed lock
|
||||
// Maybe we can check how Saved Objects service bootstraps .kibana index
|
||||
|
||||
export class IndexBootstrapper {
|
||||
private readonly gateway: IIndexManagementGateway;
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(params: ConstructorParams) {
|
||||
this.gateway = params.gateway;
|
||||
this.logger = params.logger.get('IndexBootstrapper');
|
||||
}
|
||||
|
||||
public async run(indexSpec: IndexSpecification): Promise<boolean> {
|
||||
this.logger.debug('bootstrapping elasticsearch resources starting');
|
||||
|
||||
try {
|
||||
const { indexNames, indexMappings, ilmPolicy } = indexSpec;
|
||||
await this.createIlmPolicyIfNotExists(indexNames, ilmPolicy);
|
||||
await this.createIndexTemplateIfNotExists(indexNames, indexMappings);
|
||||
await this.createInitialIndexIfNotExists(indexNames);
|
||||
} catch (err) {
|
||||
this.logger.error(`error bootstrapping elasticsearch resources: ${err.message}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger.debug('bootstrapping elasticsearch resources complete');
|
||||
return true;
|
||||
}
|
||||
|
||||
private async createIlmPolicyIfNotExists(names: IndexNames, policy?: IlmPolicy): Promise<void> {
|
||||
const { indexIlmPolicyName } = names;
|
||||
|
||||
const exists = await this.gateway.doesIlmPolicyExist(indexIlmPolicyName);
|
||||
if (!exists) {
|
||||
const ilmPolicy = policy ?? defaultIlmPolicy;
|
||||
await this.gateway.createIlmPolicy(indexIlmPolicyName, ilmPolicy);
|
||||
}
|
||||
}
|
||||
|
||||
private async createIndexTemplateIfNotExists(
|
||||
names: IndexNames,
|
||||
mappings: IndexMappings
|
||||
): Promise<void> {
|
||||
const { indexTemplateName } = names;
|
||||
|
||||
const templateVersion = 1; // TODO: get from EventSchema definition
|
||||
const template = createIndexTemplate(names, mappings, templateVersion);
|
||||
|
||||
const exists = await this.gateway.doesIndexTemplateExist(indexTemplateName);
|
||||
if (!exists) {
|
||||
await this.gateway.createIndexTemplate(indexTemplateName, template);
|
||||
} else {
|
||||
await this.gateway.updateIndexTemplate(indexTemplateName, template);
|
||||
}
|
||||
}
|
||||
|
||||
private async createInitialIndexIfNotExists(names: IndexNames): Promise<void> {
|
||||
const { indexAliasName, indexInitialName } = names;
|
||||
|
||||
const exists = await this.gateway.doesAliasExist(indexAliasName);
|
||||
if (!exists) {
|
||||
await this.gateway.createIndex(indexInitialName, {
|
||||
aliases: {
|
||||
[indexAliasName]: {
|
||||
is_write_index: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { ElasticsearchClient, Logger } from 'src/core/server';
|
||||
import { IlmPolicy } from './resources/ilm_policy';
|
||||
import { IndexTemplate } from './resources/index_template';
|
||||
|
||||
interface ConstructorParams {
|
||||
elasticsearch: Promise<ElasticsearchClient>;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export type IIndexManagementGateway = PublicMethodsOf<IndexManagementGateway>;
|
||||
|
||||
export class IndexManagementGateway {
|
||||
private readonly elasticsearch: Promise<ElasticsearchClient>;
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(params: ConstructorParams) {
|
||||
this.elasticsearch = params.elasticsearch;
|
||||
this.logger = params.logger.get('IndexManagementGateway');
|
||||
}
|
||||
|
||||
public async doesIlmPolicyExist(policyName: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking if ILM policy exists; name="${policyName}"`);
|
||||
|
||||
try {
|
||||
const es = await this.elasticsearch;
|
||||
await es.transport.request({
|
||||
method: 'GET',
|
||||
path: `/_ilm/policy/${policyName}`,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.statusCode === 404) return false;
|
||||
throw new Error(`Error checking existence of ILM policy: ${e.message}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async createIlmPolicy(policyName: string, policy: IlmPolicy): Promise<void> {
|
||||
this.logger.debug(`Creating ILM policy; name="${policyName}"`);
|
||||
|
||||
try {
|
||||
const es = await this.elasticsearch;
|
||||
await es.transport.request({
|
||||
method: 'PUT',
|
||||
path: `/_ilm/policy/${policyName}`,
|
||||
body: policy,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Error creating ILM policy: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async doesIndexTemplateExist(templateName: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking if index template exists; name="${templateName}"`);
|
||||
|
||||
try {
|
||||
const es = await this.elasticsearch;
|
||||
const { body } = await es.indices.existsTemplate({ name: templateName });
|
||||
return body as boolean;
|
||||
} catch (e) {
|
||||
throw new Error(`Error checking existence of index template: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async createIndexTemplate(templateName: string, template: IndexTemplate): Promise<void> {
|
||||
this.logger.debug(`Creating index template; name="${templateName}"`);
|
||||
|
||||
try {
|
||||
const es = await this.elasticsearch;
|
||||
await es.indices.putTemplate({ create: true, name: templateName, body: template });
|
||||
} catch (e) {
|
||||
// The error message doesn't have a type attribute we can look to guarantee it's due
|
||||
// to the template already existing (only long message) so we'll check ourselves to see
|
||||
// if the template now exists. This scenario would happen if you startup multiple Kibana
|
||||
// instances at the same time.
|
||||
const existsNow = await this.doesIndexTemplateExist(templateName);
|
||||
if (!existsNow) {
|
||||
const error = new Error(`Error creating index template: ${e.message}`);
|
||||
Object.assign(error, { wrapped: e });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async updateIndexTemplate(templateName: string, template: IndexTemplate): Promise<void> {
|
||||
this.logger.debug(`Updating index template; name="${templateName}"`);
|
||||
|
||||
try {
|
||||
const { settings, ...templateWithoutSettings } = template;
|
||||
|
||||
const es = await this.elasticsearch;
|
||||
await es.indices.putTemplate({
|
||||
create: false,
|
||||
name: templateName,
|
||||
body: templateWithoutSettings,
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(`Error updating index template: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async doesAliasExist(aliasName: string): Promise<boolean> {
|
||||
this.logger.debug(`Checking if index alias exists; name="${aliasName}"`);
|
||||
|
||||
try {
|
||||
const es = await this.elasticsearch;
|
||||
const { body } = await es.indices.existsAlias({ name: aliasName });
|
||||
return body as boolean;
|
||||
} catch (e) {
|
||||
throw new Error(`Error checking existence of initial index: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async createIndex(indexName: string, body: Record<string, unknown> = {}): Promise<void> {
|
||||
this.logger.debug(`Creating index; name="${indexName}"`);
|
||||
this.logger.debug(JSON.stringify(body, null, 2));
|
||||
|
||||
try {
|
||||
const es = await this.elasticsearch;
|
||||
await es.indices.create({
|
||||
index: indexName,
|
||||
body,
|
||||
});
|
||||
} catch (e) {
|
||||
if (e.body?.error?.type !== 'resource_already_exists_exception') {
|
||||
this.logger.error(e);
|
||||
this.logger.error(JSON.stringify(e.meta, null, 2));
|
||||
throw new Error(`Error creating initial index: ${e.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { Logger, ElasticsearchClient } from 'src/core/server';
|
||||
|
||||
interface ConstructorParams {
|
||||
indexName: string;
|
||||
elasticsearch: Promise<ElasticsearchClient>;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export type IIndexReader = PublicMethodsOf<IndexReader>;
|
||||
|
||||
export class IndexReader {
|
||||
private readonly indexName: string;
|
||||
private readonly elasticsearch: Promise<ElasticsearchClient>;
|
||||
private readonly logger: Logger;
|
||||
|
||||
constructor(params: ConstructorParams) {
|
||||
this.indexName = params.indexName;
|
||||
this.elasticsearch = params.elasticsearch;
|
||||
this.logger = params.logger.get('IndexReader');
|
||||
}
|
||||
|
||||
public async search<TDocument>(request: estypes.SearchRequest) {
|
||||
const requestToSend: estypes.SearchRequest = {
|
||||
...request,
|
||||
index: this.indexName,
|
||||
};
|
||||
|
||||
this.logger.debug(`Searching; request: ${JSON.stringify(requestToSend, null)}`);
|
||||
|
||||
const esClient = await this.elasticsearch;
|
||||
const response = await esClient.search<TDocument>(requestToSend);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { PublicMethodsOf } from '@kbn/utility-types';
|
||||
import util from 'util';
|
||||
import { Logger, ElasticsearchClient } from 'src/core/server';
|
||||
import { BufferedStream } from './utils/buffered_stream';
|
||||
|
||||
type Document = Record<string, unknown>;
|
||||
|
||||
interface BufferItem {
|
||||
index: string;
|
||||
doc: Document;
|
||||
}
|
||||
|
||||
interface ConstructorParams {
|
||||
indexName: string;
|
||||
elasticsearch: Promise<ElasticsearchClient>;
|
||||
isWriteEnabled: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export type IIndexWriter = PublicMethodsOf<IndexWriter>;
|
||||
|
||||
export class IndexWriter {
|
||||
private readonly indexName: string;
|
||||
private readonly elasticsearch: Promise<ElasticsearchClient>;
|
||||
private readonly isWriteEnabled: boolean;
|
||||
private readonly logger: Logger;
|
||||
private readonly buffer: BufferedStream<BufferItem>;
|
||||
|
||||
constructor(params: ConstructorParams) {
|
||||
this.indexName = params.indexName;
|
||||
this.elasticsearch = params.elasticsearch;
|
||||
this.isWriteEnabled = params.isWriteEnabled;
|
||||
this.logger = params.logger.get('IndexWriter');
|
||||
|
||||
this.buffer = new BufferedStream<BufferItem>({
|
||||
flush: (items) => this.bulkIndex(items),
|
||||
});
|
||||
}
|
||||
|
||||
public indexOne(doc: Document): void {
|
||||
if (this.isWriteEnabled) {
|
||||
this.logger.debug('Buffering 1 document');
|
||||
this.buffer.enqueue({ index: this.indexName, doc });
|
||||
}
|
||||
}
|
||||
|
||||
public indexMany(docs: Document[]): void {
|
||||
if (this.isWriteEnabled) {
|
||||
this.logger.debug(`Buffering ${docs.length} documents`);
|
||||
docs.forEach((doc) => {
|
||||
this.buffer.enqueue({ index: this.indexName, doc });
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public async close(): Promise<void> {
|
||||
await this.buffer.closeAndWaitUntilFlushed();
|
||||
}
|
||||
|
||||
private async bulkIndex(items: BufferItem[]): Promise<void> {
|
||||
this.logger.debug(`Indexing ${items.length} documents`);
|
||||
|
||||
const bulkBody: Array<Record<string, unknown>> = [];
|
||||
|
||||
for (const item of items) {
|
||||
if (item.doc === undefined) continue;
|
||||
|
||||
bulkBody.push({ create: { _index: item.index } });
|
||||
bulkBody.push(item.doc);
|
||||
}
|
||||
|
||||
try {
|
||||
const es = await this.elasticsearch;
|
||||
const response = await es.bulk({ body: bulkBody });
|
||||
|
||||
if (response.body.errors) {
|
||||
const error = new Error('Error writing some bulk events');
|
||||
error.stack += '\n' + util.inspect(response.body.items, { depth: null });
|
||||
this.logger.error(error);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.error(
|
||||
`error writing bulk events: "${e.message}"; docs: ${JSON.stringify(bulkBody)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
|
||||
export interface IlmPolicy {
|
||||
policy: estypes.Policy;
|
||||
}
|
||||
|
||||
export const defaultIlmPolicy: IlmPolicy = {
|
||||
policy: {
|
||||
phases: {
|
||||
hot: {
|
||||
min_age: '0ms',
|
||||
actions: {
|
||||
rollover: {
|
||||
max_age: '90d',
|
||||
max_size: '50gb',
|
||||
},
|
||||
},
|
||||
},
|
||||
delete: {
|
||||
actions: {
|
||||
delete: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface IndexMappings {
|
||||
dynamic: 'strict' | boolean;
|
||||
properties: Record<string, { type: string } | IndexMappings>;
|
||||
_meta?: Record<string, unknown>;
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface IndexParams {
|
||||
/** @example '.alerts' */
|
||||
indexPrefix: string;
|
||||
|
||||
/** @example 'security', 'security.alerts', 'observability.events' */
|
||||
logName: string;
|
||||
|
||||
/** @example 'default' */
|
||||
kibanaSpaceId: string;
|
||||
}
|
||||
|
||||
export interface IndexNames extends IndexParams {
|
||||
/** @example '.alerts-security.alerts' */
|
||||
indexBaseName: string;
|
||||
|
||||
/** @example '.alerts-security.alerts-*' */
|
||||
indexBasePattern: string;
|
||||
|
||||
/** @example '.alerts-security.alerts-default' */
|
||||
indexAliasName: string;
|
||||
|
||||
/** @example '.alerts-security.alerts-default-*' */
|
||||
indexAliasPattern: string;
|
||||
|
||||
/** @example '.alerts-security.alerts-default-policy' */
|
||||
indexIlmPolicyName: string;
|
||||
|
||||
/** @example '.alerts-security.alerts-default-template' */
|
||||
indexTemplateName: string;
|
||||
|
||||
/** @example '.alerts-security.alerts-default-000001' */
|
||||
indexInitialName: string;
|
||||
}
|
||||
|
||||
export abstract class IndexNames {
|
||||
public static create(params: IndexParams): IndexNames {
|
||||
const { indexPrefix, logName, kibanaSpaceId } = params;
|
||||
|
||||
// TODO: validate params
|
||||
|
||||
const indexBaseName = joinWithDash(indexPrefix, logName);
|
||||
const indexBasePattern = joinWithDash(indexPrefix, logName, '*');
|
||||
const indexAliasName = joinWithDash(indexPrefix, logName, kibanaSpaceId);
|
||||
const indexAliasPattern = joinWithDash(indexPrefix, logName, kibanaSpaceId, '*');
|
||||
const indexIlmPolicyName = joinWithDash(indexPrefix, logName, kibanaSpaceId, 'policy');
|
||||
const indexTemplateName = joinWithDash(indexPrefix, logName, kibanaSpaceId, 'template');
|
||||
const indexInitialName = joinWithDash(indexPrefix, logName, kibanaSpaceId, '000001');
|
||||
|
||||
return {
|
||||
indexPrefix,
|
||||
logName,
|
||||
kibanaSpaceId,
|
||||
indexBaseName,
|
||||
indexBasePattern,
|
||||
indexAliasName,
|
||||
indexAliasPattern,
|
||||
indexIlmPolicyName,
|
||||
indexTemplateName,
|
||||
indexInitialName,
|
||||
};
|
||||
}
|
||||
|
||||
public static createChild(parent: IndexNames, logName: string): IndexNames {
|
||||
return this.create({
|
||||
indexPrefix: parent.indexPrefix,
|
||||
logName: this.createChildLogName(parent.logName, logName),
|
||||
kibanaSpaceId: parent.kibanaSpaceId,
|
||||
});
|
||||
}
|
||||
|
||||
public static createChildLogName(parentLogName: string, logName: string): string {
|
||||
return joinWithDot(parentLogName, logName);
|
||||
}
|
||||
}
|
||||
|
||||
const joinWithDash = (...names: string[]): string => names.join('-');
|
||||
const joinWithDot = (...names: string[]): string => names.join('.');
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { IndexNames } from './index_names';
|
||||
import { IndexMappings } from './index_mappings';
|
||||
|
||||
export type IndexTemplate = estypes.PutIndexTemplateRequest['body'];
|
||||
|
||||
export const createIndexTemplate = (
|
||||
names: IndexNames,
|
||||
mappings: IndexMappings,
|
||||
version: number
|
||||
): IndexTemplate => {
|
||||
const { indexAliasName, indexAliasPattern, indexIlmPolicyName } = names;
|
||||
|
||||
return {
|
||||
index_patterns: [indexAliasPattern],
|
||||
settings: {
|
||||
number_of_shards: 1, // TODO: do we need to set this?
|
||||
auto_expand_replicas: '0-1', // TODO: do we need to set?
|
||||
index: {
|
||||
lifecycle: {
|
||||
name: indexIlmPolicyName,
|
||||
rollover_alias: indexAliasName,
|
||||
},
|
||||
},
|
||||
mapping: {
|
||||
total_fields: {
|
||||
limit: 10000,
|
||||
},
|
||||
},
|
||||
sort: {
|
||||
field: '@timestamp',
|
||||
order: 'desc',
|
||||
},
|
||||
},
|
||||
mappings: {
|
||||
...mappings,
|
||||
_meta: {
|
||||
...mappings._meta,
|
||||
version,
|
||||
},
|
||||
},
|
||||
version,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Subject } from 'rxjs';
|
||||
import { bufferTime, filter as rxFilter, switchMap } from 'rxjs/operators';
|
||||
|
||||
export const DEFAULT_BUFFER_TIME_MS = 1000;
|
||||
export const DEFAULT_BUFFER_SIZE = 100;
|
||||
|
||||
interface ConstructorParams<TItem> {
|
||||
maxBufferTimeMs?: number;
|
||||
maxBufferSize?: number;
|
||||
flush: (items: TItem[]) => Promise<void>;
|
||||
}
|
||||
|
||||
// TODO: handle possible exceptions in flush and maybe add retry logic
|
||||
|
||||
export class BufferedStream<TItem> {
|
||||
private readonly buffer$: Subject<TItem>;
|
||||
private readonly whenBufferCompleteAndFlushed: Promise<void>;
|
||||
|
||||
constructor(params: ConstructorParams<TItem>) {
|
||||
const maxBufferTime = params.maxBufferTimeMs ?? DEFAULT_BUFFER_TIME_MS;
|
||||
const maxBufferSize = params.maxBufferSize ?? DEFAULT_BUFFER_SIZE;
|
||||
|
||||
this.buffer$ = new Subject<TItem>();
|
||||
|
||||
// Buffer items for time/buffer length, ignore empty buffers,
|
||||
// then flush the buffered items; kick things off with a promise
|
||||
// on the observable, which we'll wait on in shutdown
|
||||
this.whenBufferCompleteAndFlushed = this.buffer$
|
||||
.pipe(
|
||||
bufferTime(maxBufferTime, null, maxBufferSize),
|
||||
rxFilter((docs) => docs.length > 0),
|
||||
switchMap(async (docs) => await params.flush(docs))
|
||||
)
|
||||
.toPromise();
|
||||
}
|
||||
|
||||
public enqueue(item: TItem): void {
|
||||
this.buffer$.next(item);
|
||||
}
|
||||
|
||||
public async closeAndWaitUntilFlushed(): Promise<void> {
|
||||
this.buffer$.complete();
|
||||
await this.whenBufferCompleteAndFlushed;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './schema_types';
|
||||
export * from './schema';
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EventSchema, Event } from './schema_types';
|
||||
import { FieldMap, runtimeTypeFromFieldMap, mergeFieldMaps } from '../../../common/field_map';
|
||||
import {
|
||||
TechnicalRuleFieldMaps,
|
||||
technicalRuleFieldMap,
|
||||
} from '../../../common/assets/field_maps/technical_rule_field_map';
|
||||
|
||||
const baseSchema = createSchema(technicalRuleFieldMap);
|
||||
|
||||
export abstract class Schema {
|
||||
public static create<TMap extends FieldMap>(fields: TMap): EventSchema<TMap> {
|
||||
return createSchema(fields);
|
||||
}
|
||||
|
||||
public static combine<T1 extends FieldMap, T2 extends FieldMap>(
|
||||
s1: EventSchema<T1>,
|
||||
s2: EventSchema<T2>
|
||||
): EventSchema<T1 & T2> {
|
||||
const combinedFields = mergeFieldMaps(s1.objectFields, s2.objectFields);
|
||||
return createSchema(combinedFields);
|
||||
}
|
||||
|
||||
public static getBase(): EventSchema<TechnicalRuleFieldMaps> {
|
||||
return baseSchema;
|
||||
}
|
||||
|
||||
public static extendBase<TMap extends FieldMap>(
|
||||
fields: TMap
|
||||
): EventSchema<TechnicalRuleFieldMaps & TMap> {
|
||||
const extensionSchema = createSchema(fields);
|
||||
return this.combine(baseSchema, extensionSchema);
|
||||
}
|
||||
}
|
||||
|
||||
function createSchema<TMap extends FieldMap>(fields: TMap): EventSchema<TMap> {
|
||||
const objectType: Event<TMap> = ({} as unknown) as Event<TMap>;
|
||||
const runtimeType = runtimeTypeFromFieldMap(fields);
|
||||
|
||||
return {
|
||||
objectFields: fields,
|
||||
objectType,
|
||||
runtimeType,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FieldMap, FieldMapType, TypeOfFieldMap } from '../../../common/field_map';
|
||||
|
||||
export interface EventSchema<TMap extends FieldMap> {
|
||||
objectFields: TMap;
|
||||
objectType: Event<TMap>;
|
||||
runtimeType: EventRuntimeType<TMap>;
|
||||
}
|
||||
|
||||
export type Event<TMap extends FieldMap> = TypeOfFieldMap<TMap>;
|
||||
|
||||
export type EventRuntimeType<TMap extends FieldMap> = FieldMapType<TMap>;
|
||||
|
||||
export { FieldMap };
|
10
x-pack/plugins/rule_registry/server/event_log/index.ts
Normal file
10
x-pack/plugins/rule_registry/server/event_log/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './elasticsearch';
|
||||
export * from './event_schema';
|
||||
export * from './log';
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { DeepPartial } from '../utils/utility_types';
|
||||
import { IndexNames } from '../elasticsearch';
|
||||
import { IEventLog, IEventLogger, IEventLoggerTemplate, IEventQueryBuilder } from './public_api';
|
||||
import { EventLogParams } from './internal_api';
|
||||
import { EventLoggerTemplate } from './event_logger_template';
|
||||
import { EventQueryBuilder } from './event_query_builder';
|
||||
|
||||
export class EventLog<TEvent> implements IEventLog<TEvent> {
|
||||
private readonly params: EventLogParams;
|
||||
private readonly initialTemplate: IEventLoggerTemplate<TEvent>;
|
||||
|
||||
constructor(params: EventLogParams) {
|
||||
this.params = params;
|
||||
this.initialTemplate = new EventLoggerTemplate<TEvent>({
|
||||
...params,
|
||||
eventLoggerName: '',
|
||||
eventFields: {},
|
||||
});
|
||||
}
|
||||
|
||||
public getNames(): IndexNames {
|
||||
return this.params.indexNames;
|
||||
}
|
||||
|
||||
public getLoggerTemplate(fields: DeepPartial<TEvent>): IEventLoggerTemplate<TEvent> {
|
||||
return this.initialTemplate.getLoggerTemplate(fields);
|
||||
}
|
||||
|
||||
public getLogger(loggerName: string, fields?: DeepPartial<TEvent>): IEventLogger<TEvent> {
|
||||
return this.initialTemplate.getLogger(loggerName, fields);
|
||||
}
|
||||
|
||||
public getQueryBuilder(): IEventQueryBuilder<TEvent> {
|
||||
return new EventQueryBuilder<TEvent>(this.params);
|
||||
}
|
||||
|
||||
public async search<TDocument = TEvent>(
|
||||
request: estypes.SearchRequest
|
||||
): Promise<estypes.SearchResponse<TDocument>> {
|
||||
const response = await this.params.indexReader.search<TDocument>(request);
|
||||
return response.body;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Logger } from 'kibana/server';
|
||||
import { IIndexBootstrapper, IndexSpecification } from '../elasticsearch';
|
||||
|
||||
interface ConstructorParams {
|
||||
indexSpec: IndexSpecification;
|
||||
indexBootstrapper: IIndexBootstrapper;
|
||||
isWriteEnabled: boolean;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export class EventLogBootstrapper {
|
||||
private readonly indexSpec: IndexSpecification;
|
||||
private readonly indexBootstrapper: IIndexBootstrapper;
|
||||
private readonly logger: Logger;
|
||||
private readonly isWriteEnabled: boolean;
|
||||
private isIndexBootstrapped: boolean;
|
||||
|
||||
constructor(params: ConstructorParams) {
|
||||
this.indexSpec = params.indexSpec;
|
||||
this.indexBootstrapper = params.indexBootstrapper;
|
||||
this.logger = params.logger.get('EventLogBootstrapper');
|
||||
this.isWriteEnabled = params.isWriteEnabled;
|
||||
this.isIndexBootstrapped = false;
|
||||
}
|
||||
|
||||
public async run(): Promise<void> {
|
||||
if (this.isIndexBootstrapped || !this.isWriteEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { logName, indexAliasName } = this.indexSpec.indexNames;
|
||||
const logInfo = `log="${logName}" index="${indexAliasName}"`;
|
||||
|
||||
this.logger.debug(`Bootstrapping started, ${logInfo}`);
|
||||
this.isIndexBootstrapped = await this.indexBootstrapper.run(this.indexSpec);
|
||||
this.logger.debug(
|
||||
`Bootstrapping ${this.isIndexBootstrapped ? 'succeeded' : 'failed'}, ${logInfo}`
|
||||
);
|
||||
|
||||
if (!this.isIndexBootstrapped) {
|
||||
throw new Error(`Event log bootstrapping failed, ${logInfo}`);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,37 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IlmPolicy, defaultIlmPolicy, IndexNames } from '../elasticsearch';
|
||||
import { EventSchema, FieldMap, Schema } from '../event_schema';
|
||||
import { EventLogOptions, IEventLogDefinition } from './public_api';
|
||||
|
||||
export class EventLogDefinition<TMap extends FieldMap> implements IEventLogDefinition<TMap> {
|
||||
public readonly eventLogName: string;
|
||||
public readonly eventSchema: EventSchema<TMap>;
|
||||
public readonly ilmPolicy: IlmPolicy;
|
||||
|
||||
constructor(options: EventLogOptions<TMap>) {
|
||||
// TODO: validate options; options.name should not contain "-" and "."
|
||||
this.eventLogName = options.name;
|
||||
this.eventSchema = options.schema;
|
||||
this.ilmPolicy = options.ilmPolicy ?? defaultIlmPolicy;
|
||||
}
|
||||
|
||||
public defineChild<TExtMap extends FieldMap = TMap>(
|
||||
options: EventLogOptions<TExtMap>
|
||||
): IEventLogDefinition<TMap & TExtMap> {
|
||||
const childName = IndexNames.createChildLogName(this.eventLogName, options.name);
|
||||
const childSchema = Schema.combine(this.eventSchema, options.schema);
|
||||
const childPolicy = options.ilmPolicy ?? this.ilmPolicy;
|
||||
|
||||
return new EventLogDefinition({
|
||||
name: childName,
|
||||
schema: childSchema,
|
||||
ilmPolicy: childPolicy,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IIndexWriter } from '../elasticsearch';
|
||||
import { IEventLog } from './public_api';
|
||||
import { IEventLogProvider } from './internal_api';
|
||||
import { EventLogBootstrapper } from './event_log_bootstrapper';
|
||||
|
||||
interface ConstructorParams<TEvent> {
|
||||
log: IEventLog<TEvent>;
|
||||
logBootstrapper: EventLogBootstrapper;
|
||||
indexWriter: IIndexWriter;
|
||||
}
|
||||
|
||||
export class EventLogProvider<TEvent> implements IEventLogProvider<TEvent> {
|
||||
constructor(private readonly params: ConstructorParams<TEvent>) {}
|
||||
|
||||
public getLog(): IEventLog<TEvent> {
|
||||
return this.params.log;
|
||||
}
|
||||
|
||||
public async bootstrapLog(): Promise<void> {
|
||||
await this.params.logBootstrapper.run();
|
||||
}
|
||||
|
||||
public async shutdownLog(): Promise<void> {
|
||||
await this.params.indexWriter.close();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Event, FieldMap } from '../event_schema';
|
||||
import { IEventLogDefinition } from './public_api';
|
||||
import { IEventLogRegistry, IEventLogProvider } from './internal_api';
|
||||
|
||||
const getRegistryKey = (definition: IEventLogDefinition<any>, spaceId: string) =>
|
||||
`${definition.eventLogName}-${spaceId}`;
|
||||
|
||||
interface RegistryEntry {
|
||||
definition: IEventLogDefinition<any>;
|
||||
spaceId: string;
|
||||
provider: IEventLogProvider<any>;
|
||||
}
|
||||
|
||||
export class EventLogRegistry implements IEventLogRegistry {
|
||||
private readonly map = new Map<string, RegistryEntry>();
|
||||
|
||||
public get<TMap extends FieldMap>(
|
||||
definition: IEventLogDefinition<TMap>,
|
||||
spaceId: string
|
||||
): IEventLogProvider<Event<TMap>> | null {
|
||||
const key = getRegistryKey(definition, spaceId);
|
||||
const entry = this.map.get(key);
|
||||
return entry != null ? (entry.provider as IEventLogProvider<Event<TMap>>) : null;
|
||||
}
|
||||
|
||||
public add<TMap extends FieldMap>(
|
||||
definition: IEventLogDefinition<TMap>,
|
||||
spaceId: string,
|
||||
provider: IEventLogProvider<Event<TMap>>
|
||||
): void {
|
||||
const key = getRegistryKey(definition, spaceId);
|
||||
|
||||
if (this.map.has(key)) {
|
||||
throw new Error(`Event log already registered, key="${key}"`);
|
||||
}
|
||||
|
||||
this.map.set(key, {
|
||||
definition,
|
||||
spaceId,
|
||||
provider,
|
||||
});
|
||||
}
|
||||
|
||||
public async shutdown(): Promise<void> {
|
||||
const entries = Array.from(this.map.values());
|
||||
const promises = entries.map(({ provider }) => provider.shutdownLog());
|
||||
await Promise.all(promises);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
IndexBootstrapper,
|
||||
IndexManagementGateway,
|
||||
IndexNames,
|
||||
IndexReader,
|
||||
IndexSpecification,
|
||||
IndexWriter,
|
||||
} from '../elasticsearch';
|
||||
|
||||
import { Event, FieldMap } from '../event_schema';
|
||||
import { IEventLogRegistry, IEventLogProvider } from './internal_api';
|
||||
import {
|
||||
EventLogServiceConfig,
|
||||
EventLogServiceDependencies,
|
||||
IEventLog,
|
||||
IEventLogDefinition,
|
||||
IEventLogResolver,
|
||||
} from './public_api';
|
||||
|
||||
import { EventLog } from './event_log';
|
||||
import { EventLogBootstrapper } from './event_log_bootstrapper';
|
||||
import { EventLogProvider } from './event_log_provider';
|
||||
import { mappingFromFieldMap } from './utils/mapping_from_field_map';
|
||||
|
||||
export class EventLogResolver implements IEventLogResolver {
|
||||
private readonly indexBootstrapper: IndexBootstrapper;
|
||||
|
||||
constructor(
|
||||
private readonly config: EventLogServiceConfig,
|
||||
private readonly deps: EventLogServiceDependencies,
|
||||
private readonly registry: IEventLogRegistry,
|
||||
private readonly bootstrapLog: boolean
|
||||
) {
|
||||
this.indexBootstrapper = this.createIndexBootstrapper();
|
||||
}
|
||||
|
||||
public async resolve<TMap extends FieldMap>(
|
||||
definition: IEventLogDefinition<TMap>,
|
||||
kibanaSpaceId: string
|
||||
): Promise<IEventLog<Event<TMap>>> {
|
||||
const provider = this.resolveLogProvider(definition, kibanaSpaceId);
|
||||
|
||||
if (this.bootstrapLog) {
|
||||
await provider.bootstrapLog();
|
||||
}
|
||||
|
||||
return provider.getLog();
|
||||
}
|
||||
|
||||
private resolveLogProvider<TMap extends FieldMap>(
|
||||
definition: IEventLogDefinition<TMap>,
|
||||
kibanaSpaceId: string
|
||||
): IEventLogProvider<Event<TMap>> {
|
||||
const existingProvider = this.registry.get(definition, kibanaSpaceId);
|
||||
if (existingProvider) {
|
||||
return existingProvider;
|
||||
}
|
||||
|
||||
const indexSpec = this.createIndexSpec(definition, kibanaSpaceId);
|
||||
const indexReader = this.createIndexReader(indexSpec);
|
||||
const indexWriter = this.createIndexWriter(indexSpec);
|
||||
const logBootstrapper = this.createEventLogBootstrapper(indexSpec);
|
||||
const log = this.createEventLog<TMap>(indexSpec, indexReader, indexWriter);
|
||||
const logProvider = new EventLogProvider({
|
||||
log,
|
||||
logBootstrapper,
|
||||
indexWriter,
|
||||
});
|
||||
|
||||
this.registry.add(definition, kibanaSpaceId, logProvider);
|
||||
|
||||
return logProvider;
|
||||
}
|
||||
|
||||
private createIndexSpec<TMap extends FieldMap>(
|
||||
definition: IEventLogDefinition<TMap>,
|
||||
kibanaSpaceId: string
|
||||
): IndexSpecification {
|
||||
const { indexPrefix } = this.config;
|
||||
const { eventLogName, eventSchema, ilmPolicy } = definition;
|
||||
|
||||
const indexNames = IndexNames.create({
|
||||
indexPrefix,
|
||||
logName: eventLogName,
|
||||
kibanaSpaceId,
|
||||
});
|
||||
|
||||
const indexMappings = mappingFromFieldMap(eventSchema.objectFields);
|
||||
|
||||
return { indexNames, indexMappings, ilmPolicy };
|
||||
}
|
||||
|
||||
private createIndexBootstrapper(): IndexBootstrapper {
|
||||
const { clusterClient, logger } = this.deps;
|
||||
|
||||
return new IndexBootstrapper({
|
||||
gateway: new IndexManagementGateway({
|
||||
elasticsearch: clusterClient.then((c) => c.asInternalUser),
|
||||
logger,
|
||||
}),
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
private createIndexReader(indexSpec: IndexSpecification): IndexReader {
|
||||
const { clusterClient, logger } = this.deps;
|
||||
const { indexNames } = indexSpec;
|
||||
|
||||
return new IndexReader({
|
||||
indexName: indexNames.indexAliasPattern,
|
||||
elasticsearch: clusterClient.then((c) => c.asInternalUser), // TODO: internal or current?
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
private createIndexWriter(indexSpec: IndexSpecification): IndexWriter {
|
||||
const { clusterClient, logger } = this.deps;
|
||||
const { isWriteEnabled } = this.config;
|
||||
const { indexNames } = indexSpec;
|
||||
|
||||
return new IndexWriter({
|
||||
indexName: indexNames.indexAliasName,
|
||||
elasticsearch: clusterClient.then((c) => c.asInternalUser), // TODO: internal or current?
|
||||
isWriteEnabled,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
private createEventLogBootstrapper(indexSpec: IndexSpecification): EventLogBootstrapper {
|
||||
const { logger } = this.deps;
|
||||
const { isWriteEnabled } = this.config;
|
||||
|
||||
return new EventLogBootstrapper({
|
||||
indexSpec,
|
||||
indexBootstrapper: this.indexBootstrapper,
|
||||
isWriteEnabled,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
|
||||
private createEventLog<TMap extends FieldMap>(
|
||||
indexSpec: IndexSpecification,
|
||||
indexReader: IndexReader,
|
||||
indexWriter: IndexWriter
|
||||
): IEventLog<Event<TMap>> {
|
||||
const { logger } = this.deps;
|
||||
|
||||
return new EventLog<Event<TMap>>({
|
||||
indexNames: indexSpec.indexNames,
|
||||
indexReader,
|
||||
indexWriter,
|
||||
logger,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { KibanaRequest } from 'kibana/server';
|
||||
|
||||
import { Event, FieldMap } from '../event_schema';
|
||||
import {
|
||||
EventLogServiceConfig,
|
||||
EventLogServiceDependencies,
|
||||
IEventLog,
|
||||
IEventLogDefinition,
|
||||
IEventLogResolver,
|
||||
IEventLogService,
|
||||
IScopedEventLogResolver,
|
||||
} from './public_api';
|
||||
|
||||
import { EventLogRegistry } from './event_log_registry';
|
||||
import { EventLogResolver } from './event_log_resolver';
|
||||
|
||||
const BOOTSTRAP_BY_DEFAULT = true;
|
||||
|
||||
interface ConstructorParams {
|
||||
config: EventLogServiceConfig;
|
||||
dependencies: EventLogServiceDependencies;
|
||||
}
|
||||
|
||||
export class EventLogService implements IEventLogService {
|
||||
private readonly registry: EventLogRegistry;
|
||||
|
||||
constructor(private readonly params: ConstructorParams) {
|
||||
this.registry = new EventLogRegistry();
|
||||
}
|
||||
|
||||
public getResolver(bootstrapLog = BOOTSTRAP_BY_DEFAULT): IEventLogResolver {
|
||||
const { params, registry } = this;
|
||||
const { config, dependencies } = params;
|
||||
|
||||
return new EventLogResolver(config, dependencies, registry, bootstrapLog);
|
||||
}
|
||||
|
||||
public getScopedResolver(
|
||||
request: KibanaRequest,
|
||||
bootstrapLog = BOOTSTRAP_BY_DEFAULT
|
||||
): IScopedEventLogResolver {
|
||||
const resolver = this.getResolver(bootstrapLog);
|
||||
|
||||
return {
|
||||
resolve: async <TMap extends FieldMap>(
|
||||
definition: IEventLogDefinition<TMap>
|
||||
): Promise<IEventLog<Event<TMap>>> => {
|
||||
const spaces = await this.params.dependencies.spacesService;
|
||||
const spaceId = spaces.getSpaceId(request);
|
||||
|
||||
const log = await resolver.resolve(definition, spaceId);
|
||||
return log;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
public async stop(): Promise<void> {
|
||||
await this.registry.shutdown();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DeepPartial } from '../utils/utility_types';
|
||||
import { mergeFields } from '../utils/fields';
|
||||
import { EventLoggerParams } from './internal_api';
|
||||
import { IEventLogger, IEventLoggerTemplate } from './public_api';
|
||||
|
||||
export class EventLogger<TEvent> implements IEventLogger<TEvent> {
|
||||
private readonly params: EventLoggerParams<TEvent>;
|
||||
private readonly ownTemplate: IEventLoggerTemplate<TEvent>;
|
||||
|
||||
constructor(params: EventLoggerParams<TEvent>, template: IEventLoggerTemplate<TEvent>) {
|
||||
this.params = params;
|
||||
this.ownTemplate = template;
|
||||
}
|
||||
|
||||
public getLoggerTemplate(fields: DeepPartial<TEvent>): IEventLoggerTemplate<TEvent> {
|
||||
return this.ownTemplate.getLoggerTemplate(fields);
|
||||
}
|
||||
|
||||
public getLogger(name: string, fields?: DeepPartial<TEvent>): IEventLogger<TEvent> {
|
||||
return this.ownTemplate.getLogger(name, fields);
|
||||
}
|
||||
|
||||
public logEvent(fields: DeepPartial<TEvent>): void {
|
||||
const { eventFields, indexWriter } = this.params;
|
||||
|
||||
const event = mergeFields(eventFields, fields);
|
||||
indexWriter.indexOne(event);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DeepPartial } from '../utils/utility_types';
|
||||
import { mergeFields } from '../utils/fields';
|
||||
import { IEventLogger, IEventLoggerTemplate } from './public_api';
|
||||
import { EventLoggerParams } from './internal_api';
|
||||
import { EventLogger } from './event_logger';
|
||||
|
||||
export class EventLoggerTemplate<TEvent> implements IEventLoggerTemplate<TEvent> {
|
||||
private readonly params: EventLoggerParams<TEvent>;
|
||||
|
||||
constructor(params: EventLoggerParams<TEvent>) {
|
||||
this.params = params;
|
||||
}
|
||||
|
||||
public getLoggerTemplate(fields: DeepPartial<TEvent>): IEventLoggerTemplate<TEvent> {
|
||||
const nextParams = this.getNextParams('', fields);
|
||||
return new EventLoggerTemplate<TEvent>(nextParams);
|
||||
}
|
||||
|
||||
public getLogger(name: string, fields?: DeepPartial<TEvent>): IEventLogger<TEvent> {
|
||||
const nextParams = this.getNextParams(name, fields);
|
||||
const nextTemplate = new EventLoggerTemplate<TEvent>(nextParams);
|
||||
return new EventLogger<TEvent>(nextParams, nextTemplate);
|
||||
}
|
||||
|
||||
private getNextParams(
|
||||
extName: string,
|
||||
extFields?: DeepPartial<TEvent>
|
||||
): EventLoggerParams<TEvent> {
|
||||
const { indexNames, eventLoggerName, eventFields } = this.params;
|
||||
|
||||
const baseName = eventLoggerName;
|
||||
const nextName = [baseName, extName].filter(Boolean).join('.');
|
||||
|
||||
const baseFields = eventFields;
|
||||
const nextFields = mergeFields(baseFields, extFields, {
|
||||
// TODO: Define a schema for own fields used/set by event log. Add it to the base schema.
|
||||
// Then maybe introduce a base type for TEvent.
|
||||
'kibana.rac.event_log.log_name': indexNames.logName,
|
||||
'kibana.rac.event_log.logger_name': nextName,
|
||||
} as any);
|
||||
|
||||
return {
|
||||
...this.params,
|
||||
eventLoggerName: nextName,
|
||||
eventFields: nextFields,
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { IIndexReader } from '../elasticsearch';
|
||||
import { truthy } from '../utils/predicates';
|
||||
import { IEventQuery } from './public_api';
|
||||
|
||||
export interface EventQueryParams {
|
||||
indexReader: IIndexReader;
|
||||
request: estypes.SearchRequest;
|
||||
}
|
||||
|
||||
export class EventQuery<TEvent> implements IEventQuery<TEvent> {
|
||||
constructor(private readonly params: EventQueryParams) {}
|
||||
|
||||
public async execute(): Promise<TEvent[]> {
|
||||
const { indexReader, request } = this.params;
|
||||
|
||||
const response = await indexReader.search<TEvent>(request);
|
||||
return response.body.hits.hits.map((hit) => hit._source).filter(truthy);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,110 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { getFlattenedObject } from '@kbn/std';
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { esKuery } from '../../../../../../src/plugins/data/server';
|
||||
|
||||
import { DeepPartial } from '../utils/utility_types';
|
||||
import { mergeFields } from '../utils/fields';
|
||||
import { EventLogParams } from './internal_api';
|
||||
import { IEventQueryBuilder, IEventQuery, SortingParams, PaginationParams } from './public_api';
|
||||
import { EventQuery } from './event_query';
|
||||
|
||||
export class EventQueryBuilder<TEvent> implements IEventQueryBuilder<TEvent> {
|
||||
private readonly params: EventLogParams;
|
||||
private loggerName: string;
|
||||
private fields: DeepPartial<TEvent> | null;
|
||||
private kql: string;
|
||||
private sorting: SortingParams;
|
||||
private pagination: PaginationParams;
|
||||
|
||||
constructor(params: EventLogParams) {
|
||||
this.params = params;
|
||||
this.loggerName = '';
|
||||
this.fields = null;
|
||||
this.kql = '';
|
||||
this.sorting = [{ '@timestamp': { order: 'desc' } }, { 'event.sequence': { order: 'desc' } }];
|
||||
this.pagination = { page: 1, perPage: 20 };
|
||||
}
|
||||
|
||||
public filterByLogger(loggerName: string): IEventQueryBuilder<TEvent> {
|
||||
this.loggerName = loggerName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public filterByFields(fields: DeepPartial<TEvent>): IEventQueryBuilder<TEvent> {
|
||||
this.fields = mergeFields(this.fields ?? {}, fields);
|
||||
return this;
|
||||
}
|
||||
|
||||
public filterByKql(kql: string): IEventQueryBuilder<TEvent> {
|
||||
this.kql = kql;
|
||||
return this;
|
||||
}
|
||||
|
||||
public sortBy(params: SortingParams): IEventQueryBuilder<TEvent> {
|
||||
this.sorting = params;
|
||||
return this;
|
||||
}
|
||||
|
||||
public paginate(params: PaginationParams): IEventQueryBuilder<TEvent> {
|
||||
this.pagination = params;
|
||||
return this;
|
||||
}
|
||||
|
||||
public buildQuery(): IEventQuery<TEvent> {
|
||||
const { indexReader } = this.params;
|
||||
const { page, perPage } = this.pagination;
|
||||
|
||||
const request: estypes.SearchRequest = {
|
||||
track_total_hits: true,
|
||||
body: {
|
||||
from: (page - 1) * perPage,
|
||||
size: perPage,
|
||||
sort: this.sorting,
|
||||
query: {
|
||||
bool: {
|
||||
filter: this.buildFilter(),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return new EventQuery<TEvent>({ indexReader, request });
|
||||
}
|
||||
|
||||
private buildFilter(): estypes.QueryContainer[] {
|
||||
const result: estypes.QueryContainer[] = [];
|
||||
|
||||
if (this.loggerName) {
|
||||
result.push({
|
||||
term: { 'kibana.rac.event_log.logger_name': this.loggerName },
|
||||
});
|
||||
}
|
||||
|
||||
if (this.fields) {
|
||||
const flatFields = getFlattenedObject(this.fields);
|
||||
Object.entries(flatFields)
|
||||
.map(([key, value]) => {
|
||||
const queryName = Array.isArray(value) ? 'terms' : 'term';
|
||||
return { [queryName]: { [key]: value } };
|
||||
})
|
||||
.forEach((query) => {
|
||||
result.push(query);
|
||||
});
|
||||
}
|
||||
|
||||
if (this.kql) {
|
||||
const dsl = esKuery.toElasticsearchQuery(esKuery.fromKueryExpression(this.kql));
|
||||
const queries = Array.isArray(dsl) ? dsl : [dsl];
|
||||
result.push(...queries);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
10
x-pack/plugins/rule_registry/server/event_log/log/index.ts
Normal file
10
x-pack/plugins/rule_registry/server/event_log/log/index.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export * from './event_log_definition';
|
||||
export * from './event_log_service';
|
||||
export * from './public_api';
|
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Logger } from 'kibana/server';
|
||||
|
||||
import { IIndexReader, IIndexWriter, IndexNames } from '../elasticsearch';
|
||||
import { Event, FieldMap } from '../event_schema';
|
||||
import { DeepPartial } from '../utils/utility_types';
|
||||
import { IEventLogDefinition, IEventLog } from './public_api';
|
||||
|
||||
export interface IEventLogRegistry {
|
||||
get<TMap extends FieldMap>(
|
||||
definition: IEventLogDefinition<TMap>,
|
||||
spaceId: string
|
||||
): IEventLogProvider<Event<TMap>> | null;
|
||||
|
||||
add<TMap extends FieldMap>(
|
||||
definition: IEventLogDefinition<TMap>,
|
||||
spaceId: string,
|
||||
provider: IEventLogProvider<Event<TMap>>
|
||||
): void;
|
||||
|
||||
shutdown(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface IEventLogProvider<TEvent> {
|
||||
getLog(): IEventLog<TEvent>;
|
||||
bootstrapLog(): Promise<void>;
|
||||
shutdownLog(): Promise<void>;
|
||||
}
|
||||
|
||||
export interface EventLogParams {
|
||||
indexNames: IndexNames;
|
||||
indexReader: IIndexReader;
|
||||
indexWriter: IIndexWriter;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface EventLoggerParams<TEvent> extends EventLogParams {
|
||||
eventLoggerName: string;
|
||||
eventFields: DeepPartial<TEvent>;
|
||||
}
|
113
x-pack/plugins/rule_registry/server/event_log/log/public_api.ts
Normal file
113
x-pack/plugins/rule_registry/server/event_log/log/public_api.ts
Normal file
|
@ -0,0 +1,113 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { estypes } from '@elastic/elasticsearch';
|
||||
import { IClusterClient, KibanaRequest, Logger } from 'kibana/server';
|
||||
import { SpacesServiceStart } from '../../../../spaces/server';
|
||||
|
||||
import { IlmPolicy, IndexNames, IndexSpecification } from '../elasticsearch';
|
||||
import { FieldMap, Event, EventSchema } from '../event_schema';
|
||||
import { DeepPartial } from '../utils/utility_types';
|
||||
|
||||
export { IlmPolicy, IndexSpecification };
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Definition API (defining log hierarchies as simple objects)
|
||||
|
||||
export interface EventLogOptions<TMap extends FieldMap> {
|
||||
name: string;
|
||||
schema: EventSchema<TMap>;
|
||||
ilmPolicy?: IlmPolicy;
|
||||
}
|
||||
|
||||
export interface IEventLogDefinition<TMap extends FieldMap> {
|
||||
eventLogName: string;
|
||||
eventSchema: EventSchema<TMap>;
|
||||
ilmPolicy: IlmPolicy;
|
||||
|
||||
defineChild<TExtMap extends FieldMap>(
|
||||
options: EventLogOptions<TExtMap>
|
||||
): IEventLogDefinition<TMap & TExtMap>;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Resolving and bootstrapping API (creating runtime objects representing logs, bootstrapping indices)
|
||||
|
||||
export interface EventLogServiceConfig {
|
||||
indexPrefix: string;
|
||||
isWriteEnabled: boolean;
|
||||
}
|
||||
|
||||
export interface EventLogServiceDependencies {
|
||||
clusterClient: Promise<IClusterClient>;
|
||||
spacesService: Promise<SpacesServiceStart>;
|
||||
logger: Logger;
|
||||
}
|
||||
|
||||
export interface IEventLogService {
|
||||
getResolver(bootstrapLog?: boolean): IEventLogResolver;
|
||||
getScopedResolver(request: KibanaRequest, bootstrapLog?: boolean): IScopedEventLogResolver;
|
||||
}
|
||||
|
||||
export interface IEventLogResolver {
|
||||
resolve<TMap extends FieldMap>(
|
||||
definition: IEventLogDefinition<TMap>,
|
||||
spaceId: string
|
||||
): Promise<IEventLog<Event<TMap>>>;
|
||||
}
|
||||
|
||||
export interface IScopedEventLogResolver {
|
||||
resolve<TMap extends FieldMap>(
|
||||
definition: IEventLogDefinition<TMap>
|
||||
): Promise<IEventLog<Event<TMap>>>;
|
||||
}
|
||||
|
||||
export interface IEventLog<TEvent> extends IEventLoggerTemplate<TEvent> {
|
||||
getNames(): IndexNames;
|
||||
|
||||
getQueryBuilder(): IEventQueryBuilder<TEvent>;
|
||||
|
||||
search<TDocument = TEvent>(
|
||||
request: estypes.SearchRequest
|
||||
): Promise<estypes.SearchResponse<TDocument>>;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Write API (logging events)
|
||||
|
||||
export interface IEventLoggerTemplate<TEvent> {
|
||||
getLoggerTemplate(fields: DeepPartial<TEvent>): IEventLoggerTemplate<TEvent>;
|
||||
getLogger(name: string, fields?: DeepPartial<TEvent>): IEventLogger<TEvent>;
|
||||
}
|
||||
|
||||
export interface IEventLogger<TEvent> extends IEventLoggerTemplate<TEvent> {
|
||||
logEvent(fields: DeepPartial<TEvent>): void;
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------------------------------
|
||||
// Read API (searching, filtering, sorting, pagination, aggregation over events)
|
||||
|
||||
export interface IEventQueryBuilder<TEvent> {
|
||||
filterByLogger(loggerName: string): IEventQueryBuilder<TEvent>;
|
||||
filterByFields(fields: DeepPartial<TEvent>): IEventQueryBuilder<TEvent>;
|
||||
filterByKql(kql: string): IEventQueryBuilder<TEvent>;
|
||||
sortBy(params: SortingParams): IEventQueryBuilder<TEvent>;
|
||||
paginate(params: PaginationParams): IEventQueryBuilder<TEvent>;
|
||||
|
||||
buildQuery(): IEventQuery<TEvent>;
|
||||
}
|
||||
|
||||
export type SortingParams = estypes.Sort;
|
||||
|
||||
export interface PaginationParams {
|
||||
page: number;
|
||||
perPage: number;
|
||||
}
|
||||
|
||||
export interface IEventQuery<TEvent> {
|
||||
execute(): Promise<TEvent[]>;
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { set } from '@elastic/safer-lodash-set';
|
||||
import { FieldMap } from '../../../../common/field_map';
|
||||
import { IndexMappings } from '../../elasticsearch';
|
||||
|
||||
export function mappingFromFieldMap(fieldMap: FieldMap): IndexMappings {
|
||||
const mappings = {
|
||||
dynamic: 'strict' as const,
|
||||
properties: {},
|
||||
};
|
||||
|
||||
const fields = Object.keys(fieldMap).map((key) => {
|
||||
const field = fieldMap[key];
|
||||
return {
|
||||
name: key,
|
||||
...field,
|
||||
};
|
||||
});
|
||||
|
||||
fields.forEach((field) => {
|
||||
const { name, required, array, ...rest } = field;
|
||||
|
||||
set(mappings.properties, field.name.split('.').join('.properties.'), rest);
|
||||
});
|
||||
|
||||
return mappings;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { merge } from 'lodash';
|
||||
import { DeepPartial } from './utility_types';
|
||||
|
||||
export const mergeFields = <TEvent>(
|
||||
base: DeepPartial<TEvent>,
|
||||
ext1?: DeepPartial<TEvent>,
|
||||
ext2?: DeepPartial<TEvent>,
|
||||
ext3?: DeepPartial<TEvent>
|
||||
): DeepPartial<TEvent> => {
|
||||
return merge({}, base, ext1 ?? {}, ext2 ?? {}, ext3 ?? {});
|
||||
};
|
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export function nonNullable<T>(value: T): value is NonNullable<T> {
|
||||
return value !== null && value !== undefined;
|
||||
}
|
||||
|
||||
export type Truthy<T> = T extends false | '' | 0 | null | undefined ? never : T; // from lodash
|
||||
|
||||
export function truthy<T>(value: T): value is Truthy<T> {
|
||||
return Boolean(value);
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { createReadySignal, ReadySignal } from './ready_signal';
|
||||
|
||||
describe('ReadySignal', () => {
|
||||
let readySignal: ReadySignal<number>;
|
||||
|
||||
beforeEach(() => {
|
||||
readySignal = createReadySignal<number>();
|
||||
});
|
||||
|
||||
test('works as expected', async () => {
|
||||
readySignal.signal(42);
|
||||
const ready = await readySignal.wait();
|
||||
expect(ready).toBe(42);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface ReadySignal<T = void> {
|
||||
wait(): Promise<T>;
|
||||
signal(value: T): void;
|
||||
}
|
||||
|
||||
export function createReadySignal<T>(): ReadySignal<T> {
|
||||
let resolver: (value: T) => void;
|
||||
|
||||
const promise = new Promise<T>((resolve) => {
|
||||
resolver = resolve;
|
||||
});
|
||||
|
||||
async function wait(): Promise<T> {
|
||||
return await promise;
|
||||
}
|
||||
|
||||
function signal(value: T) {
|
||||
resolver(value);
|
||||
}
|
||||
|
||||
return { wait, signal };
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export type DeepWriteable<T> = { -readonly [P in keyof T]: DeepWriteable<T[P]> };
|
||||
|
||||
export type DeepPartial<T> = {
|
||||
[P in keyof T]?: T[P] extends Array<infer U> ? Array<DeepPartial<U>> : DeepPartial<T[P]>;
|
||||
};
|
|
@ -5,27 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { schema, TypeOf } from '@kbn/config-schema';
|
||||
import { PluginInitializerContext } from 'src/core/server';
|
||||
import { RuleRegistryPlugin } from './plugin';
|
||||
|
||||
export * from './config';
|
||||
export type { RuleRegistryPluginSetupContract, RuleRegistryPluginStartContract } from './plugin';
|
||||
export { RuleDataClient } from './rule_data_client';
|
||||
export { IRuleDataClient } from './rule_data_client/types';
|
||||
export { getRuleExecutorData, RuleExecutorData } from './utils/get_rule_executor_data';
|
||||
export { createLifecycleRuleTypeFactory } from './utils/create_lifecycle_rule_type_factory';
|
||||
|
||||
export const config = {
|
||||
schema: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
write: schema.object({
|
||||
enabled: schema.boolean({ defaultValue: true }),
|
||||
}),
|
||||
index: schema.string({ defaultValue: '.alerts' }),
|
||||
}),
|
||||
};
|
||||
|
||||
export type RuleRegistryPluginConfig = TypeOf<typeof config.schema>;
|
||||
|
||||
export const plugin = (initContext: PluginInitializerContext) =>
|
||||
new RuleRegistryPlugin(initContext);
|
||||
|
|
|
@ -5,45 +5,99 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PluginInitializerContext, Plugin, CoreSetup } from 'src/core/server';
|
||||
import { RuleDataPluginService } from './rule_data_plugin_service';
|
||||
import { RuleRegistryPluginConfig } from '.';
|
||||
import { PluginInitializerContext, Plugin, CoreSetup, Logger } from 'src/core/server';
|
||||
import { SpacesPluginStart } from '../../spaces/server';
|
||||
|
||||
import { RuleRegistryPluginConfig } from './config';
|
||||
import { RuleDataPluginService } from './rule_data_plugin_service';
|
||||
import { EventLogService, IEventLogService } from './event_log';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
||||
interface RuleRegistryPluginSetupDependencies {}
|
||||
|
||||
interface RuleRegistryPluginStartDependencies {
|
||||
spaces: SpacesPluginStart;
|
||||
}
|
||||
|
||||
export interface RuleRegistryPluginSetupContract {
|
||||
ruleDataService: RuleDataPluginService;
|
||||
eventLogService: IEventLogService;
|
||||
}
|
||||
|
||||
export type RuleRegistryPluginSetupContract = RuleDataPluginService;
|
||||
export type RuleRegistryPluginStartContract = void;
|
||||
|
||||
export class RuleRegistryPlugin implements Plugin<RuleRegistryPluginSetupContract> {
|
||||
constructor(private readonly initContext: PluginInitializerContext) {
|
||||
this.initContext = initContext;
|
||||
export class RuleRegistryPlugin
|
||||
implements
|
||||
Plugin<
|
||||
RuleRegistryPluginSetupContract,
|
||||
RuleRegistryPluginStartContract,
|
||||
RuleRegistryPluginSetupDependencies,
|
||||
RuleRegistryPluginStartDependencies
|
||||
> {
|
||||
private readonly config: RuleRegistryPluginConfig;
|
||||
private readonly logger: Logger;
|
||||
private eventLogService: EventLogService | null;
|
||||
|
||||
constructor(initContext: PluginInitializerContext) {
|
||||
this.config = initContext.config.get<RuleRegistryPluginConfig>();
|
||||
this.logger = initContext.logger.get();
|
||||
this.eventLogService = null;
|
||||
}
|
||||
|
||||
public setup(core: CoreSetup): RuleRegistryPluginSetupContract {
|
||||
const config = this.initContext.config.get<RuleRegistryPluginConfig>();
|
||||
public setup(
|
||||
core: CoreSetup<RuleRegistryPluginStartDependencies, RuleRegistryPluginStartContract>
|
||||
): RuleRegistryPluginSetupContract {
|
||||
const { config, logger } = this;
|
||||
|
||||
const logger = this.initContext.logger.get();
|
||||
const startDependencies = core.getStartServices().then(([coreStart, pluginStart]) => {
|
||||
return {
|
||||
core: coreStart,
|
||||
...pluginStart,
|
||||
};
|
||||
});
|
||||
|
||||
const service = new RuleDataPluginService({
|
||||
const ruleDataService = new RuleDataPluginService({
|
||||
logger,
|
||||
isWriteEnabled: config.write.enabled,
|
||||
index: config.index,
|
||||
getClusterClient: async () => {
|
||||
const [coreStart] = await core.getStartServices();
|
||||
|
||||
return coreStart.elasticsearch.client.asInternalUser;
|
||||
const deps = await startDependencies;
|
||||
return deps.core.elasticsearch.client.asInternalUser;
|
||||
},
|
||||
});
|
||||
|
||||
service.init().catch((originalError) => {
|
||||
ruleDataService.init().catch((originalError) => {
|
||||
const error = new Error('Failed installing assets');
|
||||
// @ts-ignore
|
||||
error.stack = originalError.stack;
|
||||
logger.error(error);
|
||||
});
|
||||
|
||||
return service;
|
||||
const eventLogService = new EventLogService({
|
||||
config: {
|
||||
indexPrefix: this.config.index,
|
||||
isWriteEnabled: this.config.write.enabled,
|
||||
},
|
||||
dependencies: {
|
||||
clusterClient: startDependencies.then((deps) => deps.core.elasticsearch.client),
|
||||
spacesService: startDependencies.then((deps) => deps.spaces.spacesService),
|
||||
logger: logger.get('eventLog'),
|
||||
},
|
||||
});
|
||||
|
||||
this.eventLogService = eventLogService;
|
||||
return { ruleDataService, eventLogService };
|
||||
}
|
||||
|
||||
public start(): RuleRegistryPluginStartContract {}
|
||||
|
||||
public stop() {}
|
||||
public stop() {
|
||||
const { eventLogService, logger } = this;
|
||||
|
||||
if (eventLogService) {
|
||||
eventLogService.stop().catch((e) => {
|
||||
logger.error(e);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,9 @@
|
|||
"include": ["common/**/*", "server/**/*", "public/**/*", "../../../typings/**/*"],
|
||||
"references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/data/tsconfig.json" },
|
||||
{ "path": "../alerting/tsconfig.json" },
|
||||
{ "path": "../spaces/tsconfig.json" },
|
||||
{ "path": "../triggers_actions_ui/tsconfig.json" }
|
||||
]
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue