+ {error.name}: {error.message} +
+ } + /> + )} +- {error.name}: {error.message} -
- } - /> - )} -diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/is_builtin_definition.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/is_builtin_definition.ts new file mode 100644 index 00000000000000..3ad6542cbdd8c8 --- /dev/null +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/helpers/is_builtin_definition.ts @@ -0,0 +1,13 @@ +/* + * 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 { EntityDefinition } from '@kbn/entities-schema'; +import { BUILT_IN_ID_PREFIX } from '../built_in'; + +export function isBuiltinDefinition(definition: EntityDefinition) { + return definition.id.startsWith(BUILT_IN_ID_PREFIX); +} diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap index a013388882a3ff..c2e4605e5f9093 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_history_processors.test.ts.snap @@ -1,6 +1,157 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`generateHistoryProcessors(definition) should genearte a valid pipeline 1`] = ` +exports[`generateHistoryProcessors(definition) should generate a valid pipeline for builtin definition 1`] = ` +Array [ + Object { + "set": Object { + "field": "event.ingested", + "value": "{{{_ingest.timestamp}}}", + }, + }, + Object { + "set": Object { + "field": "entity.type", + "value": "service", + }, + }, + Object { + "set": Object { + "field": "entity.definitionId", + "value": "builtin_mock_entity_definition", + }, + }, + Object { + "set": Object { + "field": "entity.definitionVersion", + "value": "1.0.0", + }, + }, + Object { + "set": Object { + "field": "entity.schemaVersion", + "value": "v1", + }, + }, + Object { + "set": Object { + "field": "entity.identityFields", + "value": Array [ + "log.logger", + "event.category", + ], + }, + }, + Object { + "script": Object { + "description": "Generated the entity.id field", + "source": "// This function will recursively collect all the values of a HashMap of HashMaps +Collection collectValues(HashMap subject) { + Collection values = new ArrayList(); + // Iterate through the values + for(Object value: subject.values()) { + // If the value is a HashMap, recurse + if (value instanceof HashMap) { + values.addAll(collectValues((HashMap) value)); + } else { + values.add(String.valueOf(value)); + } + } + return values; +} +// Create the string builder +StringBuilder entityId = new StringBuilder(); +if (ctx[\\"entity\\"][\\"identity\\"] != null) { + // Get the values as a collection + Collection values = collectValues(ctx[\\"entity\\"][\\"identity\\"]); + // Convert to a list and sort + List sortedValues = new ArrayList(values); + Collections.sort(sortedValues); + // Create comma delimited string + for(String instanceValue: sortedValues) { + entityId.append(instanceValue); + entityId.append(\\":\\"); + } + // Assign the entity.id + ctx[\\"entity\\"][\\"id\\"] = entityId.length() > 0 ? entityId.substring(0, entityId.length() - 1) : \\"unknown\\"; +}", + }, + }, + Object { + "fingerprint": Object { + "fields": Array [ + "entity.id", + ], + "method": "MurmurHash3", + "target_field": "entity.id", + }, + }, + Object { + "script": Object { + "source": "if (ctx.entity?.metadata?.tags != null) { + ctx.tags = ctx.entity.metadata.tags.keySet(); +} +if (ctx.entity?.metadata?.host?.name != null) { + if (ctx.host == null) { + ctx.host = new HashMap(); + } + ctx.host.name = ctx.entity.metadata.host.name.keySet(); +} +if (ctx.entity?.metadata?.host?.os?.name != null) { + if (ctx.host == null) { + ctx.host = new HashMap(); + } + if (ctx.host.os == null) { + ctx.host.os = new HashMap(); + } + ctx.host.os.name = ctx.entity.metadata.host.os.name.keySet(); +} +if (ctx.entity?.metadata?.sourceIndex != null) { + ctx.sourceIndex = ctx.entity.metadata.sourceIndex.keySet(); +}", + }, + }, + Object { + "remove": Object { + "field": "entity.metadata", + "ignore_missing": true, + }, + }, + Object { + "set": Object { + "field": "log.logger", + "if": "ctx.entity?.identity?.log?.logger != null", + "value": "{{entity.identity.log.logger}}", + }, + }, + Object { + "set": Object { + "field": "event.category", + "if": "ctx.entity?.identity?.event?.category != null", + "value": "{{entity.identity.event.category}}", + }, + }, + Object { + "remove": Object { + "field": "entity.identity", + "ignore_missing": true, + }, + }, + Object { + "date_index_name": Object { + "date_formats": Array [ + "UNIX_MS", + "ISO8601", + "yyyy-MM-dd'T'HH:mm:ss.SSSXX", + ], + "date_rounding": "M", + "field": "@timestamp", + "index_name_prefix": ".entities.v1.history.builtin_mock_entity_definition.", + }, + }, +] +`; + +exports[`generateHistoryProcessors(definition) should generate a valid pipeline for custom definition 1`] = ` Array [ Object { "set": Object { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap index f866e34fbb69b9..f277b3ac84ab8d 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/__snapshots__/generate_latest_processors.test.ts.snap @@ -1,6 +1,123 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`generateLatestProcessors(definition) should genearte a valid pipeline 1`] = ` +exports[`generateLatestProcessors(definition) should generate a valid pipeline for builtin definition 1`] = ` +Array [ + Object { + "set": Object { + "field": "event.ingested", + "value": "{{{_ingest.timestamp}}}", + }, + }, + Object { + "set": Object { + "field": "entity.type", + "value": "service", + }, + }, + Object { + "set": Object { + "field": "entity.definitionId", + "value": "builtin_mock_entity_definition", + }, + }, + Object { + "set": Object { + "field": "entity.definitionVersion", + "value": "1.0.0", + }, + }, + Object { + "set": Object { + "field": "entity.schemaVersion", + "value": "v1", + }, + }, + Object { + "set": Object { + "field": "entity.identityFields", + "value": Array [ + "log.logger", + "event.category", + ], + }, + }, + Object { + "script": Object { + "source": "if (ctx.entity?.metadata?.tags.data != null) { + ctx.tags = ctx.entity.metadata.tags.data.keySet(); +} +if (ctx.entity?.metadata?.host?.name.data != null) { + if (ctx.host == null) { + ctx.host = new HashMap(); + } + ctx.host.name = ctx.entity.metadata.host.name.data.keySet(); +} +if (ctx.entity?.metadata?.host?.os?.name.data != null) { + if (ctx.host == null) { + ctx.host = new HashMap(); + } + if (ctx.host.os == null) { + ctx.host.os = new HashMap(); + } + ctx.host.os.name = ctx.entity.metadata.host.os.name.data.keySet(); +} +if (ctx.entity?.metadata?.sourceIndex.data != null) { + ctx.sourceIndex = ctx.entity.metadata.sourceIndex.data.keySet(); +}", + }, + }, + Object { + "remove": Object { + "field": "entity.metadata", + "ignore_missing": true, + }, + }, + Object { + "dot_expander": Object { + "field": "log.logger", + "path": "entity.identity.log.logger.top_metric", + }, + }, + Object { + "set": Object { + "field": "log.logger", + "value": "{{entity.identity.log.logger.top_metric.log.logger}}", + }, + }, + Object { + "dot_expander": Object { + "field": "event.category", + "path": "entity.identity.event.category.top_metric", + }, + }, + Object { + "set": Object { + "field": "event.category", + "value": "{{entity.identity.event.category.top_metric.event.category}}", + }, + }, + Object { + "remove": Object { + "field": "entity.identity", + "ignore_missing": true, + }, + }, + Object { + "set": Object { + "field": "entity.displayName", + "value": "{{log.logger}}{{#event.category}}:{{.}}{{/event.category}}", + }, + }, + Object { + "set": Object { + "field": "_index", + "value": ".entities.v1.latest.builtin_mock_entity_definition", + }, + }, +] +`; + +exports[`generateLatestProcessors(definition) should generate a valid pipeline for custom definition 1`] = ` Array [ Object { "set": Object { diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.test.ts index 697b3a5223f9ff..717241b89143d8 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.test.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.test.ts @@ -5,12 +5,17 @@ * 2.0. */ -import { entityDefinition } from '../helpers/fixtures/entity_definition'; +import { entityDefinition, builtInEntityDefinition } from '../helpers/fixtures'; import { generateHistoryProcessors } from './generate_history_processors'; describe('generateHistoryProcessors(definition)', () => { - it('should genearte a valid pipeline', () => { + it('should generate a valid pipeline for custom definition', () => { const processors = generateHistoryProcessors(entityDefinition); expect(processors).toMatchSnapshot(); }); + + it('should generate a valid pipeline for builtin definition', () => { + const processors = generateHistoryProcessors(builtInEntityDefinition); + expect(processors).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts index 0dd2bb0a8f4657..91a03479a3547e 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_history_processors.ts @@ -11,6 +11,7 @@ import { cleanScript, } from '../helpers/ingest_pipeline_script_processor_helpers'; import { generateHistoryIndexName } from '../helpers/generate_component_id'; +import { isBuiltinDefinition } from '../helpers/is_builtin_definition'; function mapDestinationToPainless(field: string) { return ` @@ -46,6 +47,39 @@ function liftIdentityFieldsToDocumentRoot(definition: EntityDefinition) { })); } +function getCustomIngestPipelines(definition: EntityDefinition) { + if (isBuiltinDefinition(definition)) { + return []; + } + + return [ + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}@platform`, + }, + }, + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}-history@platform`, + }, + }, + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}@custom`, + }, + }, + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}-history@custom`, + }, + }, + ]; +} + export function generateHistoryProcessors(definition: EntityDefinition) { return [ { @@ -162,30 +196,6 @@ export function generateHistoryProcessors(definition: EntityDefinition) { date_formats: ['UNIX_MS', 'ISO8601', "yyyy-MM-dd'T'HH:mm:ss.SSSXX"], }, }, - { - pipeline: { - ignore_missing_pipeline: true, - name: `${definition.id}@platform`, - }, - }, - { - pipeline: { - ignore_missing_pipeline: true, - name: `${definition.id}-history@platform`, - }, - }, - - { - pipeline: { - ignore_missing_pipeline: true, - name: `${definition.id}@custom`, - }, - }, - { - pipeline: { - ignore_missing_pipeline: true, - name: `${definition.id}-history@custom`, - }, - }, + ...getCustomIngestPipelines(definition), ]; } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.test.ts index b6a10ce3db3478..3cc75eee74b15d 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.test.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.test.ts @@ -5,12 +5,17 @@ * 2.0. */ -import { entityDefinition } from '../helpers/fixtures/entity_definition'; +import { entityDefinition, builtInEntityDefinition } from '../helpers/fixtures'; import { generateLatestProcessors } from './generate_latest_processors'; describe('generateLatestProcessors(definition)', () => { - it('should genearte a valid pipeline', () => { + it('should generate a valid pipeline for custom definition', () => { const processors = generateLatestProcessors(entityDefinition); expect(processors).toMatchSnapshot(); }); + + it('should generate a valid pipeline for builtin definition', () => { + const processors = generateLatestProcessors(builtInEntityDefinition); + expect(processors).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts index f1a45d297554e0..8efefe8ae33d9c 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/ingest_pipeline/generate_latest_processors.ts @@ -11,6 +11,7 @@ import { cleanScript, } from '../helpers/ingest_pipeline_script_processor_helpers'; import { generateLatestIndexName } from '../helpers/generate_component_id'; +import { isBuiltinDefinition } from '../helpers/is_builtin_definition'; function mapDestinationToPainless(field: string) { return ` @@ -63,6 +64,39 @@ function liftIdentityFieldsToDocumentRoot(definition: EntityDefinition) { .flat(); } +function getCustomIngestPipelines(definition: EntityDefinition) { + if (isBuiltinDefinition(definition)) { + return []; + } + + return [ + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}@platform`, + }, + }, + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}-latest@platform`, + }, + }, + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}@custom`, + }, + }, + { + pipeline: { + ignore_missing_pipeline: true, + name: `${definition.id}-latest@custom`, + }, + }, + ]; +} + export function generateLatestProcessors(definition: EntityDefinition) { return [ { @@ -135,30 +169,6 @@ export function generateLatestProcessors(definition: EntityDefinition) { value: `${generateLatestIndexName(definition)}`, }, }, - { - pipeline: { - ignore_missing_pipeline: true, - name: `${definition.id}@platform`, - }, - }, - { - pipeline: { - ignore_missing_pipeline: true, - name: `${definition.id}-latest@platform`, - }, - }, - { - pipeline: { - ignore_missing_pipeline: true, - name: `${definition.id}@custom`, - }, - }, - - { - pipeline: { - ignore_missing_pipeline: true, - name: `${definition.id}-latest@custom`, - }, - }, + ...getCustomIngestPipelines(definition), ]; } diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap index 2d0aa66c662e6c..fd4ed11f8cb94b 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_history_template.test.ts.snap @@ -1,6 +1,77 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`generateEntitiesHistoryIndexTemplateConfig(definition) should generate a valid index template 1`] = ` +exports[`generateEntitiesHistoryIndexTemplateConfig(definition) should generate a valid index template for builtin definition 1`] = ` +Object { + "_meta": Object { + "description": "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the history dataset", + "ecs_version": "8.0.0", + "managed": true, + "managed_by": "elastic_entity_model", + }, + "composed_of": Array [ + "entities_v1_history_base", + "entities_v1_entity", + "entities_v1_event", + ], + "ignore_missing_component_templates": Array [], + "index_patterns": Array [ + ".entities.v1.history.builtin_mock_entity_definition.*", + ], + "name": "entities_v1_history_builtin_mock_entity_definition_index_template", + "priority": 200, + "template": Object { + "aliases": Object { + "entities-service-history": Object {}, + }, + "mappings": Object { + "_meta": Object { + "version": "1.6.0", + }, + "date_detection": false, + "dynamic_templates": Array [ + Object { + "strings_as_keyword": Object { + "mapping": Object { + "fields": Object { + "text": Object { + "type": "text", + }, + }, + "ignore_above": 1024, + "type": "keyword", + }, + "match_mapping_type": "string", + }, + }, + Object { + "entity_metrics": Object { + "mapping": Object { + "type": "{dynamic_type}", + }, + "match_mapping_type": Array [ + "long", + "double", + ], + "path_match": "entity.metrics.*", + }, + }, + ], + }, + "settings": Object { + "index": Object { + "codec": "best_compression", + "mapping": Object { + "total_fields": Object { + "limit": 2000, + }, + }, + }, + }, + }, +} +`; + +exports[`generateEntitiesHistoryIndexTemplateConfig(definition) should generate a valid index template for custom definition 1`] = ` Object { "_meta": Object { "description": "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the history dataset", diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_latest_template.test.ts.snap b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_latest_template.test.ts.snap index a3ed6ea45f53dc..9653c5fda96c6a 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_latest_template.test.ts.snap +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/__snapshots__/entities_latest_template.test.ts.snap @@ -1,6 +1,77 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`generateEntitiesLatestIndexTemplateConfig(definition) should generate a valid index template 1`] = ` +exports[`generateEntitiesLatestIndexTemplateConfig(definition) should generate a valid index template for builtin definition 1`] = ` +Object { + "_meta": Object { + "description": "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the latest dataset", + "ecs_version": "8.0.0", + "managed": true, + "managed_by": "elastic_entity_model", + }, + "composed_of": Array [ + "entities_v1_latest_base", + "entities_v1_entity", + "entities_v1_event", + ], + "ignore_missing_component_templates": Array [], + "index_patterns": Array [ + ".entities.v1.latest.builtin_mock_entity_definition", + ], + "name": "entities_v1_latest_builtin_mock_entity_definition_index_template", + "priority": 200, + "template": Object { + "aliases": Object { + "entities-service-latest": Object {}, + }, + "mappings": Object { + "_meta": Object { + "version": "1.6.0", + }, + "date_detection": false, + "dynamic_templates": Array [ + Object { + "strings_as_keyword": Object { + "mapping": Object { + "fields": Object { + "text": Object { + "type": "text", + }, + }, + "ignore_above": 1024, + "type": "keyword", + }, + "match_mapping_type": "string", + }, + }, + Object { + "entity_metrics": Object { + "mapping": Object { + "type": "{dynamic_type}", + }, + "match_mapping_type": Array [ + "long", + "double", + ], + "path_match": "entity.metrics.*", + }, + }, + ], + }, + "settings": Object { + "index": Object { + "codec": "best_compression", + "mapping": Object { + "total_fields": Object { + "limit": 2000, + }, + }, + }, + }, + }, +} +`; + +exports[`generateEntitiesLatestIndexTemplateConfig(definition) should generate a valid index template for custom definition 1`] = ` Object { "_meta": Object { "description": "Index template for indices managed by the Elastic Entity Model's entity discovery framework for the latest dataset", diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.test.ts index 33a934032c6867..72e8d8591ab2dd 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.test.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.test.ts @@ -5,12 +5,17 @@ * 2.0. */ -import { entityDefinition } from '../helpers/fixtures/entity_definition'; +import { entityDefinition, builtInEntityDefinition } from '../helpers/fixtures'; import { generateEntitiesHistoryIndexTemplateConfig } from './entities_history_template'; describe('generateEntitiesHistoryIndexTemplateConfig(definition)', () => { - it('should generate a valid index template', () => { + it('should generate a valid index template for custom definition', () => { const template = generateEntitiesHistoryIndexTemplateConfig(entityDefinition); expect(template).toMatchSnapshot(); }); + + it('should generate a valid index template for builtin definition', () => { + const template = generateEntitiesHistoryIndexTemplateConfig(builtInEntityDefinition); + expect(template).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.ts index 5fa88367c04d1c..b1539d8108a6de 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_history_template.ts @@ -32,12 +32,12 @@ export const generateEntitiesHistoryIndexTemplateConfig = ( managed: true, managed_by: 'elastic_entity_model', }, - ignore_missing_component_templates: getCustomHistoryTemplateComponents(definition.id), + ignore_missing_component_templates: getCustomHistoryTemplateComponents(definition), composed_of: [ ENTITY_HISTORY_BASE_COMPONENT_TEMPLATE_V1, ENTITY_ENTITY_COMPONENT_TEMPLATE_V1, ENTITY_EVENT_COMPONENT_TEMPLATE_V1, - ...getCustomHistoryTemplateComponents(definition.id), + ...getCustomHistoryTemplateComponents(definition), ], index_patterns: [ `${entitiesIndexPattern({ diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.test.ts index 44dc91b72da449..bce0265cb0dee1 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.test.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.test.ts @@ -5,12 +5,17 @@ * 2.0. */ -import { entityDefinition } from '../helpers/fixtures/entity_definition'; +import { entityDefinition, builtInEntityDefinition } from '../helpers/fixtures'; import { generateEntitiesLatestIndexTemplateConfig } from './entities_latest_template'; describe('generateEntitiesLatestIndexTemplateConfig(definition)', () => { - it('should generate a valid index template', () => { + it('should generate a valid index template for custom definition', () => { const template = generateEntitiesLatestIndexTemplateConfig(entityDefinition); expect(template).toMatchSnapshot(); }); + + it('should generate a valid index template for builtin definition', () => { + const template = generateEntitiesLatestIndexTemplateConfig(builtInEntityDefinition); + expect(template).toMatchSnapshot(); + }); }); diff --git a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.ts b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.ts index b4eeb18d9435c6..ea476cf7696442 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/lib/entities/templates/entities_latest_template.ts @@ -32,12 +32,12 @@ export const generateEntitiesLatestIndexTemplateConfig = ( managed: true, managed_by: 'elastic_entity_model', }, - ignore_missing_component_templates: getCustomLatestTemplateComponents(definition.id), + ignore_missing_component_templates: getCustomLatestTemplateComponents(definition), composed_of: [ ENTITY_LATEST_BASE_COMPONENT_TEMPLATE_V1, ENTITY_ENTITY_COMPONENT_TEMPLATE_V1, ENTITY_EVENT_COMPONENT_TEMPLATE_V1, - ...getCustomLatestTemplateComponents(definition.id), + ...getCustomLatestTemplateComponents(definition), ], index_patterns: [ entitiesIndexPattern({ diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.test.ts b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.test.ts index 3321ee39edeb42..90c5e90d43f3ad 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.test.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.test.ts @@ -5,12 +5,12 @@ * 2.0. */ +import { EntityDefinition } from '@kbn/entities-schema'; import { getCustomHistoryTemplateComponents, getCustomLatestTemplateComponents } from './helpers'; describe('helpers', () => { it('getCustomLatestTemplateComponents should return template component in the right sort order', () => { - const definitionId = 'test'; - const result = getCustomLatestTemplateComponents(definitionId); + const result = getCustomLatestTemplateComponents({ id: 'test' } as EntityDefinition); expect(result).toEqual([ 'test@platform', 'test-latest@platform', @@ -20,8 +20,7 @@ describe('helpers', () => { }); it('getCustomHistoryTemplateComponents should return template component in the right sort order', () => { - const definitionId = 'test'; - const result = getCustomHistoryTemplateComponents(definitionId); + const result = getCustomHistoryTemplateComponents({ id: 'test' } as EntityDefinition); expect(result).toEqual([ 'test@platform', 'test-history@platform', diff --git a/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.ts b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.ts index e976a216da97ba..23cc7cccb6a131 100644 --- a/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.ts +++ b/x-pack/plugins/observability_solution/entity_manager/server/templates/components/helpers.ts @@ -5,16 +5,31 @@ * 2.0. */ -export const getCustomLatestTemplateComponents = (definitionId: string) => [ - `${definitionId}@platform`, // @platform goes before so it can be overwritten by custom - `${definitionId}-latest@platform`, - `${definitionId}@custom`, - `${definitionId}-latest@custom`, -]; +import { EntityDefinition } from '@kbn/entities-schema'; +import { isBuiltinDefinition } from '../../lib/entities/helpers/is_builtin_definition'; -export const getCustomHistoryTemplateComponents = (definitionId: string) => [ - `${definitionId}@platform`, // @platform goes before so it can be overwritten by custom - `${definitionId}-history@platform`, - `${definitionId}@custom`, - `${definitionId}-history@custom`, -]; +export const getCustomLatestTemplateComponents = (definition: EntityDefinition) => { + if (isBuiltinDefinition(definition)) { + return []; + } + + return [ + `${definition.id}@platform`, // @platform goes before so it can be overwritten by custom + `${definition.id}-latest@platform`, + `${definition.id}@custom`, + `${definition.id}-latest@custom`, + ]; +}; + +export const getCustomHistoryTemplateComponents = (definition: EntityDefinition) => { + if (isBuiltinDefinition(definition)) { + return []; + } + + return [ + `${definition.id}@platform`, // @platform goes before so it can be overwritten by custom + `${definition.id}-history@platform`, + `${definition.id}@custom`, + `${definition.id}-history@custom`, + ]; +}; diff --git a/x-pack/plugins/observability_solution/exploratory_view/e2e/synthetics_runner.ts b/x-pack/plugins/observability_solution/exploratory_view/e2e/synthetics_runner.ts index 66183218780f20..bc6222774f0551 100644 --- a/x-pack/plugins/observability_solution/exploratory_view/e2e/synthetics_runner.ts +++ b/x-pack/plugins/observability_solution/exploratory_view/e2e/synthetics_runner.ts @@ -124,7 +124,7 @@ export class SyntheticsRunner { dir: '.journeys/videos', }, }, - match: match === 'undefined' ? '' : match, + grepOpts: { match: match === 'undefined' ? '' : match }, pauseOnError, screenshots: 'only-on-failure', reporter: TestReporter, diff --git a/x-pack/plugins/observability_solution/infra/common/http_api/host_details/get_infra_services.ts b/x-pack/plugins/observability_solution/infra/common/http_api/host_details/get_infra_services.ts index 718513416dad77..9f330567337eb4 100644 --- a/x-pack/plugins/observability_solution/infra/common/http_api/host_details/get_infra_services.ts +++ b/x-pack/plugins/observability_solution/infra/common/http_api/host_details/get_infra_services.ts @@ -8,8 +8,7 @@ import { createLiteralValueFromUndefinedRT, inRangeFromStringRt, - dateRt, - datemathStringRt, + isoToEpochRt, } from '@kbn/io-ts-utils'; import * as rt from 'io-ts'; @@ -17,7 +16,6 @@ export const sizeRT = rt.union([ inRangeFromStringRt(1, 100), createLiteralValueFromUndefinedRT(10), ]); -export const assetDateRT = rt.union([dateRt, datemathStringRt]); export const servicesFiltersRT = rt.strict({ ['host.name']: rt.string, @@ -26,7 +24,7 @@ export const servicesFiltersRT = rt.strict({ export type ServicesFilter = rt.TypeOf; export const GetServicesRequestQueryRT = rt.intersection([ - rt.strict({ from: assetDateRT, to: assetDateRT, filters: rt.string }), + rt.strict({ from: isoToEpochRt, to: isoToEpochRt, filters: rt.string }), rt.partial({ size: sizeRT, validatedFilters: servicesFiltersRT, @@ -37,8 +35,8 @@ export type GetServicesRequestQuery = rt.TypeOf { - const { error, metric } = apmIndices; - const { filters, size = 10, from, to } = options; - const commonFiltersList: QueryDslQueryContainer[] = [ - { - range: { - '@timestamp': { - gte: from, - lte: to, - }, - }, - }, - { - exists: { - field: 'service.name', - }, - }, - ]; - - if (filters['host.name']) { - // also query for host.hostname field along with host.name, as some services may use this field - const HOST_HOSTNAME_FIELD = 'host.hostname'; - commonFiltersList.push({ - bool: { - should: [ - ...termQuery(HOST_NAME_FIELD, filters[HOST_NAME_FIELD]), - ...termQuery(HOST_HOSTNAME_FIELD, filters[HOST_NAME_FIELD]), - ], - minimum_should_match: 1, - }, - }); - } - const aggs = { - services: { - terms: { - field: 'service.name', - size, - }, - aggs: { - latestAgent: { - top_metrics: { - metrics: [{ field: 'agent.name' }], - sort: { - '@timestamp': 'desc', - }, - size: 1, - }, - }, - }, - }, - }; - // get services from transaction metrics - const metricsQuery = { - size: 0, - _source: false, - query: { - bool: { - filter: [ - { - term: { - [PROCESSOR_EVENT]: 'metric', - }, - }, - { - bool: { - should: [ - { - term: { - 'metricset.name': 'app', - }, - }, - { - bool: { - must: [ - { - term: { - 'metricset.name': 'transaction', - }, - }, - { - term: { - 'metricset.interval': '1m', // make this dynamic if we start returning time series data - }, - }, - ], - }, - }, - ], - minimum_should_match: 1, - }, - }, - ...commonFiltersList, - ], - }, - }, - aggs, - }; - // get services from logs - const logsQuery = { - size: 0, - _source: false, - query: { - bool: { - filter: commonFiltersList, - }, - }, - aggs, - }; - - const resultMetrics = await client<{}, ServicesAPIQueryAggregation>({ - body: metricsQuery, - index: [metric], - }); - const resultLogs = await client<{}, ServicesAPIQueryAggregation>({ - body: logsQuery, - index: [error], - }); - - const servicesListBucketsFromMetrics = resultMetrics.aggregations?.services?.buckets || []; - const servicesListBucketsFromLogs = resultLogs.aggregations?.services?.buckets || []; - const serviceMap = [...servicesListBucketsFromMetrics, ...servicesListBucketsFromLogs].reduce( - (acc, bucket) => { - const serviceName = bucket.key; - const latestAgentEntry = bucket.latestAgent.top[0]; - const latestTimestamp = latestAgentEntry.sort[0]; - const agentName = latestAgentEntry.metrics['agent.name']; - // dedup and get the latest timestamp - const existingService = acc.get(serviceName); - if (!existingService || existingService.latestTimestamp < latestTimestamp) { - acc.set(serviceName, { latestTimestamp, agentName }); - } - - return acc; - }, - new Map () - ); - - const services = Array.from(serviceMap) - .slice(0, size) - .map(([serviceName, { agentName }]) => ({ - serviceName, - agentName, - })); - return { services }; -}; diff --git a/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts b/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts index 86af345d5175e8..9673b317884872 100644 --- a/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts +++ b/x-pack/plugins/observability_solution/infra/server/routes/services/index.ts @@ -6,15 +6,14 @@ */ import { - GetServicesRequestQueryRT, GetServicesRequestQuery, + GetServicesRequestQueryRT, ServicesAPIResponseRT, } from '../../../common/http_api/host_details'; import { InfraBackendLibs } from '../../lib/infra_types'; -import { getServices } from '../../lib/host_details/get_services'; import { validateStringAssetFilters } from './lib/utils'; -import { createSearchClient } from '../../lib/create_search_client'; import { buildRouteValidationWithExcess } from '../../utils/route_validation'; +import { getApmDataAccessClient } from '../../lib/helpers/get_apm_data_access_client'; export const initServicesRoute = (libs: InfraBackendLibs) => { const { framework } = libs; @@ -33,18 +32,34 @@ export const initServicesRoute = (libs: InfraBackendLibs) => { }, }, }, - async (requestContext, request, response) => { - const [{ savedObjects }] = await libs.getStartServices(); + async (context, request, response) => { const { from, to, size = 10, validatedFilters } = request.query; - const client = createSearchClient(requestContext, framework, request); - const soClient = savedObjects.getScopedClient(request); - const apmIndices = await libs.plugins.apmDataAccess.setup.getApmIndices(soClient); - const services = await getServices(client, apmIndices, { - from, - to, - size, + const apmDataAccessClient = getApmDataAccessClient({ request, libs, context }); + const hasApmPrivileges = await apmDataAccessClient.hasPrivileges(); + + if (!hasApmPrivileges) { + return response.customError({ + statusCode: 403, + body: { + message: 'APM data access service is not available', + }, + }); + } + + const apmDataAccessServices = await apmDataAccessClient.getServices(); + + const apmDocumentSources = await apmDataAccessServices.getDocumentSources({ + start: from, + end: to, + }); + + const services = await apmDataAccessServices?.getHostServices({ + documentSources: apmDocumentSources, + start: from, + end: to, filters: validatedFilters!, + size, }); return response.ok({ body: ServicesAPIResponseRT.encode(services), diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/fields/status_field.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/fields/status_field.tsx index aa3f071b36dec4..dc392aea28195f 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/fields/status_field.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/fields/status_field.tsx @@ -5,8 +5,9 @@ * 2.0. */ -import { EuiIcon, EuiFormRow, EuiComboBox } from '@elastic/eui'; +import { EuiComboBox, EuiFormRow } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { InvestigationResponse } from '@kbn/investigation-shared'; import React from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { InvestigationForm } from '../investigation_edit_form'; @@ -16,16 +17,39 @@ const I18N_STATUS_LABEL = i18n.translate( { defaultMessage: 'Status' } ); +export const statusToColor: Record = { + triage: 'warning', + active: 'danger', + mitigated: 'success', + resolved: 'success', + cancelled: 'default', +}; + const options = [ { - label: 'Ongoing', - value: 'ongoing', - prepend: , + label: 'Triage', + value: 'triage', + color: statusToColor.triage, + }, + { + label: 'Active', + value: 'active', + color: statusToColor.active, + }, + { + label: 'Mitigated', + value: 'mitigated', + color: statusToColor.mitigated, + }, + { + label: 'Resolved', + value: 'resolved', + color: statusToColor.resolved, }, { - label: 'Closed', - value: 'closed', - prepend: , + label: 'Cancelled', + value: 'cancelled', + color: statusToColor.cancelled, }, ]; @@ -51,7 +75,7 @@ export function StatusField() { onChange={(selected) => { return field.onChange(selected[0].value); }} - singleSelection={{ asPlainText: true }} + singleSelection /> )} /> diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/fields/tags_field.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/fields/tags_field.tsx new file mode 100644 index 00000000000000..fb6555de53f341 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/fields/tags_field.tsx @@ -0,0 +1,74 @@ +/* + * 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 { EuiFormRow, EuiComboBox } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import React from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; +import { InvestigationForm } from '../investigation_edit_form'; + +const I18N_TAGS_LABEL = i18n.translate( + 'xpack.investigateApp.investigationEditForm.span.tagsLabel', + { defaultMessage: 'Tags' } +); + +export function TagsField() { + const { control, getFieldState } = useFormContext (); + + return ( + + + ); +} + +function generateTagOptions(tags: string[] = []) { + return tags.map((tag) => ({ + label: tag, + value: tag, + })); +} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/investigation_edit_form.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/investigation_edit_form.tsx index 5ea7486c2f6123..40d845533fe0a6 100644 --- a/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/investigation_edit_form.tsx +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_edit_form/investigation_edit_form.tsx @@ -20,21 +20,24 @@ import { EuiTitle, } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; +import { InvestigationResponse } from '@kbn/investigation-shared'; import { pick } from 'lodash'; import React from 'react'; import { Controller, FormProvider, useForm } from 'react-hook-form'; import { v4 as uuidv4 } from 'uuid'; +import { paths } from '../../../common/paths'; import { useCreateInvestigation } from '../../hooks/use_create_investigation'; import { useFetchInvestigation } from '../../hooks/use_fetch_investigation'; +import { useKibana } from '../../hooks/use_kibana'; import { useUpdateInvestigation } from '../../hooks/use_update_investigation'; import { InvestigationNotFound } from '../investigation_not_found/investigation_not_found'; import { StatusField } from './fields/status_field'; -import { useKibana } from '../../hooks/use_kibana'; -import { paths } from '../../../common/paths'; +import { TagsField } from './fields/tags_field'; export interface InvestigationForm { title: string; - status: 'ongoing' | 'closed'; + status: InvestigationResponse['status']; + tags: string[]; } interface Props { @@ -55,15 +58,14 @@ export function InvestigationEditForm({ investigationId, onClose }: Props) { data: investigation, isLoading, isError, - refetch, } = useFetchInvestigation({ id: investigationId }); const { mutateAsync: updateInvestigation } = useUpdateInvestigation(); const { mutateAsync: createInvestigation } = useCreateInvestigation(); const methods = useForm( + { + if (selected.length) { + return field.onChange(selected.map((opts) => opts.value)); + } + + field.onChange([]); + }} + onCreateOption={(searchValue: string) => { + const normalizedSearchValue = searchValue.trim().toLowerCase(); + if (!normalizedSearchValue) { + return; + } + + const values = field.value ?? []; + const tagAlreadyExists = values.find( + (tag) => tag.trim().toLowerCase() === normalizedSearchValue + ); + + if (!tagAlreadyExists) { + field.onChange([...values, searchValue]); + } + }} + /> + )} + /> + ({ - defaultValues: { title: 'New investigation', status: 'ongoing' }, - values: investigation ? pick(investigation, ['title', 'status']) : undefined, + defaultValues: { title: 'New investigation', status: 'triage', tags: [] }, + values: investigation ? pick(investigation, ['title', 'status', 'tags']) : undefined, mode: 'all', }); @@ -79,9 +81,8 @@ export function InvestigationEditForm({ investigationId, onClose }: Props) { if (isEditing) { await updateInvestigation({ investigationId: investigationId!, - payload: { title: data.title, status: data.status }, + payload: { title: data.title, status: data.status, tags: data.tags }, }); - refetch(); onClose(); } else { const resp = await createInvestigation({ @@ -93,6 +94,7 @@ export function InvestigationEditForm({ investigationId, onClose }: Props) { to: new Date().getTime(), }, }, + tags: data.tags, origin: { type: 'blank', }, @@ -147,8 +149,13 @@ export function InvestigationEditForm({ investigationId, onClose }: Props) { />
- {i18n.translate('xpack.investigateApp.investigationEditForm.p.thereWasAnErrorLabel', { + {i18n.translate('xpack.investigateApp.InvestigationNotFound.body', { defaultMessage: - 'There was an error loading the Investigation. Contact your administrator for help.', + 'There was an error loading the investigation. Contact your administrator for help.', })}
} diff --git a/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_status_badge/investigation_status_badge.tsx b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_status_badge/investigation_status_badge.tsx new file mode 100644 index 00000000000000..ece4757a4a1a51 --- /dev/null +++ b/x-pack/plugins/observability_solution/investigate_app/public/components/investigation_status_badge/investigation_status_badge.tsx @@ -0,0 +1,19 @@ +/* + * 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 { EuiBadge } from '@elastic/eui'; +import { InvestigationResponse } from '@kbn/investigation-shared'; +import React from 'react'; +import { statusToColor } from '../investigation_edit_form/fields/status_field'; + +interface Props { + status: InvestigationResponse['status']; +} + +export function InvestigationStatusBadge({ status }: Props) { + return{description}
} + > +