diff --git a/docs/apm/api.asciidoc b/docs/apm/api.asciidoc new file mode 100644 index 0000000000000..b520cc46bef8d --- /dev/null +++ b/docs/apm/api.asciidoc @@ -0,0 +1,260 @@ +[role="xpack"] +[[apm-api]] +== API + +Some APM app features are provided via a REST API: + +* <> + +TIP: Kibana provides additional <>, +and general information on <>. + +//// +******************************************************* +//// + +[[agent-config-api]] +=== Agent Configuration API + +The Agent configuration API allows you to fine-tune your APM agent configuration, +without needing to redeploy your application. + +The following Agent configuration APIs are available: + +* <> to create or update an Agent configuration +* <> to delete an Agent configuration. +* <> to list all Agent configurations. +* <> to search for an Agent configuration. + +//// +******************************************************* +//// + +[[apm-update-config]] +==== Create or update configuration + +[[apm-update-config-req]] +===== Request + +`PUT /api/apm/settings/agent-configuration` + +[[apm-update-config-req-body]] +===== Request body + +`service`:: +(required, object) Service identifying the configuration to create or update. + +`name` ::: + (required, string) Name of service + +`environment` ::: + (optional, string) Environment of service + +`settings`:: +(required) Key/value object with settings and their corresponding value. + +`agent_name`:: +(optional) The agent name is used by the UI to determine which settings to display. + + +[[apm-update-config-example]] +===== Example + +[source,console] +-------------------------------------------------- +PUT /api/apm/settings/agent-configuration +{ + "service" : { + "name" : "frontend", + "environment" : "production" + }, + "settings" : { + "transaction_sample_rate" : 0.4, + "capture_body" : "off", + "transaction_max_spans" : 500 + }, + "agent_name": "nodejs" +} +-------------------------------------------------- + +//// +******************************************************* +//// + + +[[apm-delete-config]] +==== Delete configuration + +[[apm-delete-config-req]] +===== Request + +`DELETE /api/apm/settings/agent-configuration` + +[[apm-delete-config-req-body]] +===== Request body +`service`:: +(required, object) Service identifying the configuration to delete + +`name` ::: + (required, string) Name of service + +`environment` ::: + (optional, string) Environment of service + + +[[apm-delete-config-example]] +===== Example + +[source,console] +-------------------------------------------------- +DELETE /api/apm/settings/agent-configuration +{ + "service" : { + "name" : "frontend", + "environment": "production" + } +} +-------------------------------------------------- + +//// +******************************************************* +//// + + +[[apm-list-config]] +==== List configuration + + +[[apm-list-config-req]] +===== Request + +`GET /api/apm/settings/agent-configuration` + +[[apm-list-config-body]] +===== Response body + +[source,js] +-------------------------------------------------- +[ + { + "agent_name": "go", + "service": { + "name": "opbeans-go", + "environment": "production" + }, + "settings": { + "transaction_sample_rate": 1, + "capture_body": "off", + "transaction_max_spans": 200 + }, + "@timestamp": 1581934104843, + "applied_by_agent": false, + "etag": "1e58c178efeebae15c25c539da740d21dee422fc" + }, + { + "agent_name": "go", + "service": { + "name": "opbeans-go" + }, + "settings": { + "transaction_sample_rate": 1, + "capture_body": "off", + "transaction_max_spans": 300 + }, + "@timestamp": 1581934111727, + "applied_by_agent": false, + "etag": "3eed916d3db434d9fb7f039daa681c7a04539a64" + }, + { + "agent_name": "nodejs", + "service": { + "name": "frontend" + }, + "settings": { + "transaction_sample_rate": 1, + }, + "@timestamp": 1582031336265, + "applied_by_agent": false, + "etag": "5080ed25785b7b19f32713681e79f46996801a5b" + } +] +-------------------------------------------------- + +[[apm-list-config-example]] +===== Example + +[source,console] +-------------------------------------------------- +GET /api/apm/settings/agent-configuration +-------------------------------------------------- + +//// +******************************************************* +//// + + +[[apm-search-config]] +==== Search configuration + +[[apm-search-config-req]] +===== Request + +`POST /api/apm/settings/agent-configuration/search` + +[[apm-search-config-req-body]] +===== Request body + +`service`:: +(required, object) Service identifying the configuration. + +`name` ::: + (required, string) Name of service + +`environment` ::: + (optional, string) Environment of service + +`etag`:: +(required) etag is sent by the agent to indicate the etag of the last successfully applied configuration. If the etag matches an existing configuration its `applied_by_agent` property will be set to `true`. Every time a configuration is edited `applied_by_agent` is reset to `false`. + +[[apm-search-config-body]] +===== Response body + +[source,js] +-------------------------------------------------- +{ + "_index": ".apm-agent-configuration", + "_id": "CIaqXXABmQCdPphWj8EJ", + "_score": 2, + "_source": { + "agent_name": "nodejs", + "service": { + "name": "frontend" + }, + "settings": { + "transaction_sample_rate": 1, + }, + "@timestamp": 1582031336265, + "applied_by_agent": false, + "etag": "5080ed25785b7b19f32713681e79f46996801a5b" + } +} +-------------------------------------------------- + +[[apm-search-config-example]] +===== Example + +[source,console] +-------------------------------------------------- +POST /api/apm/settings/agent-configuration/search +{ + "etag" : "1e58c178efeebae15c25c539da740d21dee422fc", + "service" : { + "name" : "frontend", + "environment": "production" + } +} +-------------------------------------------------- + +//// +******************************************************* +//// diff --git a/docs/apm/index.asciidoc b/docs/apm/index.asciidoc index 0c028f64603d8..b6defdeed2842 100644 --- a/docs/apm/index.asciidoc +++ b/docs/apm/index.asciidoc @@ -30,3 +30,5 @@ include::getting-started.asciidoc[] include::bottlenecks.asciidoc[] include::using-the-apm-ui.asciidoc[] + +include::api.asciidoc[] diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx index 496147b02589b..1564f1ae746a9 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx @@ -51,12 +51,18 @@ async function deleteConfig( ) { try { await callApmApi({ - pathname: '/api/apm/settings/agent-configuration/{configurationId}', + pathname: '/api/apm/settings/agent-configuration', method: 'DELETE', params: { - path: { configurationId: selectedConfig.id } + body: { + service: { + name: selectedConfig.service.name, + environment: selectedConfig.service.environment + } + } } }); + toasts.addSuccess({ title: i18n.translate( 'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededTitle', diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx index 653dedea733f2..c77617fbb424f 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx @@ -135,8 +135,8 @@ export function AddEditFlyout({ sampleRate, captureBody, transactionMaxSpans, - configurationId: selectedConfig ? selectedConfig.id : undefined, agentName, + isExistingConfig: Boolean(selectedConfig), toasts, trackApmEvent }); diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/saveConfig.ts b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/saveConfig.ts index 19934cafb4694..d36120a054795 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/saveConfig.ts +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/saveConfig.ts @@ -27,8 +27,8 @@ export async function saveConfig({ sampleRate, captureBody, transactionMaxSpans, - configurationId, agentName, + isExistingConfig, toasts, trackApmEvent }: { @@ -38,8 +38,8 @@ export async function saveConfig({ sampleRate: string; captureBody: string; transactionMaxSpans: string; - configurationId?: string; agentName?: string; + isExistingConfig: boolean; toasts: NotificationsStart['toasts']; trackApmEvent: UiTracker; }) { @@ -64,24 +64,14 @@ export async function saveConfig({ settings }; - if (configurationId) { - await callApmApi({ - pathname: '/api/apm/settings/agent-configuration/{configurationId}', - method: 'PUT', - params: { - path: { configurationId }, - body: configuration - } - }); - } else { - await callApmApi({ - pathname: '/api/apm/settings/agent-configuration/new', - method: 'POST', - params: { - body: configuration - } - }); - } + await callApmApi({ + pathname: '/api/apm/settings/agent-configuration', + method: 'PUT', + params: { + query: { overwrite: isExistingConfig }, + body: configuration + } + }); toasts.addSuccess({ title: i18n.translate( diff --git a/x-pack/legacy/plugins/apm/readme.md b/x-pack/legacy/plugins/apm/readme.md index 4892aa1b15bc7..a513249c296db 100644 --- a/x-pack/legacy/plugins/apm/readme.md +++ b/x-pack/legacy/plugins/apm/readme.md @@ -8,7 +8,7 @@ git clone git@github.com:elastic/kibana.git cd kibana/ yarn kbn bootstrap -yarn start +yarn start --no-base-path ``` #### APM Server, Elasticsearch and data @@ -17,8 +17,7 @@ To access an elasticsearch instance that has live data you have two options: ##### A. Connect to Elasticsearch on Cloud (internal devs only) -Add the following to the kibana config file (config/kibana.dev.yml): -https://p.elstc.co/paste/wcp7hGUC#UkNNwAZg6cCasmUQNMw8ZXntSPuau5FMXCJkSnsvXU+ +Find the credentials for the cluster [here](https://github.com/elastic/apm-dev/blob/master/docs/credentials/apm-ui-clusters.md#apmelstcco) ##### B. Start Elastic Stack and APM data generators @@ -72,9 +71,27 @@ node scripts/jest.js plugins/apm --watch node scripts/jest.js plugins/apm --updateSnapshot ``` -### Cypress E2E tests +### Functional tests -See the Cypress-specific [readme.md](cypress/README.md) +**Start server** +`node scripts/functional_tests_server --config x-pack/test/functional/config.js` + +**Run tests** +`node scripts/functional_test_runner --config x-pack/test/functional/config.js --grep='APM specs'` + +APM tests are located in `x-pack/test/functional/apps/apm`. +For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) + +### API integration tests + +**Start server** +`node scripts/functional_tests_server --config x-pack/test/api_integration/config.js` + +**Run tests** +`node scripts/functional_test_runner --config x-pack/test/api_integration/config.js --grep='APM specs'` + +APM tests are located in `x-pack/test/api_integration/apis/apm`. +For debugging access Elasticsearch on http://localhost:9220` (elastic/changeme) ### Linting @@ -92,58 +109,6 @@ yarn prettier "./x-pack/legacy/plugins/apm/**/*.{tsx,ts,js}" --write yarn eslint ./x-pack/legacy/plugins/apm --fix ``` -### Visual Studio Code - -When using [Visual Studio Code](https://code.visualstudio.com/) with APM it's best to set up a [multi-root workspace](https://code.visualstudio.com/docs/editor/multi-root-workspaces) and add the `x-pack/legacy/plugins/apm` directory, the `x-pack` directory, and the root of the Kibana repository to the workspace. This makes it so you can navigate and search within APM and use the wider workspace roots when you need to widen your search. - -#### Using the Jest extension - -The [vscode-jest extension](https://marketplace.visualstudio.com/items?itemName=Orta.vscode-jest) is a good way to run your Jest tests inside the editor. - -Some of the benefits of using the extension over just running it in a terminal are: - -• It shows the pass/fail of a test inline in the test file -• It shows the error message in the test file if it fails -• You don’t have to have the terminal process running -• It can automatically update your snapshots when they change -• Coverage mapping - -The extension doesn't really work well if you're trying to use it on all of Kibana or all of X-Pack, but it works well if you configure it to run only on the files in APM. - -If you have a workspace configured as described above you should have: - -```json -"jest.disabledWorkspaceFolders": ["kibana", "x-pack"] -``` - -in your Workspace settings, and: - -```json -"jest.pathToJest": "node scripts/jest.js --testPathPattern=legacy/plugins/apm", -"jest.rootPath": "../../.." -``` - -in the settings for the APM folder. - -#### Jest debugging - -To make the [VSCode debugger](https://vscode.readthedocs.io/en/latest/editor/debugging/) work with Jest (you can set breakpoints in the code and tests and use the VSCode debugger) you'll need the [Node Debug extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode.node-debug2) installed and can set up a launch configuration like: - -```json -{ - "type": "node", - "name": "APM Jest", - "request": "launch", - "args": ["--runInBand", "--testPathPattern=legacy/plugins/apm"], - "cwd": "${workspaceFolder}/../../..", - "console": "internalConsole", - "internalConsoleOptions": "openOnSessionStart", - "disableOptimisticBPs": true, - "program": "${workspaceFolder}/../../../scripts/jest.js", - "runtimeVersion": "10.15.2" -} -``` - #### Storybook Start the [Storybook](https://storybook.js.org/) development environment with @@ -152,4 +117,6 @@ You can access the development environment at http://localhost:9001. #### Further resources -(you'll want `runtimeVersion` to match what's in the Kibana root .nvmrc. Depending on your setup, you might be able to remove this line.) +- [Cypress integration tests](cypress/README.md) +- [VSCode setup instructions](./dev_docs/vscode_setup.md) +- [Github PR commands](./dev_docs/github_commands.md) diff --git a/x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.test.ts b/x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.test.ts new file mode 100644 index 0000000000000..4c9dc78eb41e9 --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.test.ts @@ -0,0 +1,41 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { agentConfigurationIntakeRt } from './index'; +import { isRight } from 'fp-ts/lib/Either'; + +describe('agentConfigurationIntakeRt', () => { + it('is valid when required parameters are given', () => { + const config = { + service: {}, + settings: {} + }; + + expect(isConfigValid(config)).toBe(true); + }); + + it('is valid when required and optional parameters are given', () => { + const config = { + service: { name: 'my-service', environment: 'my-environment' }, + settings: { + transaction_sample_rate: 0.5, + capture_body: 'foo', + transaction_max_spans: 10 + } + }; + + expect(isConfigValid(config)).toBe(true); + }); + + it('is invalid when required parameters are not given', () => { + const config = {}; + expect(isConfigValid(config)).toBe(false); + }); +}); + +function isConfigValid(config: any) { + return isRight(agentConfigurationIntakeRt.decode(config)); +} diff --git a/x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.ts b/x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.ts new file mode 100644 index 0000000000000..32a2832b5eaf3 --- /dev/null +++ b/x-pack/plugins/apm/common/runtime_types/agent_configuration_intake_rt/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; +import { transactionSampleRateRt } from '../transaction_sample_rate_rt'; +import { transactionMaxSpansRt } from '../transaction_max_spans_rt'; + +export const serviceRt = t.partial({ + name: t.string, + environment: t.string +}); + +export const agentConfigurationIntakeRt = t.intersection([ + t.partial({ agent_name: t.string }), + t.type({ + service: serviceRt, + settings: t.partial({ + transaction_sample_rate: transactionSampleRateRt, + capture_body: t.string, + transaction_max_spans: transactionMaxSpansRt + }) + }) +]); diff --git a/x-pack/plugins/apm/server/lib/helpers/es_client.ts b/x-pack/plugins/apm/server/lib/helpers/es_client.ts index 8ada02d085631..86eb1dba507f0 100644 --- a/x-pack/plugins/apm/server/lib/helpers/es_client.ts +++ b/x-pack/plugins/apm/server/lib/helpers/es_client.ts @@ -7,9 +7,10 @@ /* eslint-disable no-console */ import { IndexDocumentParams, - IndicesCreateParams, IndicesDeleteParams, - SearchParams + SearchParams, + IndicesCreateParams, + DeleteDocumentResponse } from 'elasticsearch'; import { cloneDeep, isString, merge, uniqueId } from 'lodash'; import { KibanaRequest } from 'src/core/server'; @@ -188,7 +189,7 @@ export function getESClient( index: (params: APMIndexDocumentParams) => { return withTime(() => callMethod('index', params)); }, - delete: (params: IndicesDeleteParams) => { + delete: (params: IndicesDeleteParams): Promise => { return withTime(() => callMethod('delete', params)); }, indicesCreate: (params: IndicesCreateParams) => { diff --git a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts index 7120d3bca6c25..ccd8b123e23e2 100644 --- a/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts +++ b/x-pack/plugins/apm/server/lib/services/get_service_node_metadata.ts @@ -58,8 +58,8 @@ export async function getServiceNodeMetadata({ const response = await client.search(query); return { - host: response.aggregations?.host.buckets[0].key || NOT_AVAILABLE_LABEL, + host: response.aggregations?.host.buckets[0]?.key || NOT_AVAILABLE_LABEL, containerId: - response.aggregations?.containerId.buckets[0].key || NOT_AVAILABLE_LABEL + response.aggregations?.containerId.buckets[0]?.key || NOT_AVAILABLE_LABEL }; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 542fdd99e2635..db34b4d5d20b5 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -1,6 +1,90 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`agent configuration queries fetches all environments 1`] = ` +exports[`agent configuration queries findExactConfiguration find configuration by service.environment 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "bool": Object { + "must_not": Array [ + Object { + "exists": Object { + "field": "service.name", + }, + }, + ], + }, + }, + Object { + "term": Object { + "service.environment": "bar", + }, + }, + ], + }, + }, + }, + "index": "myIndex", +} +`; + +exports[`agent configuration queries findExactConfiguration find configuration by service.name 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "bool": Object { + "must_not": Array [ + Object { + "exists": Object { + "field": "service.environment", + }, + }, + ], + }, + }, + ], + }, + }, + }, + "index": "myIndex", +} +`; + +exports[`agent configuration queries findExactConfiguration find configuration by service.name and service.environment 1`] = ` +Object { + "body": Object { + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + Object { + "term": Object { + "service.environment": "bar", + }, + }, + ], + }, + }, + }, + "index": "myIndex", +} +`; + +exports[`agent configuration queries getAllEnvironments fetches all environments 1`] = ` Object { "body": Object { "aggs": Object { @@ -41,14 +125,79 @@ Object { } `; -exports[`agent configuration queries fetches configurations 1`] = ` +exports[`agent configuration queries getExistingEnvironmentsForService fetches unavailable environments 1`] = ` +Object { + "body": Object { + "aggs": Object { + "environments": Object { + "terms": Object { + "field": "service.environment", + "missing": "ALL_OPTION_VALUE", + "size": 50, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "term": Object { + "service.name": "foo", + }, + }, + ], + }, + }, + "size": 0, + }, + "index": "myIndex", +} +`; + +exports[`agent configuration queries getServiceNames fetches service names 1`] = ` +Object { + "body": Object { + "aggs": Object { + "services": Object { + "terms": Object { + "field": "service.name", + "size": 50, + }, + }, + }, + "query": Object { + "bool": Object { + "filter": Array [ + Object { + "terms": Object { + "processor.event": Array [ + "transaction", + "error", + "metric", + ], + }, + }, + ], + }, + }, + "size": 0, + }, + "index": Array [ + "myIndex", + "myIndex", + "myIndex", + ], +} +`; + +exports[`agent configuration queries listConfigurations fetches configurations 1`] = ` Object { "index": "myIndex", "size": 200, } `; -exports[`agent configuration queries fetches filtered configurations with an environment 1`] = ` +exports[`agent configuration queries searchConfigurations fetches filtered configurations with an environment 1`] = ` Object { "body": Object { "query": Object { @@ -60,9 +209,7 @@ Object { "boost": 2, "filter": Object { "term": Object { - "service.name": Object { - "value": "foo", - }, + "service.name": "foo", }, }, }, @@ -72,9 +219,7 @@ Object { "boost": 1, "filter": Object { "term": Object { - "service.environment": Object { - "value": "bar", - }, + "service.environment": "bar", }, }, }, @@ -109,7 +254,7 @@ Object { } `; -exports[`agent configuration queries fetches filtered configurations without an environment 1`] = ` +exports[`agent configuration queries searchConfigurations fetches filtered configurations without an environment 1`] = ` Object { "body": Object { "query": Object { @@ -121,9 +266,7 @@ Object { "boost": 2, "filter": Object { "term": Object { - "service.name": Object { - "value": "foo", - }, + "service.name": "foo", }, }, }, @@ -157,68 +300,3 @@ Object { "index": "myIndex", } `; - -exports[`agent configuration queries fetches service names 1`] = ` -Object { - "body": Object { - "aggs": Object { - "services": Object { - "terms": Object { - "field": "service.name", - "size": 50, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "terms": Object { - "processor.event": Array [ - "transaction", - "error", - "metric", - ], - }, - }, - ], - }, - }, - "size": 0, - }, - "index": Array [ - "myIndex", - "myIndex", - "myIndex", - ], -} -`; - -exports[`agent configuration queries fetches unavailable environments 1`] = ` -Object { - "body": Object { - "aggs": Object { - "environments": Object { - "terms": Object { - "field": "service.environment", - "missing": "ALL_OPTION_VALUE", - "size": 50, - }, - }, - }, - "query": Object { - "bool": Object { - "filter": Array [ - Object { - "term": Object { - "service.name": "foo", - }, - }, - ], - }, - }, - "size": 0, - }, - "index": "myIndex", -} -`; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts index ea8f50c90c1d3..ddbe6892c5441 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts @@ -4,18 +4,15 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface AgentConfiguration { +import t from 'io-ts'; +import { agentConfigurationIntakeRt } from '../../../../common/runtime_types/agent_configuration_intake_rt'; + +export type AgentConfigurationIntake = t.TypeOf< + typeof agentConfigurationIntakeRt +>; +export type AgentConfiguration = { '@timestamp': number; applied_by_agent?: boolean; etag?: string; agent_name?: string; - service: { - name?: string; - environment?: string; - }; - settings: { - transaction_sample_rate?: number; - capture_body?: string; - transaction_max_spans?: number; - }; -} +} & AgentConfigurationIntake; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts index 5a67f78de6f65..74fcc61dde863 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -6,19 +6,19 @@ import hash from 'object-hash'; import { Setup } from '../../helpers/setup_request'; -import { AgentConfiguration } from './configuration_types'; +import { + AgentConfiguration, + AgentConfigurationIntake +} from './configuration_types'; import { APMIndexDocumentParams } from '../../helpers/es_client'; export async function createOrUpdateConfiguration({ configurationId, - configuration, + configurationIntake, setup }: { configurationId?: string; - configuration: Omit< - AgentConfiguration, - '@timestamp' | 'applied_by_agent' | 'etag' - >; + configurationIntake: AgentConfigurationIntake; setup: Setup; }) { const { internalClient, indices } = setup; @@ -27,15 +27,15 @@ export async function createOrUpdateConfiguration({ refresh: true, index: indices.apmAgentConfigurationIndex, body: { - agent_name: configuration.agent_name, + agent_name: configurationIntake.agent_name, service: { - name: configuration.service.name, - environment: configuration.service.environment + name: configurationIntake.service.name, + environment: configurationIntake.service.environment }, - settings: configuration.settings, + settings: configurationIntake.settings, '@timestamp': Date.now(), applied_by_agent: false, - etag: hash(configuration) + etag: hash(configurationIntake) } }; diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts new file mode 100644 index 0000000000000..eea409882f876 --- /dev/null +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/find_exact_configuration.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +import { + SERVICE_NAME, + SERVICE_ENVIRONMENT +} from '../../../../common/elasticsearch_fieldnames'; +import { Setup } from '../../helpers/setup_request'; +import { AgentConfiguration } from './configuration_types'; +import { ESSearchHit } from '../../../../typings/elasticsearch'; + +export async function findExactConfiguration({ + service, + setup +}: { + service: AgentConfiguration['service']; + setup: Setup; +}) { + const { internalClient, indices } = setup; + + const serviceNameFilter = service.name + ? { term: { [SERVICE_NAME]: service.name } } + : { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } }; + + const environmentFilter = service.environment + ? { term: { [SERVICE_ENVIRONMENT]: service.environment } } + : { bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } }; + + const params = { + index: indices.apmAgentConfigurationIndex, + body: { + query: { + bool: { filter: [serviceNameFilter, environmentFilter] } + } + } + }; + + const resp = await internalClient.search( + params + ); + + return resp.hits.hits[0] as ESSearchHit | undefined; +} diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts index dccf8b110d082..a9af1f6174fd5 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts @@ -48,8 +48,6 @@ export async function getAgentNameByService({ }; const { aggregations } = await client.search(params); - const agentName = aggregations?.agent_names.buckets[0].key as - | string - | undefined; - return { agentName }; + const agentName = aggregations?.agent_names.buckets[0]?.key; + return agentName as string | undefined; } diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts index a82d148781ad8..b951b7f350eed 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts @@ -8,11 +8,12 @@ import { getAllEnvironments } from './get_environments/get_all_environments'; import { getExistingEnvironmentsForService } from './get_environments/get_existing_environments_for_service'; import { getServiceNames } from './get_service_names'; import { listConfigurations } from './list_configurations'; -import { searchConfigurations } from './search'; +import { searchConfigurations } from './search_configurations'; import { SearchParamsMock, inspectSearchParams } from '../../../../../../legacy/plugins/apm/public/utils/testHelpers'; +import { findExactConfiguration } from './find_exact_configuration'; describe('agent configuration queries', () => { let mock: SearchParamsMock; @@ -21,68 +22,117 @@ describe('agent configuration queries', () => { mock.teardown(); }); - it('fetches all environments', async () => { - mock = await inspectSearchParams(setup => - getAllEnvironments({ - serviceName: 'foo', - setup - }) - ); + describe('getAllEnvironments', () => { + it('fetches all environments', async () => { + mock = await inspectSearchParams(setup => + getAllEnvironments({ + serviceName: 'foo', + setup + }) + ); - expect(mock.params).toMatchSnapshot(); + expect(mock.params).toMatchSnapshot(); + }); }); - it('fetches unavailable environments', async () => { - mock = await inspectSearchParams(setup => - getExistingEnvironmentsForService({ - serviceName: 'foo', - setup - }) - ); + describe('getExistingEnvironmentsForService', () => { + it('fetches unavailable environments', async () => { + mock = await inspectSearchParams(setup => + getExistingEnvironmentsForService({ + serviceName: 'foo', + setup + }) + ); - expect(mock.params).toMatchSnapshot(); + expect(mock.params).toMatchSnapshot(); + }); }); - it('fetches service names', async () => { - mock = await inspectSearchParams(setup => - getServiceNames({ - setup - }) - ); + describe('getServiceNames', () => { + it('fetches service names', async () => { + mock = await inspectSearchParams(setup => + getServiceNames({ + setup + }) + ); - expect(mock.params).toMatchSnapshot(); + expect(mock.params).toMatchSnapshot(); + }); }); - it('fetches configurations', async () => { - mock = await inspectSearchParams(setup => - listConfigurations({ - setup - }) - ); + describe('listConfigurations', () => { + it('fetches configurations', async () => { + mock = await inspectSearchParams(setup => + listConfigurations({ + setup + }) + ); - expect(mock.params).toMatchSnapshot(); + expect(mock.params).toMatchSnapshot(); + }); }); - it('fetches filtered configurations without an environment', async () => { - mock = await inspectSearchParams(setup => - searchConfigurations({ - serviceName: 'foo', - setup - }) - ); + describe('searchConfigurations', () => { + it('fetches filtered configurations without an environment', async () => { + mock = await inspectSearchParams(setup => + searchConfigurations({ + service: { + name: 'foo' + }, + setup + }) + ); - expect(mock.params).toMatchSnapshot(); + expect(mock.params).toMatchSnapshot(); + }); + + it('fetches filtered configurations with an environment', async () => { + mock = await inspectSearchParams(setup => + searchConfigurations({ + service: { + name: 'foo', + environment: 'bar' + }, + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); }); - it('fetches filtered configurations with an environment', async () => { - mock = await inspectSearchParams(setup => - searchConfigurations({ - serviceName: 'foo', - environment: 'bar', - setup - }) - ); + describe('findExactConfiguration', () => { + it('find configuration by service.name', async () => { + mock = await inspectSearchParams(setup => + findExactConfiguration({ + service: { name: 'foo' }, + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('find configuration by service.environment', async () => { + mock = await inspectSearchParams(setup => + findExactConfiguration({ + service: { environment: 'bar' }, + setup + }) + ); + + expect(mock.params).toMatchSnapshot(); + }); + + it('find configuration by service.name and service.environment', async () => { + mock = await inspectSearchParams(setup => + findExactConfiguration({ + service: { name: 'foo', environment: 'bar' }, + setup + }) + ); - expect(mock.params).toMatchSnapshot(); + expect(mock.params).toMatchSnapshot(); + }); }); }); diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts similarity index 68% rename from x-pack/plugins/apm/server/lib/settings/agent_configuration/search.ts rename to x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts index 766baead006b6..9bbdc96a3a797 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/search.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/search_configurations.ts @@ -12,29 +12,39 @@ import { Setup } from '../../helpers/setup_request'; import { AgentConfiguration } from './configuration_types'; export async function searchConfigurations({ - serviceName, - environment, + service, setup }: { - serviceName: string; - environment?: string; + service: AgentConfiguration['service']; setup: Setup; }) { const { internalClient, indices } = setup; - const environmentFilter = environment + + // In the following `constant_score` is being used to disable IDF calculation (where frequency of a term influences scoring). + // Additionally a boost has been added to service.name to ensure it scores higher. + // If there is tie between a config with a matching service.name and a config with a matching environment, the config that matches service.name wins + const serviceNameFilter = service.name + ? [ + { + constant_score: { + filter: { term: { [SERVICE_NAME]: service.name } }, + boost: 2 + } + } + ] + : []; + + const environmentFilter = service.environment ? [ { constant_score: { - filter: { term: { [SERVICE_ENVIRONMENT]: { value: environment } } }, + filter: { term: { [SERVICE_ENVIRONMENT]: service.environment } }, boost: 1 } } ] : []; - // In the following `constant_score` is being used to disable IDF calculation (where frequency of a term influences scoring) - // Additionally a boost has been added to service.name to ensure it scores higher - // if there is tie between a config with a matching service.name and a config with a matching environment const params = { index: indices.apmAgentConfigurationIndex, body: { @@ -42,12 +52,7 @@ export async function searchConfigurations({ bool: { minimum_should_match: 2, should: [ - { - constant_score: { - filter: { term: { [SERVICE_NAME]: { value: serviceName } } }, - boost: 2 - } - }, + ...serviceNameFilter, ...environmentFilter, { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } }, { bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } } diff --git a/x-pack/plugins/apm/server/routes/create_apm_api.ts b/x-pack/plugins/apm/server/routes/create_apm_api.ts index f65e271389938..21392edbb2c48 100644 --- a/x-pack/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/plugins/apm/server/routes/create_apm_api.ts @@ -23,11 +23,10 @@ import { import { agentConfigurationRoute, agentConfigurationSearchRoute, - createAgentConfigurationRoute, deleteAgentConfigurationRoute, listAgentConfigurationEnvironmentsRoute, listAgentConfigurationServicesRoute, - updateAgentConfigurationRoute, + createOrUpdateAgentConfigurationRoute, agentConfigurationAgentNameRoute } from './settings/agent_configuration'; import { @@ -83,11 +82,10 @@ const createApmApi = () => { .add(agentConfigurationAgentNameRoute) .add(agentConfigurationRoute) .add(agentConfigurationSearchRoute) - .add(createAgentConfigurationRoute) .add(deleteAgentConfigurationRoute) .add(listAgentConfigurationEnvironmentsRoute) .add(listAgentConfigurationServicesRoute) - .add(updateAgentConfigurationRoute) + .add(createOrUpdateAgentConfigurationRoute) // APM indices .add(apmIndexSettingsRoute) diff --git a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts index ddd6a27025131..83b845b1fc436 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -9,15 +9,19 @@ import Boom from 'boom'; import { setupRequest } from '../../lib/helpers/setup_request'; import { getServiceNames } from '../../lib/settings/agent_configuration/get_service_names'; import { createOrUpdateConfiguration } from '../../lib/settings/agent_configuration/create_or_update_configuration'; -import { searchConfigurations } from '../../lib/settings/agent_configuration/search'; +import { searchConfigurations } from '../../lib/settings/agent_configuration/search_configurations'; +import { findExactConfiguration } from '../../lib/settings/agent_configuration/find_exact_configuration'; import { listConfigurations } from '../../lib/settings/agent_configuration/list_configurations'; import { getEnvironments } from '../../lib/settings/agent_configuration/get_environments'; import { deleteConfiguration } from '../../lib/settings/agent_configuration/delete_configuration'; import { createRoute } from '../create_route'; -import { transactionSampleRateRt } from '../../../common/runtime_types/transaction_sample_rate_rt'; -import { transactionMaxSpansRt } from '../../../common/runtime_types/transaction_max_spans_rt'; import { getAgentNameByService } from '../../lib/settings/agent_configuration/get_agent_name_by_service'; import { markAppliedByAgent } from '../../lib/settings/agent_configuration/mark_applied_by_agent'; +import { + serviceRt, + agentConfigurationIntakeRt +} from '../../../common/runtime_types/agent_configuration_intake_rt'; +import { jsonRt } from '../../../common/runtime_types/json_rt'; // get list of configurations export const agentConfigurationRoute = createRoute(core => ({ @@ -31,20 +35,34 @@ export const agentConfigurationRoute = createRoute(core => ({ // delete configuration export const deleteAgentConfigurationRoute = createRoute(() => ({ method: 'DELETE', - path: '/api/apm/settings/agent-configuration/{configurationId}', + path: '/api/apm/settings/agent-configuration', options: { tags: ['access:apm', 'access:apm_write'] }, params: { - path: t.type({ - configurationId: t.string + body: t.type({ + service: serviceRt }) }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const { configurationId } = context.params.path; + const { service } = context.params.body; + + const config = await findExactConfiguration({ service, setup }); + if (!config) { + context.logger.info( + `Config was not found for ${service.name}/${service.environment}` + ); + + throw Boom.notFound(); + } + + context.logger.info( + `Deleting config ${service.name}/${service.environment} (${config._id})` + ); + return await deleteConfiguration({ - configurationId, + configurationId: config._id, setup }); } @@ -62,23 +80,6 @@ export const listAgentConfigurationServicesRoute = createRoute(() => ({ } })); -const agentPayloadRt = t.intersection([ - t.partial({ agent_name: t.string }), - t.type({ - service: t.intersection([ - t.partial({ name: t.string }), - t.partial({ environment: t.string }) - ]) - }), - t.type({ - settings: t.intersection([ - t.partial({ transaction_sample_rate: transactionSampleRateRt }), - t.partial({ capture_body: t.string }), - t.partial({ transaction_max_spans: transactionMaxSpansRt }) - ]) - }) -]); - // get environments for service export const listAgentConfigurationEnvironmentsRoute = createRoute(() => ({ path: '/api/apm/settings/agent-configuration/environments', @@ -102,55 +103,47 @@ export const agentConfigurationAgentNameRoute = createRoute(() => ({ const setup = await setupRequest(context, request); const { serviceName } = context.params.query; const agentName = await getAgentNameByService({ serviceName, setup }); - return agentName; + return { agentName }; } })); -export const createAgentConfigurationRoute = createRoute(() => ({ - method: 'POST', - path: '/api/apm/settings/agent-configuration/new', - params: { - body: agentPayloadRt - }, +export const createOrUpdateAgentConfigurationRoute = createRoute(() => ({ + method: 'PUT', + path: '/api/apm/settings/agent-configuration', options: { tags: ['access:apm', 'access:apm_write'] }, + params: { + query: t.partial({ overwrite: jsonRt.pipe(t.boolean) }), + body: agentConfigurationIntakeRt + }, handler: async ({ context, request }) => { const setup = await setupRequest(context, request); - const configuration = context.params.body; + const { body, query } = context.params; - // TODO: Remove logger. Only added temporarily to debug flaky test (https://github.com/elastic/kibana/issues/51764) - context.logger.info( - `Hitting: /api/apm/settings/agent-configuration/new with ${configuration.service.name}/${configuration.service.environment}` - ); - const res = await createOrUpdateConfiguration({ - configuration, + // if the config already exists, it is fetched and updated + // this is to avoid creating two configs with identical service params + const config = await findExactConfiguration({ + service: body.service, setup }); - context.logger.info(`Created agent configuration`); - return res; - } -})); + // if the config exists ?overwrite=true is required + if (config && !query.overwrite) { + throw Boom.badRequest( + `A configuration already exists for "${body.service.name}/${body.service.environment}. Use ?overwrite=true to overwrite the existing configuration.` + ); + } + + context.logger.info( + `${config ? 'Updating' : 'Creating'} config ${body.service.name}/${ + body.service.environment + }` + ); -export const updateAgentConfigurationRoute = createRoute(() => ({ - method: 'PUT', - path: '/api/apm/settings/agent-configuration/{configurationId}', - options: { - tags: ['access:apm', 'access:apm_write'] - }, - params: { - path: t.type({ - configurationId: t.string - }), - body: agentPayloadRt - }, - handler: async ({ context, request }) => { - const setup = await setupRequest(context, request); - const { configurationId } = context.params.path; return await createOrUpdateConfiguration({ - configurationId, - configuration: context.params.body, + configurationId: config?._id, + configurationIntake: body, setup }); } @@ -162,41 +155,33 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ path: '/api/apm/settings/agent-configuration/search', params: { body: t.type({ - service: t.intersection([ - t.type({ name: t.string }), - t.partial({ environment: t.string }) - ]), + service: serviceRt, etag: t.string }) }, handler: async ({ context, request }) => { - const { body } = context.params; - - // TODO: Remove logger. Only added temporarily to debug flaky test (https://github.com/elastic/kibana/issues/51764) - context.logger.info( - `Hitting: /api/apm/settings/agent-configuration/search for ${body.service.name}/${body.service.environment}` - ); + const { service, etag } = context.params.body; const setup = await setupRequest(context, request); const config = await searchConfigurations({ - serviceName: body.service.name, - environment: body.service.environment, + service, setup }); if (!config) { context.logger.info( - `Config was not found for ${body.service.name}/${body.service.environment}` + `Config was not found for ${service.name}/${service.environment}` ); - throw new Boom('Not found', { statusCode: 404 }); + throw Boom.notFound(); } context.logger.info( - `Config was found for ${body.service.name}/${body.service.environment}` + `Config was found for ${service.name}/${service.environment}` ); // update `applied_by_agent` field if etags match - if (body.etag === config._source.etag && !config._source.applied_by_agent) { + // this happens in the background and doesn't block the response + if (etag === config._source.etag && !config._source.applied_by_agent) { markAppliedByAgent({ id: config._id, body: config._source, setup }); } diff --git a/x-pack/test/api_integration/apis/apm/agent_configuration.ts b/x-pack/test/api_integration/apis/apm/agent_configuration.ts index 12e08869fa586..959a0c97acfa3 100644 --- a/x-pack/test/api_integration/apis/apm/agent_configuration.ts +++ b/x-pack/test/api_integration/apis/apm/agent_configuration.ts @@ -5,6 +5,7 @@ */ import expect from '@kbn/expect'; +import { AgentConfigurationIntake } from '../../../../plugins/apm/server/lib/settings/agent_configuration/configuration_types'; import { FtrProviderContext } from '../../ftr_provider_context'; export default function agentConfigurationTests({ getService }: FtrProviderContext) { @@ -18,108 +19,122 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte .set('kbn-xsrf', 'foo'); } - let createdConfigIds: any[] = []; - async function createConfiguration(configuration: any) { + async function createConfiguration(config: AgentConfigurationIntake) { + log.debug('creating configuration', config.service); const res = await supertest - .post(`/api/apm/settings/agent-configuration/new`) - .send(configuration) + .put(`/api/apm/settings/agent-configuration`) + .send(config) .set('kbn-xsrf', 'foo'); - createdConfigIds.push(res.body._id); + throwOnError(res); return res; } - function deleteCreatedConfigurations() { - const promises = Promise.all(createdConfigIds.map(deleteConfiguration)); - return promises; + async function updateConfiguration(config: AgentConfigurationIntake) { + log.debug('updating configuration', config.service); + const res = await supertest + .put(`/api/apm/settings/agent-configuration?overwrite=true`) + .send(config) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; } - function deleteConfiguration(configurationId: string) { - return supertest - .delete(`/api/apm/settings/agent-configuration/${configurationId}`) - .set('kbn-xsrf', 'foo') - .then((response: any) => { - createdConfigIds = createdConfigIds.filter(id => id === configurationId); - return response; - }); + async function deleteConfiguration({ service }: AgentConfigurationIntake) { + log.debug('deleting configuration', service); + const res = await supertest + .delete(`/api/apm/settings/agent-configuration`) + .send({ service }) + .set('kbn-xsrf', 'foo'); + + throwOnError(res); + + return res; + } + + function throwOnError(res: any) { + const { statusCode, req, body } = res; + if (statusCode !== 200) { + throw new Error(` + Endpoint: ${req.method} ${req.path} + Service: ${JSON.stringify(res.request._data.service)} + Status code: ${statusCode} + Response: ${body.message}`); + } } describe('agent configuration', () => { describe('when creating one configuration', () => { - let createdConfigId: string; + const newConfig = { + service: {}, + settings: { transaction_sample_rate: 0.55 }, + }; - const parameters = { + const searchParams = { service: { name: 'myservice', environment: 'development' }, etag: '7312bdcc34999629a3d39df24ed9b2a7553c0c39', }; before(async () => { - log.debug('creating agent configuration'); - - // all / all - const { body } = await createConfiguration({ - service: {}, - settings: { transaction_sample_rate: 0.1 }, - }); - - createdConfigId = body._id; + await createConfiguration(newConfig); }); - it('returns the created configuration', async () => { - const { statusCode, body } = await searchConfigurations(parameters); - + it('can find the created config', async () => { + const { statusCode, body } = await searchConfigurations(searchParams); expect(statusCode).to.equal(200); - expect(body._id).to.equal(createdConfigId); + expect(body._source.service).to.eql({}); + expect(body._source.settings).to.eql({ transaction_sample_rate: 0.55 }); }); - it('succesfully deletes the configuration', async () => { - await deleteConfiguration(createdConfigId); + it('can update the created config', async () => { + await updateConfiguration({ service: {}, settings: { transaction_sample_rate: 0.85 } }); - const { statusCode } = await searchConfigurations(parameters); + const { statusCode, body } = await searchConfigurations(searchParams); + expect(statusCode).to.equal(200); + expect(body._source.service).to.eql({}); + expect(body._source.settings).to.eql({ transaction_sample_rate: 0.85 }); + }); + it('can delete the created config', async () => { + await deleteConfiguration(newConfig); + const { statusCode } = await searchConfigurations(searchParams); expect(statusCode).to.equal(404); }); }); - describe('when creating four configurations', () => { - before(async () => { - log.debug('creating agent configuration'); - - // all / all - await createConfiguration({ + describe('when creating multiple configurations', () => { + const configs = [ + { service: {}, settings: { transaction_sample_rate: 0.1 }, - }); - - // my_service / all - await createConfiguration({ + }, + { service: { name: 'my_service' }, settings: { transaction_sample_rate: 0.2 }, - }); - - // all / production - await createConfiguration({ - service: { environment: 'production' }, + }, + { + service: { name: 'my_service', environment: 'development' }, settings: { transaction_sample_rate: 0.3 }, - }); - - // all / production - await createConfiguration({ - service: { environment: 'development' }, + }, + { + service: { environment: 'production' }, settings: { transaction_sample_rate: 0.4 }, - }); - - // my_service / production - await createConfiguration({ - service: { name: 'my_service', environment: 'development' }, + }, + { + service: { environment: 'development' }, settings: { transaction_sample_rate: 0.5 }, - }); + }, + ]; + + before(async () => { + await Promise.all(configs.map(config => createConfiguration(config))); }); after(async () => { - log.debug('deleting agent configurations'); - await deleteCreatedConfigurations(); + await Promise.all(configs.map(config => deleteConfiguration(config))); }); const agentsRequests = [ @@ -127,20 +142,24 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte service: { name: 'non_existing_service', environment: 'non_existing_env' }, expectedSettings: { transaction_sample_rate: 0.1 }, }, + { + service: { name: 'my_service', environment: 'non_existing_env' }, + expectedSettings: { transaction_sample_rate: 0.2 }, + }, { service: { name: 'my_service', environment: 'production' }, expectedSettings: { transaction_sample_rate: 0.2 }, }, { - service: { name: 'non_existing_service', environment: 'production' }, + service: { name: 'my_service', environment: 'development' }, expectedSettings: { transaction_sample_rate: 0.3 }, }, { - service: { name: 'non_existing_service', environment: 'development' }, + service: { name: 'non_existing_service', environment: 'production' }, expectedSettings: { transaction_sample_rate: 0.4 }, }, { - service: { name: 'my_service', environment: 'development' }, + service: { name: 'non_existing_service', environment: 'development' }, expectedSettings: { transaction_sample_rate: 0.5 }, }, ]; @@ -159,18 +178,18 @@ export default function agentConfigurationTests({ getService }: FtrProviderConte }); describe('when an agent retrieves a configuration', () => { + const config = { + service: { name: 'myservice', environment: 'development' }, + settings: { transaction_sample_rate: 0.9 }, + }; + before(async () => { log.debug('creating agent configuration'); - - await createConfiguration({ - service: { name: 'myservice', environment: 'development' }, - settings: { transaction_sample_rate: 0.9 }, - }); + await createConfiguration(config); }); after(async () => { - log.debug('deleting agent configurations'); - await deleteCreatedConfigurations(); + await deleteConfiguration(config); }); it(`should have 'applied_by_agent=false' on first request`, async () => { diff --git a/x-pack/test/api_integration/apis/apm/feature_controls.ts b/x-pack/test/api_integration/apis/apm/feature_controls.ts index ec2bbca23ddd2..3c5314d0d3261 100644 --- a/x-pack/test/api_integration/apis/apm/feature_controls.ts +++ b/x-pack/test/api_integration/apis/apm/feature_controls.ts @@ -4,8 +4,6 @@ * you may not use this file except in compliance with the Elastic License. */ -/* eslint-disable no-console */ - import expect from '@kbn/expect'; import { FtrProviderContext } from '../../ftr_provider_context'; @@ -14,8 +12,8 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const supertestWithoutAuth = getService('supertestWithoutAuth'); const security = getService('security'); const spaces = getService('spaces'); - const log = getService('log'); const es = getService('legacyEs'); + const log = getService('log'); const start = encodeURIComponent(new Date(Date.now() - 10000).toISOString()); const end = encodeURIComponent(new Date().toISOString()); @@ -33,7 +31,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) interface Endpoint { req: { url: string; - method?: 'get' | 'post' | 'delete'; + method?: 'get' | 'post' | 'delete' | 'put'; body?: any; }; expectForbidden: (result: any) => void; @@ -148,7 +146,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) index: '.apm-agent-configuration', }); - console.warn(JSON.stringify(res, null, 2)); + log.error(JSON.stringify(res, null, 2)); }, }, ]; @@ -196,7 +194,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) const { statusCode, req } = response; if (statusCode !== 200) { - log.debug(`Endpoint: ${req.method} ${req.path} + throw new Error(`Endpoint: ${req.method} ${req.path} Status code: ${statusCode} Response: ${response.body.message}`); } @@ -216,9 +214,9 @@ export default function featureControlsTests({ getService }: FtrProviderContext) spaceId?: string; }) { for (const endpoint of endpoints) { - console.log(`Requesting: ${endpoint.req.url}. Expecting: ${expectation}`); + log.info(`Requesting: ${endpoint.req.url}. Expecting: ${expectation}`); const result = await executeAsUser(endpoint.req, username, password, spaceId); - console.log(`Responded: ${endpoint.req.url}`); + log.info(`Responded: ${endpoint.req.url}`); try { if (expectation === 'forbidden') { @@ -244,26 +242,28 @@ export default function featureControlsTests({ getService }: FtrProviderContext) } describe('apm feature controls', () => { - let res: any; + const config = { + service: { name: 'test-service' }, + settings: { transaction_sample_rate: 0.5 }, + }; before(async () => { - console.log(`Creating agent configuration`); - res = await executeAsAdmin({ - method: 'post', - url: '/api/apm/settings/agent-configuration/new', - body: { - service: { name: 'test-service' }, - settings: { transaction_sample_rate: 0.5 }, - }, + log.info(`Creating agent configuration`); + await executeAsAdmin({ + method: 'put', + url: '/api/apm/settings/agent-configuration', + body: config, }); - console.log(`Agent configuration created`); + log.info(`Agent configuration created`); }); after(async () => { - console.log('deleting agent configuration'); - const configurationId = res.body._id; + log.info('deleting agent configuration'); await executeAsAdmin({ method: 'delete', - url: `/api/apm/settings/agent-configuration/${configurationId}`, + url: `/api/apm/settings/agent-configuration`, + body: { + service: config.service, + }, }); }); diff --git a/x-pack/test/api_integration/apis/apm/index.ts b/x-pack/test/api_integration/apis/apm/index.ts index c49d577537048..6f41f4abfecc3 100644 --- a/x-pack/test/api_integration/apis/apm/index.ts +++ b/x-pack/test/api_integration/apis/apm/index.ts @@ -7,7 +7,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { - describe('APM', () => { + describe('APM specs', () => { loadTestFile(require.resolve('./feature_controls')); loadTestFile(require.resolve('./agent_configuration')); }); diff --git a/x-pack/test/functional/apps/apm/index.ts b/x-pack/test/functional/apps/apm/index.ts index 945af09183f03..bf254f9b9b419 100644 --- a/x-pack/test/functional/apps/apm/index.ts +++ b/x-pack/test/functional/apps/apm/index.ts @@ -6,7 +6,7 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function({ loadTestFile }: FtrProviderContext) { - describe('APM', function() { + describe('APM specs', function() { this.tags('ciGroup6'); loadTestFile(require.resolve('./feature_controls')); });