From b79855d0e96c444b06537a98c371c3c808c4ceac Mon Sep 17 00:00:00 2001 From: Tre Date: Mon, 30 Sep 2024 14:38:06 +0100 Subject: [PATCH 1/8] [FTR][Ownership] Assign Ownership to canvas/logstash_lens ES Archives (#194412) ## Summary Modify code owner declarations for `x-pack/test/functional/es_archives/canvas/logstash_lens` in .github/CODEOWNERS ### For reviewers To verify this pr, you can use the `scripts/get_owners_for_file.js` script E.g: ``` node scripts/get_owners_for_file.js --file x-pack/test/functional/es_archives/canvas/logstash_lens # Or any other file ``` #### Notes All of these are a best guess effort. The more help from the dev teams, the more accurate this will be for reporting in the future. Contributes to: https://github.com/elastic/kibana/issues/192979 --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 7f8f48e638148..0988c59ab7d23 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1239,6 +1239,7 @@ x-pack/test/observability_ai_assistant_functional @elastic/obs-ai-assistant /x-pack/test/functional/apps/canvas/ @elastic/kibana-presentation /x-pack/test_serverless/functional/test_suites/search/dashboards/ @elastic/kibana-presentation /test/plugin_functional/test_suites/panel_actions @elastic/kibana-presentation +/x-pack/test/functional/es_archives/canvas/logstash_lens @elastic/kibana-presentation #CC# /src/plugins/kibana_react/public/code_editor/ @elastic/kibana-presentation # Machine Learning From 396931f5056600e633dba64dab81a66096d05f72 Mon Sep 17 00:00:00 2001 From: Christos Nasikas Date: Mon, 30 Sep 2024 17:11:00 +0300 Subject: [PATCH 2/8] [ResponseOps][Alerts] Fix authorization issues with `discover` as consumers (#192321) ## Summary Alerts use its own RBAC model. The RBAC relies on a property called `consumer`. The consumer is tight coupled with the feature ID. It denotes the user's access to the rule and the alerts. For example, a user with access to the "Logs" feature has access only to alerts and rules with the `consumer` set as `logs`. Users can create an ES Query rule from Discover. When the feature was [implemented](https://github.com/elastic/kibana/pull/124534) (v8.3.0) the consumer was set to `discover`. Then it [changed](https://github.com/elastic/kibana/pull/166032) (v8.11.0) to `stackAlerts` (visible only on the stack management page) and then [to](https://github.com/elastic/kibana/pull/171364) (v8.12.0) `alerts` so it can be visible in Observability. Users who created rules that generated alerts with the `discover` consumer cannot see the alerts generated by the rule when they upgrade Kibana to 8.11+ even as superusers. This PR fixes the issues around the `discover` consumer. I added the following alert document to the `data.json.gz` to test for alerts with `discover` consumer. ``` { "type": "doc", "value": { "id": "1b75bfe9-d2f5-47e9-bac6-b082dd9c9e97", "index": ".internal.alerts-stack.alerts-default-000001", "source": { "@timestamp": "2021-10-19T14:00:38.749Z", "event.action": "active", "event.kind": "signal", "kibana.alert.duration.us": 1370302000, "kibana.alert.evaluation.threshold": -1, "kibana.alert.evaluation.value": 80, "kibana.alert.instance.id": "query matched", "kibana.alert.reason": "Document count is 80 in the last 100d in .kibana_alerting_cases index. Alert when greater than -1.", "kibana.alert.rule.category": "Elasticsearch query", "kibana.alert.rule.consumer": "discover", "kibana.alert.rule.name": "EsQuery discover", "kibana.alert.rule.producer": "stackAlerts", "kibana.alert.rule.rule_type_id": ".es-query", "kibana.alert.rule.uuid": "25c14920-faa7-4a9a-830c-ce32c8211237", "kibana.alert.start": "2021-10-19T15:00:41.555Z", "kibana.alert.status": "active", "kibana.alert.time_range": { "gte": "2021-10-19T15:00:41.555Z" }, "kibana.alert.uuid": "23237979-75bf-4b68-a210-ce5056b93356", "kibana.alert.workflow_status": "open", "kibana.space_ids": [ "default" ], "kibana.version": "8.0.0", "tags": [] } } } ``` ## Testing 1. Create a rule with the consumer as `discover`. See https://github.com/elastic/kibana/issues/184595 for instructions. 2. Go to the rule details page. 3. Verify that you do not get any error toaster and you can see the alerts. Fixes: https://github.com/elastic/kibana/issues/184595 ### Checklist Delete any items that are not applicable to this PR. - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios ### For maintainers - [x] This was checked for breaking API changes and was [labeled appropriately](https://www.elastic.co/guide/en/kibana/master/contributing.html#kibana-release-notes-process) ## Release notes Fix an issue with rules not being accessible created from Discover before 8.11.0. --------- Co-authored-by: Elastic Machine --- .../alerting_authorization.test.ts | 90 +++++++++--------- .../authorization/alerting_authorization.ts | 28 +++++- .../alerts_table/cells/render_cell_value.tsx | 2 +- .../observability/alerts/data.json.gz | Bin 4223 -> 4610 bytes .../common/lib/authentication/roles.ts | 18 ++++ .../common/lib/authentication/users.ts | 8 ++ .../tests/basic/search_strategy.ts | 81 ++++++++++++++++ 7 files changed, 180 insertions(+), 47 deletions(-) diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts index 569fff83eaefe..f1cfb99a6daaa 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.test.ts @@ -5,7 +5,7 @@ * 2.0. */ -import { fromKueryExpression } from '@kbn/es-query'; +import { KueryNode, fromKueryExpression, toKqlExpression } from '@kbn/es-query'; import { KibanaRequest } from '@kbn/core/server'; import { ruleTypeRegistryMock } from '../rule_type_registry.mock'; import { securityMock } from '@kbn/security-plugin/server/mocks'; @@ -910,20 +910,19 @@ describe('AlertingAuthorization', () => { getSpaceId, }); ruleTypeRegistry.list.mockReturnValue(setOfAlertTypes); - expect( - ( - await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { - type: AlertingAuthorizationFilterType.KQL, - fieldNames: { - ruleTypeId: 'path.to.rule_type_id', - consumer: 'consumer-field', - }, - }) - ).filter - ).toEqual( - fromKueryExpression( - `((path.to.rule_type_id:myAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:mySecondAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)) or (path.to.rule_type_id:myOtherAppAlertType and consumer-field:(alerts or myApp or myOtherApp or myAppWithSubFeature)))` - ) + + const filter = ( + await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule_type_id', + consumer: 'consumer-field', + }, + }) + ).filter; + + expect(toKqlExpression(filter as KueryNode)).toMatchInlineSnapshot( + `"((path.to.rule_type_id: myAppAlertType AND (consumer-field: alerts OR consumer-field: discover OR consumer-field: myApp OR consumer-field: myOtherApp OR consumer-field: myAppWithSubFeature)) OR (path.to.rule_type_id: mySecondAppAlertType AND (consumer-field: alerts OR consumer-field: discover OR consumer-field: myApp OR consumer-field: myOtherApp OR consumer-field: myAppWithSubFeature)) OR (path.to.rule_type_id: myOtherAppAlertType AND (consumer-field: alerts OR consumer-field: discover OR consumer-field: myApp OR consumer-field: myOtherApp OR consumer-field: myAppWithSubFeature)))"` ); }); test('throws if user has no privileges to any rule type', async () => { @@ -1274,6 +1273,10 @@ describe('AlertingAuthorization', () => { "all": true, "read": true, }, + "discover": Object { + "all": true, + "read": true, + }, "myApp": Object { "all": true, "read": true, @@ -1311,6 +1314,10 @@ describe('AlertingAuthorization', () => { "all": true, "read": true, }, + "discover": Object { + "all": true, + "read": true, + }, "myApp": Object { "all": true, "read": true, @@ -2251,20 +2258,18 @@ describe('AlertingAuthorization', () => { }); }); test('creates a filter based on the privileged types', async () => { - expect( - ( - await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { - type: AlertingAuthorizationFilterType.KQL, - fieldNames: { - ruleTypeId: 'path.to.rule_type_id', - consumer: 'consumer-field', - }, - }) - ).filter - ).toEqual( - fromKueryExpression( - `path.to.rule_type_id:.esQuery and consumer-field:(alerts or stackAlerts or discover)` - ) + const filter = ( + await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule_type_id', + consumer: 'consumer-field', + }, + }) + ).filter; + + expect(toKqlExpression(filter as KueryNode)).toMatchInlineSnapshot( + `"(path.to.rule_type_id: .esQuery AND (consumer-field: alerts OR consumer-field: discover OR consumer-field: stackAlerts))"` ); }); }); @@ -2557,21 +2562,20 @@ describe('AlertingAuthorization', () => { expect(ruleTypeRegistry.get).toHaveBeenCalledTimes(1); }); }); + test('creates a filter based on the privileged types', async () => { - expect( - ( - await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { - type: AlertingAuthorizationFilterType.KQL, - fieldNames: { - ruleTypeId: 'path.to.rule_type_id', - consumer: 'consumer-field', - }, - }) - ).filter - ).toEqual( - fromKueryExpression( - `(path.to.rule_type_id:.esQuery and consumer-field:(alerts or stackAlerts or logs or discover)) or (path.to.rule_type_id:.logs-threshold-o11y and consumer-field:(alerts or stackAlerts or logs or discover)) or (path.to.rule_type_id:.threshold-rule-o11y and consumer-field:(alerts or stackAlerts or logs or discover))` - ) + const filter = ( + await alertAuthorization.getFindAuthorizationFilter(AlertingAuthorizationEntity.Rule, { + type: AlertingAuthorizationFilterType.KQL, + fieldNames: { + ruleTypeId: 'path.to.rule_type_id', + consumer: 'consumer-field', + }, + }) + ).filter; + + expect(toKqlExpression(filter as KueryNode)).toMatchInlineSnapshot( + `"((path.to.rule_type_id: .esQuery AND (consumer-field: alerts OR consumer-field: discover OR consumer-field: stackAlerts OR consumer-field: logs)) OR (path.to.rule_type_id: .logs-threshold-o11y AND (consumer-field: alerts OR consumer-field: discover OR consumer-field: stackAlerts OR consumer-field: logs)) OR (path.to.rule_type_id: .threshold-rule-o11y AND (consumer-field: alerts OR consumer-field: discover OR consumer-field: stackAlerts OR consumer-field: logs)))"` ); }); }); diff --git a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts index 0fb53c1eab0a1..6b24f2f5de9a4 100644 --- a/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts +++ b/x-pack/plugins/alerting/server/authorization/alerting_authorization.ts @@ -13,6 +13,7 @@ import { KueryNode } from '@kbn/es-query'; import { SecurityPluginSetup } from '@kbn/security-plugin/server'; import { FeaturesPluginStart } from '@kbn/features-plugin/server'; import { Space } from '@kbn/spaces-plugin/server'; +import { STACK_ALERTS_FEATURE_ID } from '@kbn/rule-data-utils'; import { RegistryRuleType } from '../rule_type_registry'; import { ALERTING_FEATURE_ID, RuleTypeRegistry } from '../types'; import { @@ -88,6 +89,8 @@ export interface ConstructorOptions { authorization?: SecurityPluginSetup['authz']; } +const DISCOVER_FEATURE_ID = 'discover'; + export class AlertingAuthorization { private readonly ruleTypeRegistry: RuleTypeRegistry; private readonly request: KibanaRequest; @@ -135,7 +138,7 @@ export class AlertingAuthorization { this.allPossibleConsumers = this.featuresIds.then((featuresIds) => { return featuresIds.size - ? asAuthorizedConsumers([ALERTING_FEATURE_ID, ...featuresIds], { + ? asAuthorizedConsumers([ALERTING_FEATURE_ID, DISCOVER_FEATURE_ID, ...featuresIds], { read: true, all: true, }) @@ -328,7 +331,22 @@ export class AlertingAuthorization { hasAllRequested: boolean; authorizedRuleTypes: Set; }> { - const fIds = featuresIds ?? (await this.featuresIds); + const fIds = new Set(featuresIds ?? (await this.featuresIds)); + + /** + * Temporary hack to fix issues with the discover consumer. + * Issue: https://github.com/elastic/kibana/issues/184595. + * PR https://github.com/elastic/kibana/pull/183756 will + * remove the hack and fix it in a generic way. + * + * The discover consumer should be authorized + * as the stackAlerts consumer. + */ + if (fIds.has(DISCOVER_FEATURE_ID)) { + fIds.delete(DISCOVER_FEATURE_ID); + fIds.add(STACK_ALERTS_FEATURE_ID); + } + if (this.authorization && this.shouldCheckAuthorization()) { const checkPrivileges = this.authorization.checkPrivilegesDynamicallyWithRequest( this.request @@ -347,11 +365,15 @@ export class AlertingAuthorization { >(); const allPossibleConsumers = await this.allPossibleConsumers; const addLegacyConsumerPrivileges = (legacyConsumer: string) => - legacyConsumer === ALERTING_FEATURE_ID || isEmpty(featuresIds); + legacyConsumer === ALERTING_FEATURE_ID || + legacyConsumer === DISCOVER_FEATURE_ID || + isEmpty(featuresIds); + for (const feature of fIds) { const featureDef = this.features .getKibanaFeatures() .find((kFeature) => kFeature.id === feature); + for (const ruleTypeId of featureDef?.alerting ?? []) { const ruleTypeAuth = ruleTypesWithAuthorization.find((rtwa) => rtwa.id === ruleTypeId); if (ruleTypeAuth) { diff --git a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/render_cell_value.tsx b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/render_cell_value.tsx index 2d76d4cd19aba..ad82a9d3ee97c 100644 --- a/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/render_cell_value.tsx +++ b/x-pack/plugins/triggers_actions_ui/public/application/sections/alerts_table/cells/render_cell_value.tsx @@ -138,7 +138,7 @@ export function getAlertFormatters(fieldFormats: FieldFormatsRegistry) { const producer = rowData?.find(({ field }) => field === ALERT_RULE_PRODUCER)?.value?.[0]; const consumer: AlertConsumers = observabilityFeatureIds.includes(producer) ? 'observability' - : producer && (value === 'alerts' || value === 'stackAlerts') + : producer && (value === 'alerts' || value === 'stackAlerts' || value === 'discover') ? producer : value; const consumerData = alertProducersData[consumer]; diff --git a/x-pack/test/functional/es_archives/observability/alerts/data.json.gz b/x-pack/test/functional/es_archives/observability/alerts/data.json.gz index 72d24a0b36668d9c5aeda8be16e998f8cd4730b6..4017aaab1bddf1ca9ba2582b819b813dfdc42e2d 100644 GIT binary patch literal 4610 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 literal 4223 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 V6Ve`%GWrNF{|_Dp+$ZP!004?fEG7T| diff --git a/x-pack/test/rule_registry/common/lib/authentication/roles.ts b/x-pack/test/rule_registry/common/lib/authentication/roles.ts index 89878a10cc6d0..df887ea463e29 100644 --- a/x-pack/test/rule_registry/common/lib/authentication/roles.ts +++ b/x-pack/test/rule_registry/common/lib/authentication/roles.ts @@ -265,6 +265,23 @@ export const logsOnlyAllSpacesAll: Role = { }, }; +export const stackAlertsOnlyAllSpacesAll: Role = { + name: 'stack_alerts_only_all_spaces_all', + privileges: { + elasticsearch: { + indices: [], + }, + kibana: [ + { + feature: { + stackAlerts: ['all'], + }, + spaces: ['*'], + }, + ], + }, +}; + /** * This role exists to test that the alert search strategy allows * users who do not have access to security solutions the ability @@ -494,6 +511,7 @@ export const allRoles = [ securitySolutionOnlyReadSpacesAll, observabilityOnlyAllSpacesAll, logsOnlyAllSpacesAll, + stackAlertsOnlyAllSpacesAll, observabilityOnlyReadSpacesAll, observabilityOnlyAllSpacesAllWithReadESIndices, observabilityMinReadAlertsRead, diff --git a/x-pack/test/rule_registry/common/lib/authentication/users.ts b/x-pack/test/rule_registry/common/lib/authentication/users.ts index 2a63c296d842a..3d418ab9e779d 100644 --- a/x-pack/test/rule_registry/common/lib/authentication/users.ts +++ b/x-pack/test/rule_registry/common/lib/authentication/users.ts @@ -30,6 +30,7 @@ import { observabilityMinReadAlertsAllSpacesAll, observabilityOnlyAllSpacesAllWithReadESIndices, securitySolutionOnlyAllSpacesAllWithReadESIndices, + stackAlertsOnlyAllSpacesAll, } from './roles'; import { User } from './types'; @@ -176,6 +177,12 @@ export const logsOnlySpacesAll: User = { roles: [logsOnlyAllSpacesAll.name], }; +export const stackAlertsOnlySpacesAll: User = { + username: 'stack_alerts_only_all_spaces_all', + password: 'stack_alerts_only_all_spaces_all', + roles: [stackAlertsOnlyAllSpacesAll.name], +}; + export const obsOnlySpacesAllEsRead: User = { username: 'obs_only_all_spaces_all_es_read', password: 'obs_only_all_spaces_all_es_read', @@ -290,6 +297,7 @@ export const allUsers = [ secOnlyReadSpacesAll, obsOnlySpacesAll, logsOnlySpacesAll, + stackAlertsOnlySpacesAll, obsSecSpacesAll, obsSecReadSpacesAll, obsMinReadAlertsRead, diff --git a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts index cbdd9d2301294..2fe2567f5e523 100644 --- a/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts +++ b/x-pack/test/rule_registry/security_and_spaces/tests/basic/search_strategy.ts @@ -13,6 +13,8 @@ import { obsOnlySpacesAll, logsOnlySpacesAll, secOnlySpacesAllEsReadAll, + stackAlertsOnlySpacesAll, + superUser, } from '../../../common/lib/authentication/users'; type RuleRegistrySearchResponseWithErrors = RuleRegistrySearchResponse & { @@ -346,6 +348,85 @@ export default ({ getService }: FtrProviderContext) => { }); }); + describe('discover', () => { + before(async () => { + await esArchiver.load('x-pack/test/functional/es_archives/observability/alerts'); + }); + after(async () => { + await esArchiver.unload('x-pack/test/functional/es_archives/observability/alerts'); + }); + + it('should return alerts from .es-query rule type with consumer discover with access only to stack rules', async () => { + const result = await secureBsearch.send({ + supertestWithoutAuth, + auth: { + username: stackAlertsOnlySpacesAll.username, + password: stackAlertsOnlySpacesAll.password, + }, + referer: 'test', + kibanaVersion, + internalOrigin: 'Kibana', + options: { + featureIds: ['discover'], + }, + strategy: 'privateRuleRegistryAlertsSearchStrategy', + }); + + expect(result.rawResponse.hits.total).to.eql(1); + + const consumers = result.rawResponse.hits.hits.map((hit) => { + return hit.fields?.['kibana.alert.rule.consumer']; + }); + + expect(consumers.every((consumer) => consumer === 'discover')); + }); + + it('should return alerts from .es-query rule type with consumer discover as superuser', async () => { + const result = await secureBsearch.send({ + supertestWithoutAuth, + auth: { + username: superUser.username, + password: superUser.password, + }, + referer: 'test', + kibanaVersion, + internalOrigin: 'Kibana', + options: { + featureIds: ['discover'], + }, + strategy: 'privateRuleRegistryAlertsSearchStrategy', + }); + + expect(result.rawResponse.hits.total).to.eql(1); + + const consumers = result.rawResponse.hits.hits.map((hit) => { + return hit.fields?.['kibana.alert.rule.consumer']; + }); + + expect(consumers.every((consumer) => consumer === 'discover')); + }); + + it('should not return alerts from .es-query rule type with consumer discover without access to stack rules', async () => { + const result = await secureBsearch.send({ + supertestWithoutAuth, + auth: { + username: logsOnlySpacesAll.username, + password: logsOnlySpacesAll.password, + }, + referer: 'test', + kibanaVersion, + internalOrigin: 'Kibana', + options: { + featureIds: ['discover'], + }, + strategy: 'privateRuleRegistryAlertsSearchStrategy', + }); + + expect(result.statusCode).to.be(500); + expect(result.message).to.be('Unauthorized to find alerts for any rule types'); + }); + }); + describe('empty response', () => { it('should return an empty response', async () => { const result = await secureBsearch.send({ From 0dada14ac580088b24a084d1d1b61b08a7055ebb Mon Sep 17 00:00:00 2001 From: Jon Date: Mon, 30 Sep 2024 09:20:16 -0500 Subject: [PATCH 3/8] [cloud deploy] Fix deployment config (#194076) Followup to https://github.com/elastic/kibana/pull/193101 Fixes https://buildkite.com/elastic/kibana-pull-request/builds/237163#01922ad1-38ef-48e6-a0bd-d3f051c4bb0f/537-542 --- .buildkite/scripts/steps/artifacts/cloud.sh | 1 - .buildkite/scripts/steps/cloud/build_and_deploy.sh | 1 - 2 files changed, 2 deletions(-) diff --git a/.buildkite/scripts/steps/artifacts/cloud.sh b/.buildkite/scripts/steps/artifacts/cloud.sh index bc28ceabd1c04..a7a556598ce42 100644 --- a/.buildkite/scripts/steps/artifacts/cloud.sh +++ b/.buildkite/scripts/steps/artifacts/cloud.sh @@ -42,7 +42,6 @@ jq ' .resources.kibana[0].plan.kibana.docker_image = "'$KIBANA_TEST_IMAGE'" | .resources.kibana[0].plan.kibana.version = "'$FULL_VERSION'" | .resources.elasticsearch[0].plan.elasticsearch.version = "'$FULL_VERSION'" | - .resources.enterprise_search[0].plan.enterprise_search.version = "'$FULL_VERSION'" | .resources.integrations_server[0].plan.integrations_server.version = "'$FULL_VERSION'" ' .buildkite/scripts/steps/cloud/deploy.json > "$DEPLOYMENT_SPEC" diff --git a/.buildkite/scripts/steps/cloud/build_and_deploy.sh b/.buildkite/scripts/steps/cloud/build_and_deploy.sh index 6615d0ec4cdd4..25e7d8fc631c9 100755 --- a/.buildkite/scripts/steps/cloud/build_and_deploy.sh +++ b/.buildkite/scripts/steps/cloud/build_and_deploy.sh @@ -68,7 +68,6 @@ if [ -z "${CLOUD_DEPLOYMENT_ID}" ] || [ "${CLOUD_DEPLOYMENT_ID}" = 'null' ]; the .name = "'$CLOUD_DEPLOYMENT_NAME'" | .resources.kibana[0].plan.kibana.version = "'$VERSION'" | .resources.elasticsearch[0].plan.elasticsearch.version = "'$VERSION'" | - .resources.enterprise_search[0].plan.enterprise_search.version = "'$VERSION'" | .resources.integrations_server[0].plan.integrations_server.version = "'$VERSION'" ' .buildkite/scripts/steps/cloud/deploy.json > /tmp/deploy.json From 9f9d911dbacc45729fd6e93bad0e08a761eb8823 Mon Sep 17 00:00:00 2001 From: Irene Blanco Date: Mon, 30 Sep 2024 16:38:45 +0200 Subject: [PATCH 4/8] [Inventory] Promote new inventory plugin in APM (#193997) ## Summary Closes https://github.com/elastic/kibana/issues/192856. This PR removes the ability to enable the new APM experience and replaces it with a link promoting the new Inventory feature. ![Screen Recording 2024-09-25 at 15 02 29](https://github.com/user-attachments/assets/5dc6b8af-f207-43e5-8125-fffb4b823e4d) --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../custom_no_data_page.cy.ts | 40 ---- .../app/service_inventory/index.tsx | 34 +-- .../entities_inventory_callout.tsx | 59 +++++ .../index.tsx} | 119 +++++------ .../templates/custom_no_data_template.tsx | 124 ----------- .../templates/service_group_template.tsx | 8 - .../shared/entity_enablement/index.tsx | 202 ------------------ .../entity_manager_context.tsx | 7 +- .../observability_solution/apm/tsconfig.json | 5 +- .../inventory/public/types.ts | 3 +- .../observability_shared/common/index.ts | 3 + .../entity_inventory_locator.ts | 24 +++ .../common/locators/index.ts | 1 + .../observability_shared/public/plugin.ts | 4 + 14 files changed, 151 insertions(+), 482 deletions(-) delete mode 100644 x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/custom_no_data_page/custom_no_data_page.cy.ts create mode 100644 x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/entities_inventory_callout.tsx rename x-pack/plugins/observability_solution/apm/public/components/routing/templates/{apm_main_template.tsx => apm_main_template/index.tsx} (63%) delete mode 100644 x-pack/plugins/observability_solution/apm/public/components/routing/templates/custom_no_data_template.tsx delete mode 100644 x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/index.tsx create mode 100644 x-pack/plugins/observability_solution/observability_shared/common/locators/entity_inventory/entity_inventory_locator.ts diff --git a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/custom_no_data_page/custom_no_data_page.cy.ts b/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/custom_no_data_page/custom_no_data_page.cy.ts deleted file mode 100644 index a246a2ccb7c34..0000000000000 --- a/x-pack/plugins/observability_solution/apm/ftr_e2e/cypress/e2e/custom_no_data_page/custom_no_data_page.cy.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * 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. - */ - -describe('Custom no data page', () => { - beforeEach(() => { - cy.loginAsEditorUser(); - }); - - before(() => { - // make sure entity centric experience is disabled - cy.updateAdvancedSettings({ - 'observability:entityCentricExperience': false, - }); - }); - - after(() => { - cy.updateAdvancedSettings({ - 'observability:entityCentricExperience': false, - }); - }); - - it('shows the default no data screen when entity centric experience is disabled ', () => { - cy.visitKibana('/app/apm'); - cy.contains('Welcome to Elastic Observability!'); - }); - - it('shows the custom no data screen when entity centric experience is enabled', () => { - cy.updateAdvancedSettings({ - 'observability:entityCentricExperience': true, - }); - cy.visitKibana('/app/apm'); - cy.contains('Welcome to Elastic Observability!').should('not.exist'); - cy.contains('Detect and resolve problems with your application'); - cy.contains('Try collecting services from logs'); - }); -}); diff --git a/x-pack/plugins/observability_solution/apm/public/components/app/service_inventory/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/app/service_inventory/index.tsx index e30c79eb483f7..55f72d21f48d7 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/app/service_inventory/index.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/app/service_inventory/index.tsx @@ -5,40 +5,8 @@ * 2.0. */ import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { EuiEmptyPrompt, EuiLoadingLogo } from '@elastic/eui'; -import { isEmpty } from 'lodash'; import { ApmServiceInventory } from './apm_signal_inventory'; -import { MultiSignalInventory } from './multi_signal_inventory'; -import { useApmParams } from '../../../hooks/use_apm_params'; -import { useEntityManagerEnablementContext } from '../../../context/entity_manager_context/use_entity_manager_enablement_context'; export function ServiceInventory() { - const { isEnablementPending, isEntityCentricExperienceViewEnabled } = - useEntityManagerEnablementContext(); - - const { - query: { serviceGroup }, - } = useApmParams('/services'); - - if (isEnablementPending) { - return ( - } - title={ -

- {i18n.translate('xpack.apm.loadingService', { - defaultMessage: 'Loading services', - })} -

- } - /> - ); - } - - return isEntityCentricExperienceViewEnabled && isEmpty(serviceGroup) ? ( - - ) : ( - - ); + return ; } diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/entities_inventory_callout.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/entities_inventory_callout.tsx new file mode 100644 index 0000000000000..16cc93e2827f2 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/entities_inventory_callout.tsx @@ -0,0 +1,59 @@ +/* + * 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 { EuiFlexGroup, EuiFlexItem, EuiLink, EuiToolTip, EuiButtonIcon } from '@elastic/eui'; +import { TechnicalPreviewBadge } from '@kbn/observability-shared-plugin/public'; +import React from 'react'; +import { FormattedMessage } from '@kbn/i18n-react'; +import { useKibana } from '@kbn/kibana-react-plugin/public'; +import { ApmPluginStartDeps } from '../../../../plugin'; + +interface EntitiesInventoryCalloutProps { + onDissmiss: () => void; +} + +export function EntitiesInventoryCallout({ onDissmiss }: EntitiesInventoryCalloutProps) { + const { services } = useKibana(); + const { observabilityShared } = services; + + const entitiesInventoryLocator = observabilityShared.locators.entitiesInventory; + + return ( + + + + + + + + + + + + } + > + + + + + ); +} diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/index.tsx similarity index 63% rename from x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template.tsx rename to x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/index.tsx index fca4f3809edb5..8633e206599b6 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/apm_main_template/index.tsx @@ -11,24 +11,22 @@ import { entityCentricExperience } from '@kbn/observability-plugin/common'; import { ObservabilityPageTemplateProps } from '@kbn/observability-shared-plugin/public'; import type { KibanaPageTemplateProps } from '@kbn/shared-ux-page-kibana-template'; import React, { useContext } from 'react'; -import { i18n } from '@kbn/i18n'; import { useLocation } from 'react-router-dom'; import { FeatureFeedbackButton } from '@kbn/observability-shared-plugin/public'; -import { useEntityManagerEnablementContext } from '../../../context/entity_manager_context/use_entity_manager_enablement_context'; -import { useDefaultAiAssistantStarterPromptsForAPM } from '../../../hooks/use_default_ai_assistant_starter_prompts_for_apm'; -import { KibanaEnvironmentContext } from '../../../context/kibana_environment_context/kibana_environment_context'; -import { getPathForFeedback } from '../../../utils/get_path_for_feedback'; -import { EnvironmentsContextProvider } from '../../../context/environments_context/environments_context'; -import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher'; -import { ApmPluginStartDeps } from '../../../plugin'; -import { ServiceGroupSaveButton } from '../../app/service_groups'; -import { ServiceGroupsButtonGroup } from '../../app/service_groups/service_groups_button_group'; -import { ApmEnvironmentFilter } from '../../shared/environment_filter'; -import { getNoDataConfig } from './no_data_config'; -import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; -import { EntityEnablement } from '../../shared/entity_enablement'; -import { CustomNoDataTemplate } from './custom_no_data_template'; -import { ServiceInventoryView } from '../../../context/entity_manager_context/entity_manager_context'; +import { useLocalStorage } from '../../../../hooks/use_local_storage'; +import { useEntityManagerEnablementContext } from '../../../../context/entity_manager_context/use_entity_manager_enablement_context'; +import { useDefaultAiAssistantStarterPromptsForAPM } from '../../../../hooks/use_default_ai_assistant_starter_prompts_for_apm'; +import { KibanaEnvironmentContext } from '../../../../context/kibana_environment_context/kibana_environment_context'; +import { getPathForFeedback } from '../../../../utils/get_path_for_feedback'; +import { EnvironmentsContextProvider } from '../../../../context/environments_context/environments_context'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { ApmPluginStartDeps } from '../../../../plugin'; +import { ServiceGroupSaveButton } from '../../../app/service_groups'; +import { ServiceGroupsButtonGroup } from '../../../app/service_groups/service_groups_button_group'; +import { ApmEnvironmentFilter } from '../../../shared/environment_filter'; +import { getNoDataConfig } from '../no_data_config'; +import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context'; +import { EntitiesInventoryCallout } from './entities_inventory_callout'; // Paths that must skip the no data screen const bypassNoDataScreenPaths = ['/settings', '/diagnostics']; @@ -78,8 +76,8 @@ export function ApmMainTemplate({ entityCentricExperience, true ); - const { isEntityCentricExperienceViewEnabled, serviceInventoryViewLocalStorageSetting } = - useEntityManagerEnablementContext(); + + const { isEntityCentricExperienceViewEnabled } = useEntityManagerEnablementContext(); const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate; @@ -117,11 +115,6 @@ export function ApmMainTemplate({ const hasApmData = !!data?.hasData; const hasApmIntegrations = !!fleetApmPoliciesData?.hasApmPolicies; - const showCustomEmptyState = - !hasApmData && - !isLoading && - isEntityCentricExperienceSettingEnabled && - serviceInventoryViewLocalStorageSetting === ServiceInventoryView.classic; const noDataConfig = getNoDataConfig({ basePath, @@ -142,6 +135,7 @@ export function ApmMainTemplate({ const rightSideItems = [...(showServiceGroupSaveButton ? [] : [])]; const sanitizedPath = getPathForFeedback(window.location.pathname); + const pageHeaderTitle = ( {pageHeader?.pageTitle ?? pageTitle} @@ -168,47 +162,44 @@ export function ApmMainTemplate({ ); - const pageTemplate = showCustomEmptyState ? ( - - ) : ( - - {isEntityCentricExperienceSettingEnabled && - showEnablementCallout && - selectedNavButton === 'allServices' ? ( - - ) : null} - {showServiceGroupsNav && selectedNavButton && ( - - )} - - ), - }} - {...pageTemplateProps} - > - {children} - + const [dismissedEntitiesInventoryCallout, setdismissedEntitiesInventoryCallout] = useLocalStorage( + `apm.dismissedEntitiesInventoryCallout`, + false ); - return {pageTemplate}; + const showEntitiesInventoryCallout = + !dismissedEntitiesInventoryCallout && + isEntityCentricExperienceSettingEnabled && + selectedNavButton !== undefined; + + return ( + + + {showEntitiesInventoryCallout ? ( + { + setdismissedEntitiesInventoryCallout(true); + }} + /> + ) : null} + {showServiceGroupsNav && selectedNavButton && ( + + )} + + ), + }} + {...pageTemplateProps} + > + {children} + + + ); } diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/custom_no_data_template.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/custom_no_data_template.tsx deleted file mode 100644 index d006e43c3971b..0000000000000 --- a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/custom_no_data_template.tsx +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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 { - EuiFlexGroup, - EuiFlexItem, - EuiSpacer, - EuiTextColor, - EuiText, - EuiButton, - EuiPageTemplate, - EuiCard, - EuiImage, - EuiScreenReaderOnly, -} from '@elastic/eui'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import React from 'react'; -import { i18n } from '@kbn/i18n'; -import { KibanaSolutionAvatar } from '@kbn/shared-ux-avatar-solution'; -import { NoDataConfig } from '@kbn/shared-ux-page-no-data-config-types'; -import { ApmPluginStartDeps } from '../../../plugin'; -import { EntityEnablement } from '../../shared/entity_enablement'; - -export function CustomNoDataTemplate({ - isPageDataLoaded, - noDataConfig, -}: { - isPageDataLoaded: boolean; - noDataConfig?: NoDataConfig; -}) { - const { services } = useKibana(); - const { http, observabilityShared } = services; - const basePath = http?.basePath.get(); - - const ObservabilityPageTemplate = observabilityShared.navigation.PageTemplate; - const imageUrl = `${basePath}/plugins/kibanaReact/assets/elastic_agent_card.svg`; - - return ( - - - - - - -

- {i18n.translate('xpack.apm.customEmtpyState.title', { - defaultMessage: 'Detect and resolve problems with your application', - })} -

- -

- {i18n.translate('xpack.apm.customEmtpyState.description', { - defaultMessage: - 'Start collecting data for your applications and services so you can detect and resolve problems faster.', - })} -

-
-
- - - - {i18n.translate('xpack.apm.customEmtpyState.title.reader', { - defaultMessage: 'Add APM data', - })} - - - } - description={i18n.translate('xpack.apm.customEmtpyState.card.description', { - defaultMessage: - 'Use APM agents to collect APM data. We make it easy with agents for many popular languages.', - })} - footer={ - - - - {noDataConfig?.action.elasticAgent.title} - - - -

- -

-
-
-
- } - image={ - - } - /> -
-
-
- ); -} diff --git a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/service_group_template.tsx b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/service_group_template.tsx index 67695c6485006..d9fb2437be14c 100644 --- a/x-pack/plugins/observability_solution/apm/public/components/routing/templates/service_group_template.tsx +++ b/x-pack/plugins/observability_solution/apm/public/components/routing/templates/service_group_template.tsx @@ -20,8 +20,6 @@ import { useApmRouter } from '../../../hooks/use_apm_router'; import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; import { ApmMainTemplate } from './apm_main_template'; import { useBreadcrumb } from '../../../context/breadcrumbs/use_breadcrumb'; -import { TechnicalPreviewBadge } from '../../shared/technical_preview_badge'; -import { useEntityManagerEnablementContext } from '../../../context/entity_manager_context/use_entity_manager_enablement_context'; export function ServiceGroupTemplate({ pageTitle, @@ -165,7 +163,6 @@ type ServiceGroupContextTab = NonNullable[0] & { function useTabs(selectedTab: ServiceGroupContextTab['key']) { const router = useApmRouter(); const { query } = useAnyOfApmParams('/services', '/service-map'); - const { isEntityCentricExperienceViewEnabled } = useEntityManagerEnablementContext(); const tabs: ServiceGroupContextTab[] = [ { @@ -180,11 +177,6 @@ function useTabs(selectedTab: ServiceGroupContextTab['key']) { defaultMessage: 'Inventory', })} - - {isEntityCentricExperienceViewEnabled && ( - - )} - ), href: router.link('/services', { query }), diff --git a/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/index.tsx b/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/index.tsx deleted file mode 100644 index 62f5cf708bcf7..0000000000000 --- a/x-pack/plugins/observability_solution/apm/public/components/shared/entity_enablement/index.tsx +++ /dev/null @@ -1,202 +0,0 @@ -/* - * 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 React, { useState } from 'react'; -import { useKibana } from '@kbn/kibana-react-plugin/public'; -import useToggle from 'react-use/lib/useToggle'; -import { - EuiButtonIcon, - EuiFlexGroup, - EuiFlexItem, - EuiLink, - EuiLoadingSpinner, - EuiPopover, - EuiPopoverFooter, - EuiSkeletonText, - EuiText, - EuiTextColor, -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import type { IHttpFetchError, ResponseErrorBody } from '@kbn/core/public'; -import { EntityManagerUnauthorizedError } from '@kbn/entityManager-plugin/public'; -import { TechnicalPreviewBadge } from '../technical_preview_badge'; -import { ApmPluginStartDeps } from '../../../plugin'; -import { useEntityManagerEnablementContext } from '../../../context/entity_manager_context/use_entity_manager_enablement_context'; -import { FeedbackModal } from './feedback_modal'; -import { ServiceInventoryView } from '../../../context/entity_manager_context/entity_manager_context'; -import { Unauthorized } from './unauthorized_modal'; -import { useLocalStorage } from '../../../hooks/use_local_storage'; - -export function EntityEnablement({ label, tooltip }: { label: string; tooltip?: string }) { - const [isFeedbackModalVisible, setsIsFeedbackModalVisible] = useLocalStorage( - 'apm.isFeedbackModalVisible', - undefined - ); - - const [isUnauthorizedModalVisible, setsIsUnauthorizedModalVisible] = useState(false); - - const { - services: { entityManager }, - notifications, - } = useKibana(); - - const { - isEntityManagerEnabled, - isEnablementPending, - refetch, - setServiceInventoryViewLocalStorageSetting, - isEntityCentricExperienceViewEnabled, - tourState, - updateTourState, - } = useEntityManagerEnablementContext(); - - const [isPopoverOpen, togglePopover] = useToggle(false); - const [isLoading, setIsLoading] = useToggle(false); - - const handleRestoreView = async () => { - setServiceInventoryViewLocalStorageSetting(ServiceInventoryView.classic); - if (isFeedbackModalVisible === undefined) { - setsIsFeedbackModalVisible(true); - } - }; - - const handleEnablement = async () => { - if (isEntityManagerEnabled) { - setServiceInventoryViewLocalStorageSetting(ServiceInventoryView.entity); - if (tourState.isModalVisible === undefined) { - updateTourState({ isModalVisible: true }); - } - return; - } - - setIsLoading(true); - try { - const response = await entityManager.entityClient.enableManagedEntityDiscovery(); - if (response.success) { - setIsLoading(false); - setServiceInventoryViewLocalStorageSetting(ServiceInventoryView.entity); - - if (tourState.isModalVisible === undefined) { - updateTourState({ isModalVisible: true }); - } - refetch(); - } else { - throw new Error(response.message); - } - } catch (error) { - setIsLoading(false); - - if (error instanceof EntityManagerUnauthorizedError) { - setsIsUnauthorizedModalVisible(true); - return; - } - - const err = error as Error | IHttpFetchError; - notifications.toasts.danger({ - title: i18n.translate('xpack.apm.eemEnablement.errorTitle', { - defaultMessage: 'Error while enabling the new experience', - }), - body: 'response' in err ? err.body?.message ?? err.response?.statusText : err.message, - }); - } - }; - - const handleOnCloseFeedback = () => { - setsIsFeedbackModalVisible(false); - }; - - return isEnablementPending ? ( - - - - ) : ( - - - {isLoading ? ( - - ) : ( - - )} - - - - {isEntityCentricExperienceViewEnabled - ? i18n.translate('xpack.apm.eemEnablement.enabled.', { - defaultMessage: 'Viewing our new experience', - }) - : label} - - - {tooltip && ( - - - } - isOpen={isPopoverOpen} - closePopover={togglePopover} - anchorPosition="downLeft" - > -
- -

- {i18n.translate('xpack.apm.entityEnablement.content', { - defaultMessage: - 'Our new experience combines both APM-instrumented services with services detected from logs in a single service inventory.', - })} -

-
-
- - - - {i18n.translate('xpack.apm.entityEnablement.footer', { - defaultMessage: 'Learn more', - })} - - - -
-
- )} - {isEntityCentricExperienceViewEnabled && ( - - - {i18n.translate('xpack.apm.eemEnablement.restoreClassicView.', { - defaultMessage: 'Restore classic view', - })} - - - )} - - setsIsUnauthorizedModalVisible(false)} - label={label} - /> -
- ); -} diff --git a/x-pack/plugins/observability_solution/apm/public/context/entity_manager_context/entity_manager_context.tsx b/x-pack/plugins/observability_solution/apm/public/context/entity_manager_context/entity_manager_context.tsx index 93205c907caa0..95a246ddce566 100644 --- a/x-pack/plugins/observability_solution/apm/public/context/entity_manager_context/entity_manager_context.tsx +++ b/x-pack/plugins/observability_solution/apm/public/context/entity_manager_context/entity_manager_context.tsx @@ -62,11 +62,6 @@ export function EntityManagerEnablementContextProvider({ true ); - const isEntityCentricExperienceViewEnabled = - isEntityManagerEnabled && - serviceInventoryViewLocalStorageSetting === ServiceInventoryView.entity && - isEntityCentricExperienceSettingEnabled; - function handleServiceInventoryViewChange(nextView: ServiceInventoryView) { setServiceInventoryViewLocalStorageSetting(nextView); // Updates the telemetry context variable every time the user switches views @@ -88,7 +83,7 @@ export function EntityManagerEnablementContextProvider({ refetch, serviceInventoryViewLocalStorageSetting, setServiceInventoryViewLocalStorageSetting: handleServiceInventoryViewChange, - isEntityCentricExperienceViewEnabled, + isEntityCentricExperienceViewEnabled: isEntityCentricExperienceSettingEnabled, tourState, updateTourState: handleTourStateUpdate, }} diff --git a/x-pack/plugins/observability_solution/apm/tsconfig.json b/x-pack/plugins/observability_solution/apm/tsconfig.json index 9195c2547a71a..6f3ff13a2af3e 100644 --- a/x-pack/plugins/observability_solution/apm/tsconfig.json +++ b/x-pack/plugins/observability_solution/apm/tsconfig.json @@ -120,10 +120,6 @@ "@kbn/test-jest-helpers", "@kbn/security-plugin-types-common", "@kbn/entityManager-plugin", - "@kbn/react-hooks", - "@kbn/shared-ux-avatar-solution", - "@kbn/shared-ux-page-no-data-config-types", - "@kbn/react-hooks", "@kbn/server-route-repository-utils", "@kbn/core-analytics-browser", "@kbn/apm-types", @@ -131,6 +127,7 @@ "@kbn/serverless", "@kbn/aiops-log-rate-analysis", "@kbn/router-utils", + "@kbn/react-hooks", ], "exclude": ["target/**/*"] } diff --git a/x-pack/plugins/observability_solution/inventory/public/types.ts b/x-pack/plugins/observability_solution/inventory/public/types.ts index ed4a500edca68..2393b1b55e2b6 100644 --- a/x-pack/plugins/observability_solution/inventory/public/types.ts +++ b/x-pack/plugins/observability_solution/inventory/public/types.ts @@ -13,10 +13,10 @@ import { EntityManagerPublicPluginStart, } from '@kbn/entityManager-plugin/public'; import type { InferencePublicStart, InferencePublicSetup } from '@kbn/inference-plugin/public'; +import type { SharePluginSetup, SharePluginStart } from '@kbn/share-plugin/public'; import type { UnifiedSearchPublicPluginStart } from '@kbn/unified-search-plugin/public'; import type { DataViewsPublicPluginStart } from '@kbn/data-views-plugin/public'; import type { DataPublicPluginSetup, DataPublicPluginStart } from '@kbn/data-plugin/public'; -import type { SharePluginStart } from '@kbn/share-plugin/public'; /* eslint-disable @typescript-eslint/no-empty-interface*/ @@ -25,6 +25,7 @@ export interface ConfigSchema {} export interface InventorySetupDependencies { observabilityShared: ObservabilitySharedPluginSetup; inference: InferencePublicSetup; + share: SharePluginSetup; data: DataPublicPluginSetup; entityManager: EntityManagerPublicPluginSetup; } diff --git a/x-pack/plugins/observability_solution/observability_shared/common/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/index.ts index d845ea1d398fd..d13e2b32839d6 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/index.ts @@ -178,6 +178,7 @@ export type { ServiceEntityLocatorParams, TransactionDetailsByTraceIdLocator, TransactionDetailsByTraceIdLocatorParams, + EntitiesInventoryLocator, } from './locators'; export { @@ -201,6 +202,8 @@ export { SERVICE_ENTITY_LOCATOR, TransactionDetailsByTraceIdLocatorDefinition, TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR, + EntitiesInventoryLocatorDefinition, + ENTITIES_INVENTORY_LOCATOR_ID, } from './locators'; export { COMMON_OBSERVABILITY_GROUPING } from './embeddable_grouping'; diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/entity_inventory/entity_inventory_locator.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/entity_inventory/entity_inventory_locator.ts new file mode 100644 index 0000000000000..deb820b0d5e0a --- /dev/null +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/entity_inventory/entity_inventory_locator.ts @@ -0,0 +1,24 @@ +/* + * 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 { SerializableRecord } from '@kbn/utility-types'; +import { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/common'; + +export type EntitiesInventoryLocator = LocatorPublic; + +export const ENTITIES_INVENTORY_LOCATOR_ID = 'ENTITY_INVENTORY_LOCATOR'; + +export class EntitiesInventoryLocatorDefinition implements LocatorDefinition { + public readonly id = ENTITIES_INVENTORY_LOCATOR_ID; + + public readonly getLocation = async () => { + return { + app: 'observability', + path: `/inventory`, + state: {}, + }; + }; +} diff --git a/x-pack/plugins/observability_solution/observability_shared/common/locators/index.ts b/x-pack/plugins/observability_solution/observability_shared/common/locators/index.ts index 9c5ded4940d5a..34a6ff391d672 100644 --- a/x-pack/plugins/observability_solution/observability_shared/common/locators/index.ts +++ b/x-pack/plugins/observability_solution/observability_shared/common/locators/index.ts @@ -17,3 +17,4 @@ export * from './infra/metrics_explorer_locator'; export * from './profiling/flamegraph_locator'; export * from './profiling/stacktraces_locator'; export * from './profiling/topn_functions_locator'; +export * from './entity_inventory/entity_inventory_locator'; diff --git a/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts b/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts index 7cd63d7be7602..7a131a2686ad0 100644 --- a/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts +++ b/x-pack/plugins/observability_solution/observability_shared/public/plugin.ts @@ -45,6 +45,8 @@ import { type MetricsExplorerLocator, type ServiceEntityLocator, type TransactionDetailsByTraceIdLocator, + type EntitiesInventoryLocator, + EntitiesInventoryLocatorDefinition, } from '../common'; import { updateGlobalNavigation } from './services/update_global_navigation'; export interface ObservabilitySharedSetup { @@ -82,6 +84,7 @@ interface ObservabilitySharedLocators { transactionDetailsByTraceId: TransactionDetailsByTraceIdLocator; serviceEntity: ServiceEntityLocator; }; + entitiesInventory: EntitiesInventoryLocator; } export class ObservabilitySharedPlugin implements Plugin { @@ -159,6 +162,7 @@ export class ObservabilitySharedPlugin implements Plugin { ), serviceEntity: urlService.locators.create(new ServiceEntityLocatorDefinition()), }, + entitiesInventory: urlService.locators.create(new EntitiesInventoryLocatorDefinition()), }; } } From 05926c20c57b7abc69c6c068d5733f29306f73ba Mon Sep 17 00:00:00 2001 From: Ying Mao Date: Mon, 30 Sep 2024 10:40:02 -0400 Subject: [PATCH 5/8] [Response Ops][Alerting] Use ES client to update rule SO at end of rule run instead of SO client. (#193341) Resolves https://github.com/elastic/kibana/issues/192397 ## Summary Updates alerting task runner end of run updates to use the ES client update function for a true partial update instead of the saved objects client update function that performs a GET then an update. ## To verify Create a rule in multiple spaces and ensure they run correctly and their execution status and monitoring history are updated at the end of each run. Because we're performing a partial update on attributes that are not in the AAD, the rule should continue running without any encryption errors. ## Risk Matrix | Risk | Probability | Severity | Mitigation/Notes | |---------------------------|-------------|----------|-------------------------| | Updating saved object directly using ES client will break BWC | Medium | High | Response Ops follows an intermediate release strategy for any changes to the rule saved object where schema changes are introduced in an intermediate release before any changes to the saved object are actually made in a followup release. This ensures that any rollbacks that may be required in a release will roll back to a version that is already aware of the new schema. The team is socialized to this strategy as we are requiring users of the alerting framework to also follow this strategy. This should address any backward compatibility issues that might arise by circumventing the saved objects client update function. | | Updating saved object directly using ES client will break AAD | Medium | High | An explicit allowlist of non-AAD fields that are allowed to be partially updated has been introduced and any fields not in this allowlist will not be included in the partial update. Any updates to the rule saved object that might break AAD would show up with > 1 execution of a rule and we have a plethora of functional tests that rely on multiple executions of a rule that would flag if there were issues running due to AAD issues. | --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Elastic Machine --- .../alerting/server/saved_objects/index.ts | 2 +- .../partially_update_rule.test.ts | 164 +++++++++++++++++- .../saved_objects/partially_update_rule.ts | 50 ++++++ .../alerting/server/task_runner/fixtures.ts | 89 +++++----- .../server/task_runner/task_runner.test.ts | 51 ++---- .../server/task_runner/task_runner.ts | 19 +- .../task_runner_alerts_client.test.ts | 18 +- .../task_runner/task_runner_cancel.test.ts | 85 ++++----- x-pack/plugins/alerting/tsconfig.json | 3 +- 9 files changed, 340 insertions(+), 141 deletions(-) diff --git a/x-pack/plugins/alerting/server/saved_objects/index.ts b/x-pack/plugins/alerting/server/saved_objects/index.ts index eb07a84950d14..a3bb0b4f0afe8 100644 --- a/x-pack/plugins/alerting/server/saved_objects/index.ts +++ b/x-pack/plugins/alerting/server/saved_objects/index.ts @@ -23,7 +23,7 @@ import { RawRule } from '../types'; import { getImportWarnings } from './get_import_warnings'; import { isRuleExportable } from './is_rule_exportable'; import { RuleTypeRegistry } from '../rule_type_registry'; -export { partiallyUpdateRule } from './partially_update_rule'; +export { partiallyUpdateRule, partiallyUpdateRuleWithEs } from './partially_update_rule'; import { RULES_SETTINGS_SAVED_OBJECT_TYPE, MAINTENANCE_WINDOW_SAVED_OBJECT_TYPE, diff --git a/x-pack/plugins/alerting/server/saved_objects/partially_update_rule.test.ts b/x-pack/plugins/alerting/server/saved_objects/partially_update_rule.test.ts index 5fcf23cbae6fb..294bc81481540 100644 --- a/x-pack/plugins/alerting/server/saved_objects/partially_update_rule.test.ts +++ b/x-pack/plugins/alerting/server/saved_objects/partially_update_rule.test.ts @@ -10,16 +10,23 @@ import { ISavedObjectsRepository, SavedObjectsErrorHelpers, } from '@kbn/core/server'; - -import { PartiallyUpdateableRuleAttributes, partiallyUpdateRule } from './partially_update_rule'; -import { savedObjectsClientMock } from '@kbn/core/server/mocks'; +import { + PartiallyUpdateableRuleAttributes, + partiallyUpdateRule, + partiallyUpdateRuleWithEs, +} from './partially_update_rule'; +import { elasticsearchServiceMock, savedObjectsClientMock } from '@kbn/core/server/mocks'; import { RULE_SAVED_OBJECT_TYPE } from '.'; +import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; +import { estypes } from '@elastic/elasticsearch'; +import { RuleExecutionStatuses } from '@kbn/alerting-types'; const MockSavedObjectsClientContract = savedObjectsClientMock.create(); const MockISavedObjectsRepository = MockSavedObjectsClientContract as unknown as jest.Mocked; +const esClient = elasticsearchServiceMock.createClusterClient().asInternalUser; -describe('partially_update_rule', () => { +describe('partiallyUpdateRule', () => { beforeEach(() => { jest.resetAllMocks(); }); @@ -104,6 +111,101 @@ describe('partially_update_rule', () => { }); }); +describe('partiallyUpdateRuleWithEs', () => { + beforeEach(() => { + jest.resetAllMocks(); + jest.clearAllMocks(); + }); + + test('should work with no options', async () => { + esClient.update.mockResolvedValueOnce(MockEsUpdateResponse(MockRuleId)); + + await partiallyUpdateRuleWithEs(esClient, MockRuleId, DefaultAttributesForEsUpdate); + expect(esClient.update).toHaveBeenCalledTimes(1); + expect(esClient.update).toHaveBeenCalledWith({ + id: `alert:${MockRuleId}`, + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + doc: { + alert: DefaultAttributesForEsUpdate, + }, + }); + }); + + test('should strip unallowed attributes ', async () => { + const attributes = + AttributesForEsUpdateWithUnallowedFields as unknown as PartiallyUpdateableRuleAttributes; + esClient.update.mockResolvedValueOnce(MockEsUpdateResponse(MockRuleId)); + + await partiallyUpdateRuleWithEs(esClient, MockRuleId, attributes); + expect(esClient.update).toHaveBeenCalledWith({ + id: `alert:${MockRuleId}`, + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + doc: { + alert: DefaultAttributesForEsUpdate, + }, + }); + }); + + test('should handle ES errors', async () => { + esClient.update.mockRejectedValueOnce(new Error('wops')); + + await expect( + partiallyUpdateRuleWithEs(esClient, MockRuleId, DefaultAttributes) + ).rejects.toThrowError('wops'); + }); + + test('should handle the version option', async () => { + esClient.update.mockResolvedValueOnce(MockEsUpdateResponse(MockRuleId)); + + await partiallyUpdateRuleWithEs(esClient, MockRuleId, DefaultAttributesForEsUpdate, { + version: 'WzQsMV0=', + }); + expect(esClient.update).toHaveBeenCalledWith({ + id: `alert:${MockRuleId}`, + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + if_primary_term: 1, + if_seq_no: 4, + doc: { + alert: DefaultAttributesForEsUpdate, + }, + }); + }); + + test('should handle the ignore404 option', async () => { + esClient.update.mockResolvedValueOnce(MockEsUpdateResponse(MockRuleId)); + + await partiallyUpdateRuleWithEs(esClient, MockRuleId, DefaultAttributesForEsUpdate, { + ignore404: true, + }); + expect(esClient.update).toHaveBeenCalledWith( + { + id: `alert:${MockRuleId}`, + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + doc: { + alert: DefaultAttributesForEsUpdate, + }, + }, + { ignore: [404] } + ); + }); + + test('should handle the refresh option', async () => { + esClient.update.mockResolvedValueOnce(MockEsUpdateResponse(MockRuleId)); + + await partiallyUpdateRuleWithEs(esClient, MockRuleId, DefaultAttributesForEsUpdate, { + refresh: 'wait_for', + }); + expect(esClient.update).toHaveBeenCalledWith({ + id: `alert:${MockRuleId}`, + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + doc: { + alert: DefaultAttributesForEsUpdate, + }, + refresh: 'wait_for', + }); + }); +}); + function getMockSavedObjectClients(): Record< string, jest.Mocked @@ -126,6 +228,50 @@ const DefaultAttributes = { const ExtraneousAttributes = { ...DefaultAttributes, foo: 'bar' }; +const DefaultAttributesForEsUpdate = { + running: false, + executionStatus: { + status: 'active' as RuleExecutionStatuses, + lastExecutionDate: '2023-01-01T08:44:40.000Z', + lastDuration: 12, + error: null, + warning: null, + }, + monitoring: { + run: { + calculated_metrics: { + success_ratio: 20, + }, + history: [ + { + success: true, + timestamp: 1640991880000, + duration: 12, + outcome: 'success', + }, + ], + last_run: { + timestamp: '2023-01-01T08:44:40.000Z', + metrics: { + duration: 12, + gap_duration_s: null, + total_alerts_created: null, + total_alerts_detected: null, + total_indexing_duration_ms: null, + total_search_duration_ms: null, + }, + }, + }, + }, +}; + +const AttributesForEsUpdateWithUnallowedFields = { + ...DefaultAttributesForEsUpdate, + alertTypeId: 'foo', + consumer: 'consumer', + randomField: 'bar', +}; + const MockRuleId = 'rule-id'; const MockUpdateValue = { @@ -137,3 +283,13 @@ const MockUpdateValue = { }, references: [], }; + +const MockEsUpdateResponse = (id: string) => ({ + _index: '.kibana_alerting_cases_9.0.0_001', + _id: `alert:${id}`, + _version: 3, + result: 'updated' as estypes.Result, + _shards: { total: 1, successful: 1, failed: 0 }, + _seq_no: 5, + _primary_term: 1, +}); diff --git a/x-pack/plugins/alerting/server/saved_objects/partially_update_rule.ts b/x-pack/plugins/alerting/server/saved_objects/partially_update_rule.ts index 2665845a1110f..f9b4da5ed767b 100644 --- a/x-pack/plugins/alerting/server/saved_objects/partially_update_rule.ts +++ b/x-pack/plugins/alerting/server/saved_objects/partially_update_rule.ts @@ -7,10 +7,13 @@ import { omit, pick } from 'lodash'; import { + ElasticsearchClient, SavedObjectsClient, SavedObjectsErrorHelpers, SavedObjectsUpdateOptions, } from '@kbn/core/server'; +import { decodeRequestVersion } from '@kbn/core-saved-objects-base-server-internal'; +import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import { RawRule } from '../types'; import { @@ -67,3 +70,50 @@ export async function partiallyUpdateRule( throw err; } } + +// Explicit list of attributes that we allow to be partially updated +// There should be no overlap between this list and RuleAttributesIncludedInAAD or RuleAttributesToEncrypt +const RuleAttributesAllowedForPartialUpdate = [ + 'executionStatus', + 'lastRun', + 'monitoring', + 'nextRun', + 'running', +]; + +// direct, partial update to a rule saved object via ElasticsearchClient + +// we do this direct partial update to avoid the overhead of the SavedObjectsClient for +// only these allow-listed fields which don't impact encryption. in addition, because these +// fields are only updated by the system user at the end of a rule run, they should not +// need to be included in any (user-centric) audit logs. +export async function partiallyUpdateRuleWithEs( + esClient: ElasticsearchClient, + id: string, + attributes: PartiallyUpdateableRuleAttributes, + options: PartiallyUpdateRuleSavedObjectOptions = {} +): Promise { + // ensure we only have the valid attributes that are not encrypted and are excluded from AAD + const attributeUpdates = omit(attributes, [ + ...RuleAttributesToEncrypt, + ...RuleAttributesIncludedInAAD, + ]); + // ensure we only have attributes that we explicitly allow to be updated + const attributesAllowedForUpdate = pick(attributeUpdates, RuleAttributesAllowedForPartialUpdate); + + const updateParams = { + id: `alert:${id}`, + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + ...(options.version ? decodeRequestVersion(options.version) : {}), + doc: { + alert: attributesAllowedForUpdate, + }, + ...(options.refresh ? { refresh: options.refresh } : {}), + }; + + if (options.ignore404) { + await esClient.update(updateParams, { ignore: [404] }); + } else { + await esClient.update(updateParams); + } +} diff --git a/x-pack/plugins/alerting/server/task_runner/fixtures.ts b/x-pack/plugins/alerting/server/task_runner/fixtures.ts index ae8eccfcb1f86..5174aa9b965ec 100644 --- a/x-pack/plugins/alerting/server/task_runner/fixtures.ts +++ b/x-pack/plugins/alerting/server/task_runner/fixtures.ts @@ -7,6 +7,7 @@ import { TaskStatus } from '@kbn/task-manager-plugin/server'; import { SavedObject } from '@kbn/core/server'; +import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import { Rule, RuleTypeParams, @@ -64,7 +65,7 @@ const defaultHistory = [ }, ]; -export const generateSavedObjectParams = ({ +export const generateRuleUpdateParams = ({ error = null, warning = null, status = 'ok', @@ -83,53 +84,59 @@ export const generateSavedObjectParams = ({ history?: RuleMonitoring['run']['history']; alertsCount?: Record; }) => [ - RULE_SAVED_OBJECT_TYPE, - '1', { - monitoring: { - run: { - calculated_metrics: { - success_ratio: successRatio, + id: `alert:1`, + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + doc: { + alert: { + monitoring: { + run: { + calculated_metrics: { + success_ratio: successRatio, + }, + history, + last_run: { + timestamp: '1970-01-01T00:00:00.000Z', + metrics: { + duration: 0, + gap_duration_s: null, + total_alerts_created: null, + total_alerts_detected: null, + total_indexing_duration_ms: null, + total_search_duration_ms: null, + }, + }, + }, }, - history, - last_run: { - timestamp: '1970-01-01T00:00:00.000Z', - metrics: { - duration: 0, - gap_duration_s: null, - total_alerts_created: null, - total_alerts_detected: null, - total_indexing_duration_ms: null, - total_search_duration_ms: null, + executionStatus: { + error, + lastDuration: 0, + lastExecutionDate: '1970-01-01T00:00:00.000Z', + status, + warning, + }, + lastRun: { + outcome, + outcomeOrder: RuleLastRunOutcomeOrderMap[outcome], + outcomeMsg: + (error?.message && [error?.message]) || + (warning?.message && [warning?.message]) || + null, + warning: error?.reason || warning?.reason || null, + alertsCount: { + active: 0, + ignored: 0, + new: 0, + recovered: 0, + ...(alertsCount || {}), }, }, + nextRun, + running: false, }, }, - executionStatus: { - error, - lastDuration: 0, - lastExecutionDate: '1970-01-01T00:00:00.000Z', - status, - warning, - }, - lastRun: { - outcome, - outcomeOrder: RuleLastRunOutcomeOrderMap[outcome], - outcomeMsg: - (error?.message && [error?.message]) || (warning?.message && [warning?.message]) || null, - warning: error?.reason || warning?.reason || null, - alertsCount: { - active: 0, - ignored: 0, - new: 0, - recovered: 0, - ...(alertsCount || {}), - }, - }, - nextRun, - running: false, }, - { refresh: false, namespace: undefined }, + { ignore: [404] }, ]; export const GENERIC_ERROR_MESSAGE = 'GENERIC ERROR MESSAGE'; diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts index 438ffb3685e2a..e06c260109b76 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.test.ts @@ -59,7 +59,7 @@ import { generateRunnerResult, RULE_ACTIONS, generateEnqueueFunctionInput, - generateSavedObjectParams, + generateRuleUpdateParams, mockTaskInstance, GENERIC_ERROR_MESSAGE, generateAlertInstance, @@ -341,8 +341,8 @@ describe('Task Runner', () => { testAlertingEventLogCalls({ status: 'ok' }); - expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith( - ...generateSavedObjectParams({}) + expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith( + ...generateRuleUpdateParams({}) ); expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); @@ -2676,8 +2676,8 @@ describe('Task Runner', () => { status: 'ok', }); - expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith( - ...generateSavedObjectParams({}) + expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith( + ...generateRuleUpdateParams({}) ); expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled(); }); @@ -2789,10 +2789,8 @@ describe('Task Runner', () => { }); await taskRunner.run(); - expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith( - ...generateSavedObjectParams({ - nextRun: '1970-01-01T00:00:10.000Z', - }) + expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith( + ...generateRuleUpdateParams({ nextRun: '1970-01-01T00:00:10.000Z' }) ); }); @@ -2825,21 +2823,14 @@ describe('Task Runner', () => { ); await taskRunner.run(); ruleType.executor.mockClear(); - expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith( - ...generateSavedObjectParams({ - error: { - message: GENERIC_ERROR_MESSAGE, - reason: 'execute', - }, + + expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith( + ...generateRuleUpdateParams({ + error: { message: GENERIC_ERROR_MESSAGE, reason: 'execute' }, outcome: 'failed', status: 'error', successRatio: 0, - history: [ - { - success: false, - timestamp: 0, - }, - ], + history: [{ success: false, timestamp: 0 }], }) ); }); @@ -2947,15 +2938,12 @@ describe('Task Runner', () => { expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); - expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith( - ...generateSavedObjectParams({ + expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith( + ...generateRuleUpdateParams({ status: 'warning', outcome: 'warning', warning, - alertsCount: { - active: 1, - new: 1, - }, + alertsCount: { active: 1, new: 1 }, }) ); @@ -3117,15 +3105,12 @@ describe('Task Runner', () => { expect(actionsClient.bulkEnqueueExecution).toHaveBeenCalledTimes(1); - expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith( - ...generateSavedObjectParams({ + expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith( + ...generateRuleUpdateParams({ status: 'warning', outcome: 'warning', warning, - alertsCount: { - active: 2, - new: 2, - }, + alertsCount: { active: 2, new: 2 }, }) ); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner.ts b/x-pack/plugins/alerting/server/task_runner/task_runner.ts index e01dd73df7e58..b5a1854581bf3 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner.ts @@ -42,7 +42,7 @@ import { import { asErr, asOk, isErr, isOk, map, resolveErr, Result } from '../lib/result_type'; import { taskInstanceToAlertTaskInstance } from './alert_task_instance'; import { isAlertSavedObjectNotFoundError, isEsUnavailableError } from '../lib/is_alerting_error'; -import { partiallyUpdateRule, RULE_SAVED_OBJECT_TYPE } from '../saved_objects'; +import { partiallyUpdateRuleWithEs, RULE_SAVED_OBJECT_TYPE } from '../saved_objects'; import { AlertInstanceContext, AlertInstanceState, @@ -204,7 +204,6 @@ export class TaskRunner< private async updateRuleSavedObjectPostRun( ruleId: string, - namespace: string | undefined, attributes: { executionStatus?: RawRuleExecutionStatus; monitoring?: RawRuleMonitoring; @@ -212,7 +211,7 @@ export class TaskRunner< lastRun?: RawRuleLastRun | null; } ) { - const client = this.internalSavedObjectsRepository; + const client = this.context.elasticsearch.client.asInternalUser; try { // Future engineer -> Here we are just checking if we need to wait for // the update of the attribute `running` in the rule's saved object @@ -223,13 +222,12 @@ export class TaskRunner< // eslint-disable-next-line no-empty } catch {} try { - await partiallyUpdateRule( + await partiallyUpdateRuleWithEs( client, ruleId, { ...attributes, running: false }, { ignore404: true, - namespace, refresh: false, } ); @@ -548,7 +546,7 @@ export class TaskRunner< const { executionStatus: execStatus, executionMetrics: execMetrics } = await this.timer.runWithTimer(TaskRunnerTimerSpan.ProcessRuleRun, async () => { const { - params: { alertId: ruleId, spaceId }, + params: { alertId: ruleId }, startedAt, schedule: taskSchedule, } = this.taskInstance; @@ -560,8 +558,6 @@ export class TaskRunner< nextRun = getNextRun({ startDate: startedAt, interval: taskSchedule.interval }); } - const namespace = this.context.spaceIdToNamespace(spaceId); - const { executionStatus, executionMetrics, lastRun, outcome } = processRunResults({ logger: this.logger, logPrefix: `${this.ruleType.id}:${ruleId}`, @@ -602,7 +598,7 @@ export class TaskRunner< )} - ${JSON.stringify(lastRun)}` ); } - await this.updateRuleSavedObjectPostRun(ruleId, namespace, { + await this.updateRuleSavedObjectPostRun(ruleId, { executionStatus: ruleExecutionStatusToRaw(executionStatus), nextRun, lastRun: lastRunToRaw(lastRun), @@ -758,11 +754,10 @@ export class TaskRunner< // Write event log entry const { - params: { alertId: ruleId, spaceId, consumer }, + params: { alertId: ruleId, consumer }, schedule: taskSchedule, startedAt, } = this.taskInstance; - const namespace = this.context.spaceIdToNamespace(spaceId); if (consumer && !this.ruleConsumer) { this.ruleConsumer = consumer; @@ -803,7 +798,7 @@ export class TaskRunner< `Updating rule task for ${this.ruleType.id} rule with id ${ruleId} - execution error due to timeout` ); const outcome = 'failed'; - await this.updateRuleSavedObjectPostRun(ruleId, namespace, { + await this.updateRuleSavedObjectPostRun(ruleId, { executionStatus: ruleExecutionStatusToRaw(executionStatus), lastRun: { outcome, diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts index c116230016e9b..6c7331de463ea 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_alerts_client.test.ts @@ -46,7 +46,7 @@ import { RULE_NAME, generateRunnerResult, RULE_ACTIONS, - generateSavedObjectParams, + generateRuleUpdateParams, mockTaskInstance, DATE_1970, DATE_1970_5_MIN, @@ -376,8 +376,8 @@ describe('Task Runner', () => { { tags: ['1', 'test'] } ); - expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith( - ...generateSavedObjectParams({}) + expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith( + ...generateRuleUpdateParams({}) ); expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); @@ -510,8 +510,8 @@ describe('Task Runner', () => { 'ruleRunMetrics for test:1: {"numSearches":3,"totalSearchDurationMs":23423,"esSearchDurationMs":33,"numberOfTriggeredActions":0,"numberOfGeneratedActions":0,"numberOfActiveAlerts":0,"numberOfRecoveredAlerts":0,"numberOfNewAlerts":0,"numberOfDelayedAlerts":0,"hasReachedAlertLimit":false,"hasReachedQueuedActionsLimit":false,"triggeredActionsStatus":"complete"}', { tags: ['1', 'test'] } ); - expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith( - ...generateSavedObjectParams({}) + expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith( + ...generateRuleUpdateParams({}) ); expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); expect( @@ -708,8 +708,8 @@ describe('Task Runner', () => { tags: ['1', 'test'], }); - expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith( - ...generateSavedObjectParams({}) + expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith( + ...generateRuleUpdateParams({}) ); expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); @@ -799,8 +799,8 @@ describe('Task Runner', () => { tags: ['1', 'test'], }); - expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith( - ...generateSavedObjectParams({}) + expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith( + ...generateRuleUpdateParams({}) ); expect(taskRunnerFactoryInitializerParams.executionContext.withContext).toBeCalledTimes(1); diff --git a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts index 3a6a9547fb902..e5572707ae6fd 100644 --- a/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts +++ b/x-pack/plugins/alerting/server/task_runner/task_runner_cancel.test.ts @@ -63,6 +63,7 @@ import { TaskRunnerContext } from './types'; import { backfillClientMock } from '../backfill_client/backfill_client.mock'; import { UntypedNormalizedRuleType } from '../rule_type_registry'; import { rulesSettingsServiceMock } from '../rules_settings/rules_settings_service.mock'; +import { ALERTING_CASES_SAVED_OBJECT_INDEX } from '@kbn/core-saved-objects-server'; import { maintenanceWindowsServiceMock } from './maintenance_windows/maintenance_windows_service.mock'; jest.mock('uuid', () => ({ @@ -225,53 +226,57 @@ describe('Task Runner Cancel', () => { testAlertingEventLogCalls({ status: 'ok' }); - expect(internalSavedObjectsRepository.update).toHaveBeenCalledTimes(1); - expect(internalSavedObjectsRepository.update).toHaveBeenCalledWith( - RULE_SAVED_OBJECT_TYPE, - '1', + expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledTimes(1); + expect(elasticsearchService.client.asInternalUser.update).toHaveBeenCalledWith( { - executionStatus: { - error: { - message: `test:1: execution cancelled due to timeout - exceeded rule type timeout of 5m`, - reason: 'timeout', - }, - lastDuration: 0, - lastExecutionDate: '1970-01-01T00:00:00.000Z', - status: 'error', - warning: null, - }, - lastRun: { - alertsCount: {}, - outcome: 'failed', - outcomeMsg: [ - 'test:1: execution cancelled due to timeout - exceeded rule type timeout of 5m', - ], - outcomeOrder: 20, - warning: 'timeout', - }, - monitoring: { - run: { - calculated_metrics: { - success_ratio: 0, + id: `alert:1`, + index: ALERTING_CASES_SAVED_OBJECT_INDEX, + doc: { + alert: { + executionStatus: { + error: { + message: `test:1: execution cancelled due to timeout - exceeded rule type timeout of 5m`, + reason: 'timeout', + }, + lastDuration: 0, + lastExecutionDate: '1970-01-01T00:00:00.000Z', + status: 'error', + warning: null, + }, + lastRun: { + alertsCount: {}, + outcome: 'failed', + outcomeMsg: [ + 'test:1: execution cancelled due to timeout - exceeded rule type timeout of 5m', + ], + outcomeOrder: 20, + warning: 'timeout', }, - history: [], - last_run: { - metrics: { - duration: 0, - gap_duration_s: null, - total_alerts_created: null, - total_alerts_detected: null, - total_indexing_duration_ms: null, - total_search_duration_ms: null, + monitoring: { + run: { + calculated_metrics: { + success_ratio: 0, + }, + history: [], + last_run: { + metrics: { + duration: 0, + gap_duration_s: null, + total_alerts_created: null, + total_alerts_detected: null, + total_indexing_duration_ms: null, + total_search_duration_ms: null, + }, + timestamp: '1970-01-01T00:00:00.000Z', + }, }, - timestamp: '1970-01-01T00:00:00.000Z', }, + nextRun: '1970-01-01T00:00:10.000Z', + running: false, }, }, - nextRun: '1970-01-01T00:00:10.000Z', - running: false, }, - { refresh: false, namespace: undefined } + { ignore: [404] } ); expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(1); expect(mockUsageCounter.incrementCounter).toHaveBeenCalledWith({ diff --git a/x-pack/plugins/alerting/tsconfig.json b/x-pack/plugins/alerting/tsconfig.json index c09816222b010..c0951663a8489 100644 --- a/x-pack/plugins/alerting/tsconfig.json +++ b/x-pack/plugins/alerting/tsconfig.json @@ -72,7 +72,8 @@ "@kbn/alerting-state-types", "@kbn/core-security-server", "@kbn/core-http-server", - "@kbn/zod" + "@kbn/zod", + "@kbn/core-saved-objects-base-server-internal" ], "exclude": [ "target/**/*" From 2a935dcce37bd3e3f3fce32d6257b2ec7191dac5 Mon Sep 17 00:00:00 2001 From: Nathan L Smith Date: Mon, 30 Sep 2024 09:55:49 -0500 Subject: [PATCH 6/8] Hide progress bar on initial load when client prefers reduced motion (#194365) ## Summary See https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-reduced-motion ![CleanShot 2024-09-29 at 21 57 36](https://github.com/user-attachments/assets/27436ec4-986b-4c91-9d9f-e49d59d76e7c) --- .../apps/core-apps-server-internal/assets/legacy_styles.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/packages/core/apps/core-apps-server-internal/assets/legacy_styles.css b/packages/core/apps/core-apps-server-internal/assets/legacy_styles.css index 55ed0fbfdf8ba..ea149e7fec132 100644 --- a/packages/core/apps/core-apps-server-internal/assets/legacy_styles.css +++ b/packages/core/apps/core-apps-server-internal/assets/legacy_styles.css @@ -114,3 +114,9 @@ body, html { transform: scaleX(1) translateX(100%); } } + +@media (prefers-reduced-motion) { + .kbnProgress { + display: none; + } +} From 508141423e7d18ce87628d826628d161c3292418 Mon Sep 17 00:00:00 2001 From: Hanna Tamoudi Date: Mon, 30 Sep 2024 17:46:05 +0200 Subject: [PATCH 7/8] [Automatic Import] add fields mapping to readme (#193717) --- .../build_integration.test.ts | 62 ++++-- .../integration_builder/build_integration.ts | 43 +++-- .../integration_builder/data_stream.test.ts | 12 ++ .../server/integration_builder/data_stream.ts | 37 +++- .../server/integration_builder/fields.test.ts | 34 ++++ .../server/integration_builder/fields.ts | 20 +- .../integration_builder/readme_files.test.ts | 182 ++++++++++++++++++ .../integration_builder/readme_files.ts | 42 ++++ .../server/templates/build_readme.md.njk | 8 + .../server/templates/package_readme.md.njk | 43 ++--- .../server/templates/readme.njk | 31 +++ .../server/util/samples.test.ts | 159 ++++++++++++++- .../server/util/samples.ts | 43 ++++- 13 files changed, 634 insertions(+), 82 deletions(-) create mode 100644 x-pack/plugins/integration_assistant/server/integration_builder/readme_files.test.ts create mode 100644 x-pack/plugins/integration_assistant/server/integration_builder/readme_files.ts create mode 100644 x-pack/plugins/integration_assistant/server/templates/build_readme.md.njk create mode 100644 x-pack/plugins/integration_assistant/server/templates/readme.njk diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/build_integration.test.ts b/x-pack/plugins/integration_assistant/server/integration_builder/build_integration.test.ts index e8800af12653f..419e287e23bf7 100644 --- a/x-pack/plugins/integration_assistant/server/integration_builder/build_integration.test.ts +++ b/x-pack/plugins/integration_assistant/server/integration_builder/build_integration.test.ts @@ -14,6 +14,7 @@ import { createAgentInput } from './agent'; import { createPipeline } from './pipeline'; import { DataStream, Docs, InputType, Pipeline, Integration } from '../../common'; import yaml from 'js-yaml'; +import { createReadme } from './readme_files'; const mockedDataPath = 'path'; const mockedId = 123; @@ -23,6 +24,10 @@ jest.mock('./data_stream'); jest.mock('./fields'); jest.mock('./agent'); jest.mock('./pipeline'); +jest.mock('./readme_files'); + +(createFieldMapping as jest.Mock).mockReturnValue([]); +(createDataStream as jest.Mock).mockReturnValue([]); (generateUniqueId as jest.Mock).mockReturnValue(mockedId); @@ -106,22 +111,11 @@ describe('buildPackage', () => { // _dev files expect(ensureDirSync).toHaveBeenCalledWith(`${integrationPath}/_dev/build`); - expect(createSync).toHaveBeenCalledWith( - `${integrationPath}/_dev/build/docs/README.md`, - expect.any(String) - ); expect(createSync).toHaveBeenCalledWith( `${integrationPath}/_dev/build/build.yml`, expect.any(String) ); - // Docs files - expect(ensureDirSync).toHaveBeenCalledWith(`${integrationPath}/docs/`); - expect(createSync).toHaveBeenCalledWith( - `${integrationPath}/docs/README.md`, - expect.any(String) - ); - // Changelog file expect(createSync).toHaveBeenCalledWith(`${integrationPath}/changelog.yml`, expect.any(String)); @@ -188,6 +182,52 @@ describe('buildPackage', () => { secondDataStreamDocs ); }); + + it('Should call createReadme once with sorted fields', async () => { + jest.clearAllMocks(); + + const firstDSFieldsMapping = [{ name: 'name a', description: 'description 1', type: 'type 1' }]; + + const firstDataStreamFields = [ + { name: 'name b', description: 'description 1', type: 'type 1' }, + ]; + + const secondDSFieldsMapping = [ + { name: 'name c', description: 'description 2', type: 'type 2' }, + { name: 'name e', description: 'description 3', type: 'type 3' }, + ]; + + const secondDataStreamFields = [ + { name: 'name d', description: 'description 2', type: 'type 2' }, + ]; + + (createFieldMapping as jest.Mock).mockReturnValueOnce(firstDSFieldsMapping); + (createDataStream as jest.Mock).mockReturnValueOnce(firstDataStreamFields); + + (createFieldMapping as jest.Mock).mockReturnValueOnce(secondDSFieldsMapping); + (createDataStream as jest.Mock).mockReturnValueOnce(secondDataStreamFields); + + await buildPackage(testIntegration); + + expect(createReadme).toHaveBeenCalledWith(integrationPath, testIntegration.name, [ + { + datastream: firstDatastreamName, + fields: [ + { name: 'name a', description: 'description 1', type: 'type 1' }, + + { name: 'name b', description: 'description 1', type: 'type 1' }, + ], + }, + { + datastream: secondDatastreamName, + fields: [ + { name: 'name c', description: 'description 2', type: 'type 2' }, + { name: 'name d', description: 'description 2', type: 'type 2' }, + { name: 'name e', description: 'description 3', type: 'type 3' }, + ], + }, + ]); + }); }); describe('renderPackageManifestYAML', () => { diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/build_integration.ts b/x-pack/plugins/integration_assistant/server/integration_builder/build_integration.ts index b9bc1b55268d7..8743ada38bdb6 100644 --- a/x-pack/plugins/integration_assistant/server/integration_builder/build_integration.ts +++ b/x-pack/plugins/integration_assistant/server/integration_builder/build_integration.ts @@ -16,6 +16,8 @@ import { createAgentInput } from './agent'; import { createDataStream } from './data_stream'; import { createFieldMapping } from './fields'; import { createPipeline } from './pipeline'; +import { createReadme } from './readme_files'; +import { Field, flattenObjectsList } from '../util/samples'; const initialVersion = '1.0.0'; @@ -37,17 +39,27 @@ export async function buildPackage(integration: Integration): Promise { const packageDir = createDirectories(workingDir, integration, packageDirectoryName); const dataStreamsDir = joinPath(packageDir, 'data_stream'); - - for (const dataStream of integration.dataStreams) { + const fieldsPerDatastream = integration.dataStreams.map((dataStream) => { const dataStreamName = dataStream.name; const specificDataStreamDir = joinPath(dataStreamsDir, dataStreamName); - createDataStream(integration.name, specificDataStreamDir, dataStream); + const dataStreamFields = createDataStream(integration.name, specificDataStreamDir, dataStream); createAgentInput(specificDataStreamDir, dataStream.inputTypes); createPipeline(specificDataStreamDir, dataStream.pipeline); - createFieldMapping(integration.name, dataStreamName, specificDataStreamDir, dataStream.docs); - } + const fields = createFieldMapping( + integration.name, + dataStreamName, + specificDataStreamDir, + dataStream.docs + ); + + return { + datastream: dataStreamName, + fields: mergeAndSortFields(fields, dataStreamFields), + }; + }); + createReadme(packageDir, integration.name, fieldsPerDatastream); const zipBuffer = await createZipArchive(workingDir, packageDirectoryName); removeDirSync(workingDir); @@ -67,7 +79,6 @@ function createDirectories( } function createPackage(packageDir: string, integration: Integration): void { - createReadme(packageDir, integration); createChangelog(packageDir); createBuildFile(packageDir); createPackageManifest(packageDir, integration); @@ -102,20 +113,6 @@ function createChangelog(packageDir: string): void { createSync(joinPath(packageDir, 'changelog.yml'), changelogTemplate); } -function createReadme(packageDir: string, integration: Integration) { - const readmeDirPath = joinPath(packageDir, '_dev/build/docs/'); - const mainReadmeDirPath = joinPath(packageDir, 'docs/'); - ensureDirSync(mainReadmeDirPath); - ensureDirSync(readmeDirPath); - const readmeTemplate = nunjucks.render('package_readme.md.njk', { - package_name: integration.name, - data_streams: integration.dataStreams, - }); - - createSync(joinPath(readmeDirPath, 'README.md'), readmeTemplate); - createSync(joinPath(mainReadmeDirPath, 'README.md'), readmeTemplate); -} - async function createZipArchive(workingDir: string, packageDirectoryName: string): Promise { const tmpPackageDir = joinPath(workingDir, packageDirectoryName); const zip = new AdmZip(); @@ -124,6 +121,12 @@ async function createZipArchive(workingDir: string, packageDirectoryName: string return buffer; } +function mergeAndSortFields(fields: Field[], dataStreamFields: Field[]): Field[] { + const mergedFields = [...fields, ...dataStreamFields]; + + return flattenObjectsList(mergedFields); +} + /* eslint-disable @typescript-eslint/naming-convention */ /** * Creates a package manifest dictionary. diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.test.ts b/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.test.ts index 550c6118636cc..0a269fa07a1c8 100644 --- a/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.test.ts +++ b/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.test.ts @@ -81,4 +81,16 @@ describe('createDataStream', () => { expect(render).toHaveBeenCalledWith(`filestream_manifest.yml.njk`, expect.anything()); expect(render).toHaveBeenCalledWith(`azure_eventhub_manifest.yml.njk`, expect.anything()); }); + + it('Should return the list of fields', async () => { + const fields = createDataStream(packageName, dataStreamPath, firstDataStream); + + expect(Array.isArray(fields)).toBe(true); + fields.forEach((field) => { + expect(field).toMatchObject({ + name: expect.any(String), + type: expect.any(String), + }); + }); + }); }); diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.ts b/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.ts index 02b3f12f53d68..d66ee1958b3ea 100644 --- a/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.ts +++ b/x-pack/plugins/integration_assistant/server/integration_builder/data_stream.ts @@ -7,14 +7,16 @@ import nunjucks from 'nunjucks'; import { join as joinPath } from 'path'; +import { load } from 'js-yaml'; import type { DataStream } from '../../common'; -import { copySync, createSync, ensureDirSync, listDirSync } from '../util'; +import { copySync, createSync, ensureDirSync, listDirSync, readSync } from '../util'; +import { Field } from '../util/samples'; export function createDataStream( packageName: string, specificDataStreamDir: string, dataStream: DataStream -): void { +): Field[] { const dataStreamName = dataStream.name; const pipelineDir = joinPath(specificDataStreamDir, 'elasticsearch', 'ingest_pipeline'); const title = dataStream.title; @@ -23,7 +25,7 @@ export function createDataStream( const useMultilineNDJSON = samplesFormat.name === 'ndjson' && samplesFormat.multiline === true; ensureDirSync(specificDataStreamDir); - createDataStreamFolders(specificDataStreamDir, pipelineDir); + const fields = createDataStreamFolders(specificDataStreamDir, pipelineDir); createPipelineTests(specificDataStreamDir, dataStream.rawSamples, packageName, dataStreamName); const dataStreams: string[] = []; @@ -51,19 +53,34 @@ export function createDataStream( }); createSync(joinPath(specificDataStreamDir, 'manifest.yml'), finalManifest); + + return fields; +} + +function createDataStreamFolders(specificDataStreamDir: string, pipelineDir: string): Field[] { + ensureDirSync(pipelineDir); + return copyFilesFromTemplateDir(specificDataStreamDir); } -function createDataStreamFolders(specificDataStreamDir: string, pipelineDir: string): void { +function copyFilesFromTemplateDir(specificDataStreamDir: string): Field[] { const dataStreamTemplatesDir = joinPath(__dirname, '../templates/data_stream'); const items = listDirSync(dataStreamTemplatesDir); + return items.flatMap((item) => { + const sourcePath = joinPath(dataStreamTemplatesDir, item); + const destinationPath = joinPath(specificDataStreamDir, item); + copySync(sourcePath, destinationPath); + const files = listDirSync(sourcePath); - for (const item of items) { - const s = joinPath(dataStreamTemplatesDir, item); - const d = joinPath(specificDataStreamDir, item); - copySync(s, d); - } + return loadFieldsFromFiles(sourcePath, files); + }); +} - ensureDirSync(pipelineDir); +function loadFieldsFromFiles(sourcePath: string, files: string[]): Field[] { + return files.flatMap((file) => { + const filePath = joinPath(sourcePath, file); + const content = readSync(filePath); + return load(content) as Field[]; + }); } function createPipelineTests( diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/fields.test.ts b/x-pack/plugins/integration_assistant/server/integration_builder/fields.test.ts index a657f699cfff9..bb76577d64fd3 100644 --- a/x-pack/plugins/integration_assistant/server/integration_builder/fields.test.ts +++ b/x-pack/plugins/integration_assistant/server/integration_builder/fields.test.ts @@ -65,4 +65,38 @@ describe('createFieldMapping', () => { ); expect(createSync).toHaveBeenCalledWith(`${dataStreamPath}/fields/fields.yml`, expectedFields); }); + + it('Should return all fields flattened', async () => { + const docs: Docs = [ + { + key: 'foo', + anotherKey: 'bar', + }, + ]; + + const baseFields = `- name: data_stream.type + type: constant_keyword + description: Data stream type. +- name: data_stream.dataset + type: constant_keyword +- name: "@timestamp" + type: date + description: Event timestamp. +`; + (render as jest.Mock).mockReturnValue(baseFields); + + const fieldsResult = createFieldMapping(packageName, dataStreamName, dataStreamPath, docs); + + expect(fieldsResult).toEqual([ + { + name: 'data_stream.type', + type: 'constant_keyword', + description: 'Data stream type.', + }, + { name: 'data_stream.dataset', type: 'constant_keyword' }, + { name: '@timestamp', type: 'date', description: 'Event timestamp.' }, + { name: 'key', type: 'keyword' }, + { name: 'anotherKey', type: 'keyword' }, + ]); + }); }); diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/fields.ts b/x-pack/plugins/integration_assistant/server/integration_builder/fields.ts index 79977ef2f3927..476bc0e74d697 100644 --- a/x-pack/plugins/integration_assistant/server/integration_builder/fields.ts +++ b/x-pack/plugins/integration_assistant/server/integration_builder/fields.ts @@ -6,7 +6,8 @@ */ import nunjucks from 'nunjucks'; - +import { load } from 'js-yaml'; +import { Field } from '../util/samples'; import { createSync, generateFields, mergeSamples } from '../util'; export function createFieldMapping( @@ -14,28 +15,33 @@ export function createFieldMapping( dataStreamName: string, specificDataStreamDir: string, docs: object[] -): void { +): Field[] { const dataStreamFieldsDir = `${specificDataStreamDir}/fields`; - createBaseFields(dataStreamFieldsDir, packageName, dataStreamName); - createCustomFields(dataStreamFieldsDir, docs); + const baseFields = createBaseFields(dataStreamFieldsDir, packageName, dataStreamName); + const customFields = createCustomFields(dataStreamFieldsDir, docs); + + return [...baseFields, ...customFields]; } function createBaseFields( dataStreamFieldsDir: string, packageName: string, dataStreamName: string -): void { +): Field[] { const datasetName = `${packageName}.${dataStreamName}`; const baseFields = nunjucks.render('base_fields.yml.njk', { module: packageName, dataset: datasetName, }); - createSync(`${dataStreamFieldsDir}/base-fields.yml`, baseFields); + + return load(baseFields) as Field[]; } -function createCustomFields(dataStreamFieldsDir: string, pipelineResults: object[]): void { +function createCustomFields(dataStreamFieldsDir: string, pipelineResults: object[]): Field[] { const mergedResults = mergeSamples(pipelineResults); const fieldKeys = generateFields(mergedResults); createSync(`${dataStreamFieldsDir}/fields.yml`, fieldKeys); + + return load(fieldKeys) as Field[]; } diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.test.ts b/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.test.ts new file mode 100644 index 0000000000000..ae9080fff8a74 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.test.ts @@ -0,0 +1,182 @@ +/* + * 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 { testIntegration } from '../../__jest__/fixtures/build_integration'; +import { ensureDirSync, createSync } from '../util'; +import { configure } from 'nunjucks'; +import { join as joinPath } from 'path'; +import { createReadme } from './readme_files'; + +jest.mock('../util', () => ({ + ...jest.requireActual('../util'), + createSync: jest.fn(), + ensureDirSync: jest.fn(), +})); + +describe('createReadme', () => { + const integrationPath = 'path'; + + const templateDir = joinPath(__dirname, '../templates'); + configure([templateDir], { + autoescape: false, + }); + + beforeEach(async () => { + jest.clearAllMocks(); + }); + + it('Should create expected files', async () => { + const fields = [ + { + datastream: 'data_stream_1', + fields: [ + { + name: 'data_stream.type', + type: 'constant_keyword', + description: 'Data stream type.', + }, + { + name: 'data_stream.dataset', + type: 'constant_keyword', + description: 'Data stream dataset name.', + }, + { + name: 'event.dataset', + type: 'constant_keyword', + description: 'Event dataset', + value: 'package.datastream', + }, + { name: '@timestamp', type: 'date', description: 'Event timestamp.' }, + ], + }, + { + datastream: 'data_stream_2', + fields: [{ name: '@timestamp', type: 'date', description: 'Event timestamp.' }], + }, + ]; + + createReadme(integrationPath, testIntegration.name, fields); + + expect(createSync).toHaveBeenCalledWith( + `${integrationPath}/_dev/build/docs/README.md`, + expect.any(String) + ); + + // Docs files + expect(ensureDirSync).toHaveBeenCalledWith(`${integrationPath}/docs/`); + expect(createSync).toHaveBeenCalledWith( + `${integrationPath}/docs/README.md`, + expect.any(String) + ); + }); + + it('Should render a table per datastream with fields mapping in package readme', async () => { + const fields = [ + { + datastream: 'data_stream_1', + fields: [ + { + name: 'data_stream.type', + type: 'constant_keyword', + description: 'Data stream type.', + }, + { + name: 'data_stream.dataset', + type: 'constant_keyword', + }, + { + name: 'event.dataset', + type: 'constant_keyword', + description: 'Event dataset', + value: 'package.datastream', + }, + { name: '@timestamp', type: 'date', description: 'Event timestamp.' }, + ], + }, + { + datastream: 'data_stream_2', + fields: [{ name: '@timestamp', type: 'date', description: 'Event timestamp.' }], + }, + ]; + + createReadme(integrationPath, testIntegration.name, fields); + + const firstDatastreamFieldsDisplayed = ` +| Field | Description | Type | +|---|---|---| +| data_stream.type | Data stream type. | constant_keyword | +| data_stream.dataset | | constant_keyword | +| event.dataset | Event dataset | constant_keyword | +| @timestamp | Event timestamp. | date | +`; + + const secondDatastreamFieldsDisplayed = ` +| Field | Description | Type | +|---|---|---| +| @timestamp | Event timestamp. | date | +`; + + expect(createSync).toHaveBeenCalledWith( + `${integrationPath}/docs/README.md`, + expect.stringContaining(firstDatastreamFieldsDisplayed) + ); + + expect(createSync).toHaveBeenCalledWith( + `${integrationPath}/docs/README.md`, + expect.stringContaining(secondDatastreamFieldsDisplayed) + ); + }); + + it('Should not render a fields mapping table in build readme', async () => { + const fields = [ + { + datastream: 'data_stream_1', + fields: [{ name: '@timestamp', type: 'date', description: 'Event timestamp.' }], + }, + ]; + + createReadme(integrationPath, testIntegration.name, fields); + + expect(createSync).toHaveBeenCalledWith( + `${integrationPath}/_dev/build/docs/README.md`, + expect.stringContaining('{{fields "data_stream_1"}}') + ); + }); + + it('Should render a formatted table per datastream with fields mapping in package readme', async () => { + const fields = [ + { + datastream: 'data_stream_1', + fields: [ + { + name: 'data_stream.type', + type: 'constant_keyword', + description: 'Data stream type.\n', + }, + { + name: 'data_stream.dataset', + type: 'constant_keyword', + }, + ], + }, + ]; + + createReadme(integrationPath, testIntegration.name, fields); + + const firstDatastreamFieldsDisplayed = ` +| Field | Description | Type | +|---|---|---| +| data_stream.type | Data stream type. | constant_keyword | +| data_stream.dataset | | constant_keyword | +`; + + expect(createSync).toHaveBeenCalledWith( + `${integrationPath}/docs/README.md`, + expect.stringContaining(firstDatastreamFieldsDisplayed) + ); + }); +}); diff --git a/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.ts b/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.ts new file mode 100644 index 0000000000000..163b2b04b52f9 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/integration_builder/readme_files.ts @@ -0,0 +1,42 @@ +/* + * 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 nunjucks from 'nunjucks'; + +import { join as joinPath } from 'path'; +import { createSync, ensureDirSync } from '../util'; + +export function createReadme(packageDir: string, integrationName: string, fields: object[]) { + createPackageReadme(packageDir, integrationName, fields); + createBuildReadme(packageDir, integrationName, fields); +} + +function createPackageReadme(packageDir: string, integrationName: string, fields: object[]) { + const dirPath = joinPath(packageDir, 'docs/'); + createReadmeFile(dirPath, 'package_readme.md.njk', integrationName, fields); +} + +function createBuildReadme(packageDir: string, integrationName: string, fields: object[]) { + const dirPath = joinPath(packageDir, '_dev/build/docs/'); + createReadmeFile(dirPath, 'build_readme.md.njk', integrationName, fields); +} + +function createReadmeFile( + targetDir: string, + templateName: string, + integrationName: string, + fields: object[] +) { + ensureDirSync(targetDir); + + const template = nunjucks.render(templateName, { + package_name: integrationName, + fields, + }); + + createSync(joinPath(targetDir, 'README.md'), template); +} diff --git a/x-pack/plugins/integration_assistant/server/templates/build_readme.md.njk b/x-pack/plugins/integration_assistant/server/templates/build_readme.md.njk new file mode 100644 index 0000000000000..e23fa4af9efe8 --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/templates/build_readme.md.njk @@ -0,0 +1,8 @@ +{% include "readme.njk" %} +{% for data_stream in fields %} +### {{ data_stream.datastream }} + +Insert a description of the datastream here. + +{% raw %}{{fields {% endraw %}"{{ data_stream.datastream }}"{% raw %}}}{% endraw %} +{% endfor %} \ No newline at end of file diff --git a/x-pack/plugins/integration_assistant/server/templates/package_readme.md.njk b/x-pack/plugins/integration_assistant/server/templates/package_readme.md.njk index 02bf606ab386a..b47e3491b5bc2 100644 --- a/x-pack/plugins/integration_assistant/server/templates/package_readme.md.njk +++ b/x-pack/plugins/integration_assistant/server/templates/package_readme.md.njk @@ -1,38 +1,17 @@ -# {{ package_name }} Integration +{% include "readme.njk" %} +{% for data_stream in fields %} +### {{ data_stream.datastream }} -## Overview - -Explain what the integration is, define the third-party product that is providing data, establish its relationship to the larger ecosystem of Elastic products, and help the reader understand how it can be used to solve a tangible problem. -Check the [overview guidelines](https://www.elastic.co/guide/en/integrations-developer/current/documentation-guidelines.html#idg-docs-guidelines-overview) for more information. - -## Datastreams - -Provide a high-level overview of the kind of data that is collected by the integration. -Check the [datastreams guidelines](https://www.elastic.co/guide/en/integrations-developer/current/documentation-guidelines.html#idg-docs-guidelines-datastreams) for more information. - -## Requirements - -The requirements section helps readers to confirm that the integration will work with their systems. -Check the [requirements guidelines](https://www.elastic.co/guide/en/integrations-developer/current/documentation-guidelines.html#idg-docs-guidelines-requirements) for more information. - -## Setup - -Point the reader to the [Observability Getting started guide](https://www.elastic.co/guide/en/observability/master/observability-get-started.html) for generic, step-by-step instructions. Include any additional setup instructions beyond what’s included in the guide, which may include instructions to update the configuration of a third-party service. -Check the [setup guidelines](https://www.elastic.co/guide/en/integrations-developer/current/documentation-guidelines.html#idg-docs-guidelines-setup) for more information. - -## Troubleshooting (optional) - -Provide information about special cases and exceptions that aren’t necessary for getting started or won’t be applicable to all users. Check the [troubleshooting guidelines](https://www.elastic.co/guide/en/integrations-developer/current/documentation-guidelines.html#idg-docs-guidelines-troubleshooting) for more information. - -## Reference +Insert a description of the datastream here. -Provide detailed information about the log or metric types we support within the integration. Check the [reference guidelines](https://www.elastic.co/guide/en/integrations-developer/current/documentation-guidelines.html#idg-docs-guidelines-reference) for more information. +**ECS Field Reference** -## Logs -{% for data_stream in data_streams %} -### {{ data_stream.name }} +Please refer to the following [document](https://www.elastic.co/guide/en/ecs/current/ecs-field-reference.html) for detailed information on ECS fields. -Insert a description of the datastream here. +**Exported fields** -{% raw %}{{fields {% endraw %}"{{ data_stream.name }}"{% raw %}}}{% endraw %} +| Field | Description | Type | +|---|---|---| +{% for field in data_stream.fields %}| {{ field.name }} | {{ field.description | default('') | replace('\n', ' ') | trim }} | {{ field.type }} | {% endfor %} +{% endfor %} \ No newline at end of file diff --git a/x-pack/plugins/integration_assistant/server/templates/readme.njk b/x-pack/plugins/integration_assistant/server/templates/readme.njk new file mode 100644 index 0000000000000..91c1bf6f1b40c --- /dev/null +++ b/x-pack/plugins/integration_assistant/server/templates/readme.njk @@ -0,0 +1,31 @@ +# {{ package_name }} Integration + +## Overview + +Explain what the integration is, define the third-party product that is providing data, establish its relationship to the larger ecosystem of Elastic products, and help the reader understand how it can be used to solve a tangible problem. +Check the [overview guidelines](https://www.elastic.co/guide/en/integrations-developer/current/documentation-guidelines.html#idg-docs-guidelines-overview) for more information. + +## Datastreams + +Provide a high-level overview of the kind of data that is collected by the integration. +Check the [datastreams guidelines](https://www.elastic.co/guide/en/integrations-developer/current/documentation-guidelines.html#idg-docs-guidelines-datastreams) for more information. + +## Requirements + +The requirements section helps readers to confirm that the integration will work with their systems. +Check the [requirements guidelines](https://www.elastic.co/guide/en/integrations-developer/current/documentation-guidelines.html#idg-docs-guidelines-requirements) for more information. + +## Setup + +Point the reader to the [Observability Getting started guide](https://www.elastic.co/guide/en/observability/master/observability-get-started.html) for generic, step-by-step instructions. Include any additional setup instructions beyond what’s included in the guide, which may include instructions to update the configuration of a third-party service. +Check the [setup guidelines](https://www.elastic.co/guide/en/integrations-developer/current/documentation-guidelines.html#idg-docs-guidelines-setup) for more information. + +## Troubleshooting (optional) + +Provide information about special cases and exceptions that aren’t necessary for getting started or won’t be applicable to all users. Check the [troubleshooting guidelines](https://www.elastic.co/guide/en/integrations-developer/current/documentation-guidelines.html#idg-docs-guidelines-troubleshooting) for more information. + +## Reference + +Provide detailed information about the log or metric types we support within the integration. Check the [reference guidelines](https://www.elastic.co/guide/en/integrations-developer/current/documentation-guidelines.html#idg-docs-guidelines-reference) for more information. + +## Logs \ No newline at end of file diff --git a/x-pack/plugins/integration_assistant/server/util/samples.test.ts b/x-pack/plugins/integration_assistant/server/util/samples.test.ts index 131135e842334..f87f9a96ca2c0 100644 --- a/x-pack/plugins/integration_assistant/server/util/samples.test.ts +++ b/x-pack/plugins/integration_assistant/server/util/samples.test.ts @@ -5,7 +5,164 @@ * 2.0. */ -import { merge } from './samples'; +import { flattenObjectsList, merge } from './samples'; + +describe('flattenObjectsList', () => { + it('Should return a list with flattened key/value entries', async () => { + const result = flattenObjectsList([ + { + name: 'a', + type: 'group', + fields: [ + { + name: 'b', + type: 'keyword', + description: 'Some description for b', + }, + { + name: 'c', + type: 'group', + fields: [ + { + name: 'd', + type: 'keyword', + }, + { + name: 'e', + description: 'Some description for e', + type: 'keyword', + }, + ], + }, + ], + }, + ]); + + expect(result).toEqual([ + { + name: 'a.b', + type: 'keyword', + description: 'Some description for b', + }, + { + name: 'a.c.d', + type: 'keyword', + description: undefined, + }, + { + name: 'a.c.e', + type: 'keyword', + description: 'Some description for e', + }, + ]); + }); + + it('Should return an empty list if passed an empty list', async () => { + const result = flattenObjectsList([]); + + expect(result).toEqual([]); + }); + + it('Should return a list with key/value entries', async () => { + const result = flattenObjectsList([ + { + name: 'a', + type: 'keyword', + description: 'Some description for a', + }, + ]); + + expect(result).toEqual([ + { + name: 'a', + type: 'keyword', + description: 'Some description for a', + }, + ]); + }); + + it('Should return an sorted list of key/value entries', async () => { + const result = flattenObjectsList([ + { + name: 'c', + type: 'group', + fields: [ + { + name: 'b', + type: 'keyword', + description: 'Some description for b', + }, + { + name: 'a', + type: 'group', + fields: [ + { + name: 'e', + type: 'keyword', + description: 'Some description for e', + }, + { + name: 'd', + type: 'keyword', + }, + ], + }, + ], + }, + ]); + + expect(result).toEqual([ + { + name: 'c.a.d', + type: 'keyword', + description: undefined, + }, + { + name: 'c.a.e', + type: 'keyword', + description: 'Some description for e', + }, + { + name: 'c.b', + type: 'keyword', + description: 'Some description for b', + }, + ]); + }); + + it('Should not error if group type is not an array', async () => { + const result = flattenObjectsList([ + { + name: 'a', + type: 'group', + fields: [ + { + name: 'b', + type: 'keyword', + description: 'Some description for b', + }, + { + name: 'c', + type: 'group', + }, + ], + }, + ]); + + expect(result).toEqual([ + { + name: 'a.b', + type: 'keyword', + description: 'Some description for b', + }, + { + name: 'a.c', + type: 'group', + description: undefined, + }, + ]); + }); +}); describe('merge', () => { it('Should return source if target is empty', async () => { diff --git a/x-pack/plugins/integration_assistant/server/util/samples.ts b/x-pack/plugins/integration_assistant/server/util/samples.ts index a29813c1643f8..6993e87a774e9 100644 --- a/x-pack/plugins/integration_assistant/server/util/samples.ts +++ b/x-pack/plugins/integration_assistant/server/util/samples.ts @@ -18,9 +18,10 @@ interface NewObj { }; } -interface Field { +export interface Field { name: string; type: string; + description?: string; fields?: Field[]; } @@ -233,3 +234,43 @@ export function mergeSamples(objects: any[]): string { return JSON.stringify(result, null, 2); } + +export function flattenObjectsList( + obj: Field[] +): Array<{ name: string; type: string; description?: string }> { + const result: Array<{ name: string; type: string; description?: string }> = []; + flattenObject(obj, '', '.', result); + + return sortArrayOfObjects(result); +} + +function flattenObject( + obj: Field[], + parentKey: string = '', + separator: string = '.', + result: Array<{ name: string; type: string; description?: string }> +): void { + obj.forEach((element) => { + if (element.name) { + const newKey = parentKey ? `${parentKey}${separator}${element.name}` : element.name; + + if (element.fields && Array.isArray(element.fields)) { + flattenObject(element.fields, newKey, separator, result); + } else { + result.push({ + name: newKey, + type: element.type, + description: element.description, + }); + } + } + }); +} + +function sortArrayOfObjects( + objectsArray: Array<{ name: string; type: string; description?: string }> +): Array<{ name: string; type: string; description?: string }> { + return objectsArray.sort((a, b) => { + return a.name.localeCompare(b.name); + }); +} From 896dce358c05d6553ac184abae2164a907447c31 Mon Sep 17 00:00:00 2001 From: Shahzad Date: Mon, 30 Sep 2024 18:09:52 +0200 Subject: [PATCH 8/8] [SLOs] Update API docs for group-by field !! (#194393) ## Summary Update API docs for group-by field !! --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> --- .../output/kibana.serverless.staging.yaml | 29 +++++++++----- oas_docs/output/kibana.serverless.yaml | 29 +++++++++----- oas_docs/output/kibana.staging.yaml | 29 +++++++++----- oas_docs/output/kibana.yaml | 29 +++++++++----- .../slo/docs/openapi/slo/bundled.json | 40 ++++++++++++++----- .../slo/docs/openapi/slo/bundled.yaml | 27 ++++++++----- .../schemas/create_slo_request.yaml | 4 +- .../slo/components/schemas/group_by.yaml | 11 +++++ .../schemas/slo_definition_response.yaml | 6 +-- .../schemas/slo_with_summary_response.yaml | 6 +-- .../schemas/update_slo_request.yaml | 2 + 11 files changed, 147 insertions(+), 65 deletions(-) create mode 100644 x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/group_by.yaml diff --git a/oas_docs/output/kibana.serverless.staging.yaml b/oas_docs/output/kibana.serverless.staging.yaml index cf5cdbac0e9a4..a5d53bd71cc83 100644 --- a/oas_docs/output/kibana.serverless.staging.yaml +++ b/oas_docs/output/kibana.serverless.staging.yaml @@ -32014,9 +32014,7 @@ components: description: A description for the SLO. type: string groupBy: - description: optional group by field to use to generate an SLO per distinct value - example: some.field - type: string + $ref: '#/components/schemas/SLOs_group_by' id: description: >- A optional and unique identifier for the SLO. Must be between 8 and @@ -32175,6 +32173,21 @@ components: type: number title: Find SLO response type: object + SLOs_group_by: + description: >- + optional group by field or fields to use to generate an SLO per distinct + value + example: + - - service.name + - service.name + - - service.name + - service.environment + oneOf: + - type: string + - items: + type: string + type: array + title: Group by SLOs_indicator_properties_apm_availability: description: Defines properties for the APM availability indicator type type: object @@ -32765,9 +32778,7 @@ components: example: true type: boolean groupBy: - description: optional group by field to use to generate an SLO per distinct value - example: some.field - type: string + $ref: '#/components/schemas/SLOs_group_by' id: description: The identifier of the SLO. example: 8853df00-ae2e-11ed-90af-09bb6422b258 @@ -32851,9 +32862,7 @@ components: example: true type: boolean groupBy: - description: optional group by field to use to generate an SLO per distinct value - example: some.field - type: string + $ref: '#/components/schemas/SLOs_group_by' id: description: The identifier of the SLO. example: 8853df00-ae2e-11ed-90af-09bb6422b258 @@ -33074,6 +33083,8 @@ components: description: description: A description for the SLO. type: string + groupBy: + $ref: '#/components/schemas/SLOs_group_by' indicator: oneOf: - $ref: '#/components/schemas/SLOs_indicator_properties_custom_kql' diff --git a/oas_docs/output/kibana.serverless.yaml b/oas_docs/output/kibana.serverless.yaml index 675e8c0903b2f..93c3a5533c8a0 100644 --- a/oas_docs/output/kibana.serverless.yaml +++ b/oas_docs/output/kibana.serverless.yaml @@ -15304,9 +15304,7 @@ components: description: A description for the SLO. type: string groupBy: - description: optional group by field to use to generate an SLO per distinct value - example: some.field - type: string + $ref: '#/components/schemas/SLOs_group_by' id: description: >- A optional and unique identifier for the SLO. Must be between 8 and @@ -15465,6 +15463,21 @@ components: type: number title: Find SLO response type: object + SLOs_group_by: + description: >- + optional group by field or fields to use to generate an SLO per distinct + value + example: + - - service.name + - service.name + - - service.name + - service.environment + oneOf: + - type: string + - items: + type: string + type: array + title: Group by SLOs_indicator_properties_apm_availability: description: Defines properties for the APM availability indicator type type: object @@ -16055,9 +16068,7 @@ components: example: true type: boolean groupBy: - description: optional group by field to use to generate an SLO per distinct value - example: some.field - type: string + $ref: '#/components/schemas/SLOs_group_by' id: description: The identifier of the SLO. example: 8853df00-ae2e-11ed-90af-09bb6422b258 @@ -16141,9 +16152,7 @@ components: example: true type: boolean groupBy: - description: optional group by field to use to generate an SLO per distinct value - example: some.field - type: string + $ref: '#/components/schemas/SLOs_group_by' id: description: The identifier of the SLO. example: 8853df00-ae2e-11ed-90af-09bb6422b258 @@ -16364,6 +16373,8 @@ components: description: description: A description for the SLO. type: string + groupBy: + $ref: '#/components/schemas/SLOs_group_by' indicator: oneOf: - $ref: '#/components/schemas/SLOs_indicator_properties_custom_kql' diff --git a/oas_docs/output/kibana.staging.yaml b/oas_docs/output/kibana.staging.yaml index c70f9b4bce454..96352fc0cd962 100644 --- a/oas_docs/output/kibana.staging.yaml +++ b/oas_docs/output/kibana.staging.yaml @@ -40005,9 +40005,7 @@ components: description: A description for the SLO. type: string groupBy: - description: optional group by field to use to generate an SLO per distinct value - example: some.field - type: string + $ref: '#/components/schemas/SLOs_group_by' id: description: >- A optional and unique identifier for the SLO. Must be between 8 and @@ -40166,6 +40164,21 @@ components: type: number title: Find SLO response type: object + SLOs_group_by: + description: >- + optional group by field or fields to use to generate an SLO per distinct + value + example: + - - service.name + - service.name + - - service.name + - service.environment + oneOf: + - type: string + - items: + type: string + type: array + title: Group by SLOs_indicator_properties_apm_availability: description: Defines properties for the APM availability indicator type type: object @@ -40756,9 +40769,7 @@ components: example: true type: boolean groupBy: - description: optional group by field to use to generate an SLO per distinct value - example: some.field - type: string + $ref: '#/components/schemas/SLOs_group_by' id: description: The identifier of the SLO. example: 8853df00-ae2e-11ed-90af-09bb6422b258 @@ -40842,9 +40853,7 @@ components: example: true type: boolean groupBy: - description: optional group by field to use to generate an SLO per distinct value - example: some.field - type: string + $ref: '#/components/schemas/SLOs_group_by' id: description: The identifier of the SLO. example: 8853df00-ae2e-11ed-90af-09bb6422b258 @@ -41065,6 +41074,8 @@ components: description: description: A description for the SLO. type: string + groupBy: + $ref: '#/components/schemas/SLOs_group_by' indicator: oneOf: - $ref: '#/components/schemas/SLOs_indicator_properties_custom_kql' diff --git a/oas_docs/output/kibana.yaml b/oas_docs/output/kibana.yaml index 947ae1155041f..6d53cb1a38bdd 100644 --- a/oas_docs/output/kibana.yaml +++ b/oas_docs/output/kibana.yaml @@ -22291,9 +22291,7 @@ components: description: A description for the SLO. type: string groupBy: - description: optional group by field to use to generate an SLO per distinct value - example: some.field - type: string + $ref: '#/components/schemas/SLOs_group_by' id: description: >- A optional and unique identifier for the SLO. Must be between 8 and @@ -22452,6 +22450,21 @@ components: type: number title: Find SLO response type: object + SLOs_group_by: + description: >- + optional group by field or fields to use to generate an SLO per distinct + value + example: + - - service.name + - service.name + - - service.name + - service.environment + oneOf: + - type: string + - items: + type: string + type: array + title: Group by SLOs_indicator_properties_apm_availability: description: Defines properties for the APM availability indicator type type: object @@ -23042,9 +23055,7 @@ components: example: true type: boolean groupBy: - description: optional group by field to use to generate an SLO per distinct value - example: some.field - type: string + $ref: '#/components/schemas/SLOs_group_by' id: description: The identifier of the SLO. example: 8853df00-ae2e-11ed-90af-09bb6422b258 @@ -23128,9 +23139,7 @@ components: example: true type: boolean groupBy: - description: optional group by field to use to generate an SLO per distinct value - example: some.field - type: string + $ref: '#/components/schemas/SLOs_group_by' id: description: The identifier of the SLO. example: 8853df00-ae2e-11ed-90af-09bb6422b258 @@ -23351,6 +23360,8 @@ components: description: description: A description for the SLO. type: string + groupBy: + $ref: '#/components/schemas/SLOs_group_by' indicator: oneOf: - $ref: '#/components/schemas/SLOs_indicator_properties_custom_kql' diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.json b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.json index 7e28ae729cc46..b8d3e28ce210a 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.json +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.json @@ -1738,6 +1738,31 @@ } } }, + "group_by": { + "title": "Group by", + "description": "optional group by field or fields to use to generate an SLO per distinct value", + "example": [ + [ + "service.name" + ], + "service.name", + [ + "service.name", + "service.environment" + ] + ], + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ] + }, "slo_with_summary_response": { "title": "SLO response", "type": "object", @@ -1835,9 +1860,7 @@ "example": true }, "groupBy": { - "description": "optional group by field to use to generate an SLO per distinct value", - "type": "string", - "example": "some.field" + "$ref": "#/components/schemas/group_by" }, "instanceId": { "description": "the value derived from the groupBy field, if present, otherwise '*'", @@ -2046,9 +2069,7 @@ "$ref": "#/components/schemas/settings" }, "groupBy": { - "description": "optional group by field to use to generate an SLO per distinct value", - "type": "string", - "example": "some.field" + "$ref": "#/components/schemas/group_by" }, "tags": { "description": "List of tags", @@ -2142,6 +2163,9 @@ "settings": { "$ref": "#/components/schemas/settings" }, + "groupBy": { + "$ref": "#/components/schemas/group_by" + }, "tags": { "description": "List of tags", "type": "array", @@ -2243,9 +2267,7 @@ "example": true }, "groupBy": { - "description": "optional group by field to use to generate an SLO per distinct value", - "type": "string", - "example": "some.field" + "$ref": "#/components/schemas/group_by" }, "tags": { "description": "List of tags", diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.yaml index 0426d2d03bc89..dc57f3e4ea4f6 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.yaml +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/bundled.yaml @@ -1204,6 +1204,19 @@ components: example: 0.9836 errorBudget: $ref: '#/components/schemas/error_budget' + group_by: + title: Group by + description: optional group by field or fields to use to generate an SLO per distinct value + example: + - - service.name + - service.name + - - service.name + - service.environment + oneOf: + - type: string + - type: array + items: + type: string slo_with_summary_response: title: SLO response type: object @@ -1274,9 +1287,7 @@ components: type: boolean example: true groupBy: - description: optional group by field to use to generate an SLO per distinct value - type: string - example: some.field + $ref: '#/components/schemas/group_by' instanceId: description: the value derived from the groupBy field, if present, otherwise '*' type: string @@ -1425,9 +1436,7 @@ components: settings: $ref: '#/components/schemas/settings' groupBy: - description: optional group by field to use to generate an SLO per distinct value - type: string - example: some.field + $ref: '#/components/schemas/group_by' tags: description: List of tags type: array @@ -1487,6 +1496,8 @@ components: $ref: '#/components/schemas/objective' settings: $ref: '#/components/schemas/settings' + groupBy: + $ref: '#/components/schemas/group_by' tags: description: List of tags type: array @@ -1558,9 +1569,7 @@ components: type: boolean example: true groupBy: - description: optional group by field to use to generate an SLO per distinct value - type: string - example: some.field + $ref: '#/components/schemas/group_by' tags: description: List of tags type: array diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/create_slo_request.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/create_slo_request.yaml index c3a848fe52133..292836da1c535 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/create_slo_request.yaml +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/create_slo_request.yaml @@ -37,9 +37,7 @@ properties: settings: $ref: "settings.yaml" groupBy: - description: optional group by field to use to generate an SLO per distinct value - type: string - example: "some.field" + $ref: "group_by.yaml" tags: description: List of tags type: array diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/group_by.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/group_by.yaml new file mode 100644 index 0000000000000..6870d539c17ee --- /dev/null +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/group_by.yaml @@ -0,0 +1,11 @@ +title: Group by +description: optional group by field or fields to use to generate an SLO per distinct value +example: + - [ "service.name" ] + - service.name + - [ "service.name", "service.environment" ] +oneOf: + - type: string + - type: array + items: + type: string diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/slo_definition_response.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/slo_definition_response.yaml index 0b4ffa774d10f..430b105eb32fc 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/slo_definition_response.yaml +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/slo_definition_response.yaml @@ -63,9 +63,7 @@ properties: type: boolean example: true groupBy: - description: optional group by field to use to generate an SLO per distinct value - type: string - example: "some.field" + $ref: "group_by.yaml" tags: description: List of tags type: array @@ -82,4 +80,4 @@ properties: version: description: The internal SLO version type: number - example: 2 \ No newline at end of file + example: 2 diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/slo_with_summary_response.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/slo_with_summary_response.yaml index df8e35996feb3..3da2423acb154 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/slo_with_summary_response.yaml +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/slo_with_summary_response.yaml @@ -67,9 +67,7 @@ properties: type: boolean example: true groupBy: - description: optional group by field to use to generate an SLO per distinct value - type: string - example: "some.field" + $ref: "group_by.yaml" instanceId: description: the value derived from the groupBy field, if present, otherwise '*' type: string @@ -90,4 +88,4 @@ properties: version: description: The internal SLO version type: number - example: 2 \ No newline at end of file + example: 2 diff --git a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/update_slo_request.yaml b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/update_slo_request.yaml index 8d2c61c7b2249..95603878e8e99 100644 --- a/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/update_slo_request.yaml +++ b/x-pack/plugins/observability_solution/slo/docs/openapi/slo/components/schemas/update_slo_request.yaml @@ -26,6 +26,8 @@ properties: $ref: "objective.yaml" settings: $ref: "settings.yaml" + groupBy: + $ref: "group_by.yaml" tags: description: List of tags type: array