diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 53eb49e1013b..a8a391995b00 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -408,12 +408,12 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'text', _meta: { description: 'Non-default value of setting.' }, }, - 'apm:enableSignificantTerms': { + 'observability:enableInspectEsQueries': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, - 'observability:enableInspectEsQueries': { - type: 'boolean', + 'observability:maxSuggestions': { + type: 'integer', _meta: { description: 'Non-default value of setting.' }, }, 'banners:placement': { diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index b76ef14e62b8..7ea80ffb77dd 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -34,8 +34,8 @@ export interface UsageStats { 'discover:showMultiFields': boolean; 'discover:maxDocFieldsDisplayed': number; 'securitySolution:rulesTableRefresh': string; - 'apm:enableSignificantTerms': boolean; 'observability:enableInspectEsQueries': boolean; + 'observability:maxSuggestions': number; 'visualize:enableLabs': boolean; 'visualization:heatmap:maxBuckets': number; 'visualization:colorMapping': string; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 2363c0ca103a..c174840ba604 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -7629,14 +7629,14 @@ "description": "Non-default value of setting." } }, - "apm:enableSignificantTerms": { + "observability:enableInspectEsQueries": { "type": "boolean", "_meta": { "description": "Non-default value of setting." } }, - "observability:enableInspectEsQueries": { - "type": "boolean", + "observability:maxSuggestions": { + "type": "integer", "_meta": { "description": "Non-default value of setting." } diff --git a/x-pack/plugins/apm/dev_docs/feature_flags.md b/x-pack/plugins/apm/dev_docs/feature_flags.md deleted file mode 100644 index 9f722dd5eac5..000000000000 --- a/x-pack/plugins/apm/dev_docs/feature_flags.md +++ /dev/null @@ -1,14 +0,0 @@ -## Feature flags - -To set up a flagged feature, add the name of the feature key (`apm:myFeature`) to [commmon/ui_settings_keys.ts](./common/ui_settings_keys.ts) and the feature parameters to [server/ui_settings.ts](./server/ui_settings.ts). - -Test for the feature like: - -```js -import { myFeatureEnabled } from '../ui_settings_keys'; -if (core.uiSettings.get(myFeatureEnabled)) { - doStuff(); -} -``` - -Settings can be managed in Kibana under Stack Management > Advanced Settings > Observability. \ No newline at end of file diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts new file mode 100644 index 000000000000..42da37aa7ef5 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/integration/power_user/rules/error_count.spec.ts @@ -0,0 +1,108 @@ +/* + * 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('Rules', () => { + describe('Error count', () => { + const ruleName = 'Error count threshold'; + const comboBoxInputSelector = + '.euiPopover__panel-isOpen [data-test-subj=comboBoxSearchInput]'; + const confirmModalButtonSelector = + '.euiModal button[data-test-subj=confirmModalConfirmButton]'; + const deleteButtonSelector = + '[data-test-subj=deleteActionHoverButton]:first'; + const editButtonSelector = '[data-test-subj=editActionHoverButton]:first'; + + describe('when created from APM', () => { + describe('when created from Service Inventory', () => { + before(() => { + cy.loginAsPowerUser(); + }); + + it('creates and updates a rule', () => { + // Create a rule in APM + cy.visit('/app/apm/services'); + cy.contains('Alerts and rules').click(); + cy.contains('Error count').click(); + cy.contains('Create threshold rule').click(); + + // Change the environment to "testing" + cy.contains('Environment All').click(); + cy.get(comboBoxInputSelector).type('testing{enter}'); + + // Save, with no actions + cy.contains('button:not(:disabled)', 'Save').click(); + cy.get(confirmModalButtonSelector).click(); + + cy.contains(`Created rule "${ruleName}`); + + // Go to Stack Management + cy.contains('Alerts and rules').click(); + cy.contains('Manage rules').click(); + + // Edit the rule, changing the environment to "All" + cy.get(editButtonSelector).click(); + cy.contains('Environment testing').click(); + cy.get(comboBoxInputSelector).type('All{enter}'); + cy.contains('button:not(:disabled)', 'Save').click(); + + cy.contains(`Updated '${ruleName}'`); + + // Wait for the table to be ready for next edit click + cy.get('.euiBasicTable').not('.euiBasicTable-loading'); + + // Ensure the rule now shows "All" for the environment + cy.get(editButtonSelector).click(); + cy.contains('Environment All'); + cy.contains('button', 'Cancel').click(); + + // Delete the rule + cy.get(deleteButtonSelector).click(); + cy.get(confirmModalButtonSelector).click(); + + // Ensure the table is empty + cy.contains('Create your first rule'); + }); + }); + }); + + describe('when created from Stack management', () => { + before(() => { + cy.loginAsPowerUser(); + }); + + it('creates a rule', () => { + // Go to stack management + cy.visit('/app/management/insightsAndAlerting/triggersActions/rules'); + + // Create a rule + cy.contains('button', 'Create rule').click(); + cy.get('[name=name]').type(ruleName); + cy.contains('.euiFlyout button', ruleName).click(); + + // Change the environment to "testing" + cy.contains('Environment All').click(); + cy.get(comboBoxInputSelector).type('testing{enter}'); + + // Save, with no actions + cy.contains('button:not(:disabled)', 'Save').click(); + cy.get(confirmModalButtonSelector).click(); + + cy.contains(`Created rule "${ruleName}`); + + // Wait for the table to be ready for next delete click + cy.get('.euiBasicTable').not('.euiBasicTable-loading'); + + // Delete the rule + cy.get(deleteButtonSelector).click(); + cy.get(confirmModalButtonSelector).click(); + + // Ensure the table is empty + cy.contains('Create your first rule'); + }); + }); + }); +}); diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/error_count_alert_trigger.stories.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/error_count_alert_trigger.stories.tsx new file mode 100644 index 000000000000..d28d3076b21c --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/error_count_alert_trigger.stories.tsx @@ -0,0 +1,152 @@ +/* + * 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 { Meta, Story } from '@storybook/react'; +import React, { useState } from 'react'; +import { AlertParams, ErrorCountAlertTrigger } from '.'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; +import { AlertMetadata } from '../helper'; + +const KibanaReactContext = createKibanaReactContext({ + notifications: { toasts: { add: () => {} } }, +} as unknown as Partial); + +interface Args { + alertParams: AlertParams; + metadata?: AlertMetadata; +} + +const stories: Meta<{}> = { + title: 'alerting/ErrorCountAlertTrigger', + component: ErrorCountAlertTrigger, + decorators: [ + (StoryComponent) => { + return ( + +
+ +
+
+ ); + }, + ], +}; +export default stories; + +export const CreatingInApmFromInventory: Story = ({ + alertParams, + metadata, +}) => { + const [params, setParams] = useState(alertParams); + + function setAlertParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; +CreatingInApmFromInventory.args = { + alertParams: {}, + metadata: { + end: '2021-09-10T14:14:04.789Z', + environment: ENVIRONMENT_ALL.value, + serviceName: undefined, + start: '2021-09-10T13:59:00.000Z', + }, +}; + +export const CreatingInApmFromService: Story = ({ + alertParams, + metadata, +}) => { + const [params, setParams] = useState(alertParams); + + function setAlertParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; +CreatingInApmFromService.args = { + alertParams: {}, + metadata: { + end: '2021-09-10T14:14:04.789Z', + environment: 'testEnvironment', + serviceName: 'testServiceName', + start: '2021-09-10T13:59:00.000Z', + }, +}; + +export const EditingInStackManagement: Story = ({ + alertParams, + metadata, +}) => { + const [params, setParams] = useState(alertParams); + + function setAlertParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; +EditingInStackManagement.args = { + alertParams: { + environment: 'testEnvironment', + serviceName: 'testServiceName', + threshold: 25, + windowSize: 1, + windowUnit: 'm', + }, + metadata: undefined, +}; + +export const CreatingInStackManagement: Story = ({ + alertParams, + metadata, +}) => { + const [params, setParams] = useState(alertParams); + + function setAlertParams(property: string, value: any) { + setParams({ ...params, [property]: value }); + } + + return ( + {}} + /> + ); +}; +CreatingInStackManagement.args = { + alertParams: {}, + metadata: undefined, +}; diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/error_count_alert_trigger.test.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/error_count_alert_trigger.test.tsx new file mode 100644 index 000000000000..26c62b10e622 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/error_count_alert_trigger.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { render } from '@testing-library/react'; +import React from 'react'; +import * as stories from './error_count_alert_trigger.stories'; +import { composeStories } from '@storybook/testing-react'; + +const { CreatingInApmFromService } = composeStories(stories); + +describe('ErrorCountAlertTrigger', () => { + it('renders', () => { + expect(() => render()).not.toThrowError(); + }); +}); diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx deleted file mode 100644 index b6ee1a61cea5..000000000000 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.stories.tsx +++ /dev/null @@ -1,51 +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 { AlertParams, ErrorCountAlertTrigger } from '.'; -import { CoreStart } from '../../../../../../../src/core/public'; -import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; - -const KibanaReactContext = createKibanaReactContext({ - notifications: { toasts: { add: () => {} } }, -} as unknown as Partial); - -export default { - title: 'alerting/ErrorCountAlertTrigger', - component: ErrorCountAlertTrigger, - decorators: [ - (Story: React.ComponentClass) => ( - -
- -
-
- ), - ], -}; - -export function Example() { - const [params, setParams] = useState({ - serviceName: 'testServiceName', - environment: 'testEnvironment', - threshold: 2, - windowSize: 5, - windowUnit: 'm', - }); - - function setAlertParams(property: string, value: any) { - setParams({ ...params, [property]: value }); - } - - return ( - {}} - /> - ); -} diff --git a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx index dd94cf4b175a..cb7b367fb390 100644 --- a/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/error_count_alert_trigger/index.tsx @@ -7,29 +7,25 @@ import { i18n } from '@kbn/i18n'; import { defaults, omit } from 'lodash'; -import React from 'react'; +import React, { useEffect } from 'react'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { asInteger } from '../../../../common/utils/formatters'; -import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useFetcher } from '../../../hooks/use_fetcher'; +import { createCallApmApi } from '../../../services/rest/createCallApmApi'; import { ChartPreview } from '../chart_preview'; import { EnvironmentField, IsAboveField, ServiceField } from '../fields'; -import { - AlertMetadata, - getIntervalAndTimeRange, - isNewApmRuleFromStackManagement, - TimeUnit, -} from '../helper'; -import { NewAlertEmptyPrompt } from '../new_alert_empty_prompt'; +import { AlertMetadata, getIntervalAndTimeRange, TimeUnit } from '../helper'; import { ServiceAlertTrigger } from '../service_alert_trigger'; export interface AlertParams { - windowSize: number; - windowUnit: string; - threshold: number; - serviceName: string; - environment: string; + windowSize?: number; + windowUnit?: TimeUnit; + threshold?: number; + serviceName?: string; + environment?: string; } interface Props { @@ -40,13 +36,12 @@ interface Props { } export function ErrorCountAlertTrigger(props: Props) { + const { services } = useKibana(); const { alertParams, metadata, setAlertParams, setAlertProperty } = props; - const { environmentOptions } = useEnvironmentsFetcher({ - serviceName: metadata?.serviceName, - start: metadata?.start, - end: metadata?.end, - }); + useEffect(() => { + createCallApmApi(services as CoreStart); + }, [services]); const params = defaults( { ...omit(metadata, ['start', 'end']), ...alertParams }, @@ -87,16 +82,14 @@ export function ErrorCountAlertTrigger(props: Props) { ] ); - if (isNewApmRuleFromStackManagement(alertParams, metadata)) { - return ; - } - const fields = [ - , + setAlertParams('serviceName', value)} + />, setAlertParams('environment', e.target.value)} + onChange={(value) => setAlertParams('environment', value)} />, { describe('Service Field', () => { it('renders with value', () => { - const component = render(); + const component = render( + {}} /> + ); expectTextsInDocument(component, ['foo']); }); it('renders with All when value is not defined', () => { - const component = render(); + const component = render( {}} />); expectTextsInDocument(component, ['All']); }); }); - describe('Transaction Type Field', () => { - it('renders select field when multiple options available', () => { - const options = [ - { text: 'Foo', value: 'foo' }, - { text: 'Bar', value: 'bar' }, - ]; - const { getByText, getByTestId } = render( - - ); - act(() => { - fireEvent.click(getByText('Foo')); - }); - - const selectBar = getByTestId('transactionTypeField'); - expect(selectBar instanceof HTMLSelectElement).toBeTruthy(); - const selectOptions = (selectBar as HTMLSelectElement).options; - expect(selectOptions.length).toEqual(2); - expect( - Object.values(selectOptions).map((option) => option.value) - ).toEqual(['foo', 'bar']); - }); - it('renders read-only field when single option available', () => { - const options = [{ text: 'Bar', value: 'bar' }]; + describe('TransactionTypeField', () => { + it('renders', () => { const component = render( - + {}} /> ); expectTextsInDocument(component, ['Bar']); }); - it('renders read-only All option when no option available', () => { - const component = render(); - expectTextsInDocument(component, ['All']); - }); it('renders current value when available', () => { - const component = render(); + const component = render( + {}} /> + ); expectTextsInDocument(component, ['foo']); }); }); diff --git a/x-pack/plugins/apm/public/components/alerting/fields.tsx b/x-pack/plugins/apm/public/components/alerting/fields.tsx index e48263515236..171953ea522e 100644 --- a/x-pack/plugins/apm/public/components/alerting/fields.tsx +++ b/x-pack/plugins/apm/public/components/alerting/fields.tsx @@ -5,59 +5,96 @@ * 2.0. */ -import { EuiSelect, EuiExpression, EuiFieldNumber } from '@elastic/eui'; -import React from 'react'; +import { EuiComboBoxOptionOption, EuiFieldNumber } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; -import { EuiSelectOption } from '@elastic/eui'; +import React from 'react'; +import { + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../common/elasticsearch_fieldnames'; import { ENVIRONMENT_ALL, getEnvironmentLabel, } from '../../../common/environment_filter_values'; +import { SuggestionsSelect } from '../shared/suggestions_select'; import { PopoverExpression } from './service_alert_trigger/popover_expression'; -const ALL_OPTION = i18n.translate('xpack.apm.alerting.fields.all_option', { +const allOptionText = i18n.translate('xpack.apm.alerting.fields.allOption', { defaultMessage: 'All', }); +const allOption: EuiComboBoxOptionOption = { + label: allOptionText, + value: allOptionText, +}; +const environmentAllOption: EuiComboBoxOptionOption = { + label: ENVIRONMENT_ALL.text, + value: ENVIRONMENT_ALL.value, +}; -export function ServiceField({ value }: { value?: string }) { +export function ServiceField({ + allowAll = true, + currentValue, + onChange, +}: { + allowAll?: boolean; + currentValue?: string; + onChange: (value?: string) => void; +}) { return ( - + > + + ); } export function EnvironmentField({ currentValue, - options, onChange, }: { currentValue: string; - options: EuiSelectOption[]; - onChange: (event: React.ChangeEvent) => void; + onChange: (value?: string) => void; }) { - const title = i18n.translate('xpack.apm.alerting.fields.environment', { - defaultMessage: 'Environment', - }); - if (options.length === 1) { - return ( - - ); - } - return ( - - + ); @@ -65,31 +102,33 @@ export function EnvironmentField({ export function TransactionTypeField({ currentValue, - options, onChange, }: { currentValue?: string; - options?: EuiSelectOption[]; - onChange?: (event: React.ChangeEvent) => void; + onChange: (value?: string) => void; }) { const label = i18n.translate('xpack.apm.alerting.fields.type', { defaultMessage: 'Type', }); - - if (!options || options.length <= 1) { - return ( - - ); - } - return ( - - + ); diff --git a/x-pack/plugins/apm/public/components/alerting/helper.ts b/x-pack/plugins/apm/public/components/alerting/helper.ts index b3dac5c2643d..4032c33fa30b 100644 --- a/x-pack/plugins/apm/public/components/alerting/helper.ts +++ b/x-pack/plugins/apm/public/components/alerting/helper.ts @@ -36,14 +36,3 @@ export function getIntervalAndTimeRange({ end: new Date(end).toISOString(), }; } - -export function isNewApmRuleFromStackManagement( - alertParams: any, - metadata?: AlertMetadata -) { - return ( - alertParams !== undefined && - Object.keys(alertParams).length === 0 && - metadata === undefined - ); -} diff --git a/x-pack/plugins/apm/public/components/alerting/new_alert_empty_prompt.tsx b/x-pack/plugins/apm/public/components/alerting/new_alert_empty_prompt.tsx deleted file mode 100644 index 4777da7871b6..000000000000 --- a/x-pack/plugins/apm/public/components/alerting/new_alert_empty_prompt.tsx +++ /dev/null @@ -1,47 +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. - */ - -/* eslint-disable @elastic/eui/href-or-on-click */ - -import { EuiButton, EuiEmptyPrompt } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React, { MouseEvent } from 'react'; -import { useKibana } from '../../../../../../src/plugins/kibana_react/public'; - -export function NewAlertEmptyPrompt() { - const { services } = useKibana(); - const apmUrl = services.http?.basePath.prepend('/app/apm'); - const navigateToUrl = services.application?.navigateToUrl; - const handleClick = (event: MouseEvent) => { - event.preventDefault(); - if (apmUrl && navigateToUrl) { - navigateToUrl(apmUrl); - } - }; - - return ( - - {i18n.translate('xpack.apm.NewAlertEmptyPrompt.goToApmLinkText', { - defaultMessage: 'Go to APM', - })} - , - ]} - /> - ); -} diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx index dbbb7186de65..5327eb561bfc 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_duration_alert_trigger/index.tsx @@ -8,14 +8,12 @@ import { EuiSelect } from '@elastic/eui'; import { i18n } from '@kbn/i18n'; import { defaults, map, omit } from 'lodash'; -import React from 'react'; +import React, { useEffect } from 'react'; import { CoreStart } from '../../../../../../../src/core/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { getDurationFormatter } from '../../../../common/utils/formatters'; -import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher'; -import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useFetcher } from '../../../hooks/use_fetcher'; import { createCallApmApi } from '../../../services/rest/createCallApmApi'; import { @@ -29,13 +27,7 @@ import { ServiceField, TransactionTypeField, } from '../fields'; -import { - AlertMetadata, - getIntervalAndTimeRange, - isNewApmRuleFromStackManagement, - TimeUnit, -} from '../helper'; -import { NewAlertEmptyPrompt } from '../new_alert_empty_prompt'; +import { AlertMetadata, getIntervalAndTimeRange, TimeUnit } from '../helper'; import { ServiceAlertTrigger } from '../service_alert_trigger'; import { PopoverExpression } from '../service_alert_trigger/popover_expression'; @@ -81,13 +73,9 @@ export function TransactionDurationAlertTrigger(props: Props) { const { services } = useKibana(); const { alertParams, metadata, setAlertParams, setAlertProperty } = props; - createCallApmApi(services as CoreStart); - - const transactionTypes = useServiceTransactionTypesFetcher({ - serviceName: metadata?.serviceName, - start: metadata?.start, - end: metadata?.end, - }); + useEffect(() => { + createCallApmApi(services as CoreStart); + }, [services]); const params = defaults( { @@ -100,16 +88,9 @@ export function TransactionDurationAlertTrigger(props: Props) { windowSize: 5, windowUnit: 'm', environment: ENVIRONMENT_ALL.value, - transactionType: transactionTypes[0], } ); - const { environmentOptions } = useEnvironmentsFetcher({ - serviceName: params.serviceName, - start: metadata?.start, - end: metadata?.end, - }); - const { data } = useFetcher( (callApmApi) => { const { interval, start, end } = getIntervalAndTimeRange({ @@ -160,25 +141,19 @@ export function TransactionDurationAlertTrigger(props: Props) { /> ); - if (isNewApmRuleFromStackManagement(alertParams, metadata)) { - return ; - } - - if (!params.serviceName) { - return null; - } - const fields = [ - , + setAlertParams('serviceName', value)} + />, ({ text: key, value: key }))} - onChange={(e) => setAlertParams('transactionType', e.target.value)} + onChange={(value) => setAlertParams('transactionType', value)} />, setAlertParams('environment', e.target.value)} + onChange={(value) => setAlertParams('environment', value)} />, { + createCallApmApi(services as CoreStart); + }, [services]); const params = defaults( { @@ -68,27 +67,18 @@ export function TransactionDurationAnomalyAlertTrigger(props: Props) { } ); - const { environmentOptions } = useEnvironmentsFetcher({ - serviceName: params.serviceName, - start: metadata?.start, - end: metadata?.end, - }); - - if (isNewApmRuleFromStackManagement(alertParams, metadata)) { - return ; - } - const fields = [ - , + setAlertParams('serviceName', value)} + />, ({ text: key, value: key }))} - onChange={(e) => setAlertParams('transactionType', e.target.value)} + onChange={(value) => setAlertParams('transactionType', value)} />, setAlertParams('environment', e.target.value)} + onChange={(value) => setAlertParams('environment', value)} />, } diff --git a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx index 22e8bef9bc78..3bad7ae15f65 100644 --- a/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx +++ b/x-pack/plugins/apm/public/components/alerting/transaction_error_rate_alert_trigger/index.tsx @@ -6,14 +6,12 @@ */ import { defaults, omit } from 'lodash'; -import React from 'react'; +import React, { useEffect } from 'react'; import { CoreStart } from '../../../../../../../src/core/public'; import { useKibana } from '../../../../../../../src/plugins/kibana_react/public'; import { ForLastExpression } from '../../../../../triggers_actions_ui/public'; import { ENVIRONMENT_ALL } from '../../../../common/environment_filter_values'; import { asPercent } from '../../../../common/utils/formatters'; -import { useServiceTransactionTypesFetcher } from '../../../context/apm_service/use_service_transaction_types_fetcher'; -import { useEnvironmentsFetcher } from '../../../hooks/use_environments_fetcher'; import { useFetcher } from '../../../hooks/use_fetcher'; import { createCallApmApi } from '../../../services/rest/createCallApmApi'; import { ChartPreview } from '../chart_preview'; @@ -23,22 +21,16 @@ import { ServiceField, TransactionTypeField, } from '../fields'; -import { - AlertMetadata, - getIntervalAndTimeRange, - isNewApmRuleFromStackManagement, - TimeUnit, -} from '../helper'; -import { NewAlertEmptyPrompt } from '../new_alert_empty_prompt'; +import { AlertMetadata, getIntervalAndTimeRange, TimeUnit } from '../helper'; import { ServiceAlertTrigger } from '../service_alert_trigger'; interface AlertParams { - windowSize: number; - windowUnit: string; - threshold: number; - serviceName: string; - transactionType: string; - environment: string; + windowSize?: number; + windowUnit?: string; + threshold?: number; + serviceName?: string; + transactionType?: string; + environment?: string; } interface Props { @@ -52,12 +44,9 @@ export function TransactionErrorRateAlertTrigger(props: Props) { const { services } = useKibana(); const { alertParams, metadata, setAlertParams, setAlertProperty } = props; - createCallApmApi(services as CoreStart); - const transactionTypes = useServiceTransactionTypesFetcher({ - serviceName: metadata?.serviceName, - start: metadata?.start, - end: metadata?.end, - }); + useEffect(() => { + createCallApmApi(services as CoreStart); + }, [services]); const params = defaults( { ...omit(metadata, ['start', 'end']), ...alertParams }, @@ -69,12 +58,6 @@ export function TransactionErrorRateAlertTrigger(props: Props) { } ); - const { environmentOptions } = useEnvironmentsFetcher({ - serviceName: params.serviceName, - start: metadata?.start, - end: metadata?.end, - }); - const thresholdAsPercent = (params.threshold ?? 0) / 100; const { data } = useFetcher( @@ -108,21 +91,18 @@ export function TransactionErrorRateAlertTrigger(props: Props) { ] ); - if (isNewApmRuleFromStackManagement(alertParams, metadata)) { - return ; - } - const fields = [ - , + setAlertParams('serviceName', value)} + />, ({ text: key, value: key }))} - onChange={(e) => setAlertParams('transactionType', e.target.value)} + onChange={(value) => setAlertParams('transactionType', value)} />, setAlertParams('environment', e.target.value)} + onChange={(value) => setAlertParams('environment', value)} />, ; + customOptionText: string; + defaultValue?: string; + field: string; + onChange: (value?: string) => void; + placeholder: string; +} + +export function SuggestionsSelect({ + allOption, + customOptionText, + defaultValue, + field, + onChange, + placeholder, +}: SuggestionsSelectProps) { + const allowAll = !!allOption; + let defaultOption: EuiComboBoxOptionOption | undefined; + + if (allowAll && !defaultValue) { + defaultOption = allOption; + } + if (defaultValue) { + defaultOption = { label: defaultValue, value: defaultValue }; + } + const [selectedOptions, setSelectedOptions] = useState( + defaultOption ? [defaultOption] : [] + ); + + const [searchValue, setSearchValue] = useState(''); + + const { data, status } = useFetcher( + (callApmApi) => { + return callApmApi({ + endpoint: 'GET /internal/apm/suggestions', + params: { + query: { field, string: searchValue }, + }, + }); + }, + [field, searchValue], + { preservePreviousData: false } + ); + + const handleChange = useCallback( + (changedOptions: Array>) => { + setSelectedOptions(changedOptions); + if (changedOptions.length === 1) { + onChange( + changedOptions[0].value + ? changedOptions[0].value.trim() + : changedOptions[0].value + ); + } + }, + [onChange] + ); + + const handleCreateOption = useCallback( + (value: string) => { + handleChange([{ label: value, value }]); + }, + [handleChange] + ); + + const terms = data?.terms ?? []; + + const options: Array> = [ + ...(allOption && + (searchValue === '' || + searchValue.toLowerCase() === allOption.label.toLowerCase()) + ? [allOption] + : []), + ...terms.map((name) => { + return { label: name, value: name }; + }), + ]; + + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.stories.tsx b/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.stories.tsx new file mode 100644 index 000000000000..d83ca13a9bb2 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.stories.tsx @@ -0,0 +1,77 @@ +/* + * 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 { EuiComboBoxOptionOption } from '@elastic/eui'; +import { Meta, Story } from '@storybook/react'; +import React from 'react'; +import { CoreStart } from '../../../../../../../src/core/public'; +import { createKibanaReactContext } from '../../../../../../../src/plugins/kibana_react/public'; +import { createCallApmApi } from '../../../services/rest/createCallApmApi'; +import { SuggestionsSelect } from './'; + +interface Args { + allOption: EuiComboBoxOptionOption; + customOptionText: string; + field: string; + placeholder: string; + terms: string[]; +} + +const stories: Meta = { + title: 'shared/SuggestionsSelect', + component: SuggestionsSelect, + decorators: [ + (StoryComponent, { args }) => { + const { terms } = args; + + const coreMock = { + http: { + get: () => { + return { terms }; + }, + }, + notifications: { toasts: { add: () => {} } }, + uiSettings: { get: () => true }, + } as unknown as CoreStart; + + const KibanaReactContext = createKibanaReactContext(coreMock); + + createCallApmApi(coreMock); + + return ( + + + + ); + }, + ], +}; +export default stories; + +export const Example: Story = ({ + allOption, + customOptionText, + field, + placeholder, +}) => { + return ( + {}} + placeholder={placeholder} + /> + ); +}; +Example.args = { + allOption: { label: 'All the things', value: 'ALL_THE_THINGS' }, + terms: ['thing1', 'thing2'], + customOptionText: 'Add {searchValue} as a new thing', + field: 'test.field', + placeholder: 'Select thing', +}; diff --git a/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.test.tsx b/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.test.tsx new file mode 100644 index 000000000000..b1fce1c439f3 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/suggestions_select/suggestions_select.test.tsx @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { composeStories } from '@storybook/testing-react'; +import { render } from '@testing-library/react'; +import React from 'react'; +import * as stories from './suggestions_select.stories'; + +const { Example } = composeStories(stories); + +describe('SuggestionsSelect', () => { + it('renders', () => { + expect(() => render()).not.toThrowError(); + }); +}); diff --git a/x-pack/plugins/apm/server/index.ts b/x-pack/plugins/apm/server/index.ts index 2c21ff17f779..b7002ff7cbe7 100644 --- a/x-pack/plugins/apm/server/index.ts +++ b/x-pack/plugins/apm/server/index.ts @@ -7,12 +7,13 @@ import { schema, TypeOf } from '@kbn/config-schema'; import { - PluginInitializerContext, PluginConfigDescriptor, + PluginInitializerContext, } from 'src/core/server'; import { APMOSSConfig } from 'src/plugins/apm_oss/server'; -import { APMPlugin } from './plugin'; +import { maxSuggestions } from '../../observability/common'; import { SearchAggregatedTransactionSetting } from '../common/aggregated_transactions'; +import { APMPlugin } from './plugin'; const configSchema = schema.object({ enabled: schema.boolean({ defaultValue: true }), @@ -40,8 +41,6 @@ const configSchema = schema.object({ ), telemetryCollectionEnabled: schema.boolean({ defaultValue: true }), metricsInterval: schema.number({ defaultValue: 30 }), - maxServiceEnvironments: schema.number({ defaultValue: 100 }), - maxServiceSelection: schema.number({ defaultValue: 50 }), profilingEnabled: schema.boolean({ defaultValue: false }), agent: schema.object({ migrations: schema.object({ @@ -52,7 +51,17 @@ const configSchema = schema.object({ // plugin config export const config: PluginConfigDescriptor = { - deprecations: ({ deprecate }) => [deprecate('enabled', '8.0.0')], + deprecations: ({ deprecate, renameFromRoot }) => [ + deprecate('enabled', '8.0.0'), + renameFromRoot( + 'xpack.apm.maxServiceEnvironments', + `uiSettings.overrides[${maxSuggestions}]` + ), + renameFromRoot( + 'xpack.apm.maxServiceSelections', + `uiSettings.overrides[${maxSuggestions}]` + ), + ], exposeToBrowser: { serviceMapEnabled: true, ui: true, @@ -91,8 +100,6 @@ export function mergeConfigs( 'xpack.apm.serviceMapMaxTracesPerRequest': apmConfig.serviceMapMaxTracesPerRequest, 'xpack.apm.ui.enabled': apmConfig.ui.enabled, - 'xpack.apm.maxServiceEnvironments': apmConfig.maxServiceEnvironments, - 'xpack.apm.maxServiceSelection': apmConfig.maxServiceSelection, 'xpack.apm.ui.maxTraceItems': apmConfig.ui.maxTraceItems, 'xpack.apm.ui.transactionGroupBucketSize': apmConfig.ui.transactionGroupBucketSize, diff --git a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap index da2309afa07c..61f5b575a520 100644 --- a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_all_environments.test.ts.snap @@ -15,7 +15,7 @@ Object { "terms": Object { "field": "service.environment", "missing": undefined, - "size": 100, + "size": 50, }, }, }, @@ -50,7 +50,7 @@ Object { "terms": Object { "field": "service.environment", "missing": "ENVIRONMENT_NOT_DEFINED", - "size": 100, + "size": 50, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap index 47c5e9033eb0..d35aeda9e868 100644 --- a/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap +++ b/x-pack/plugins/apm/server/lib/environments/__snapshots__/get_environments.test.ts.snap @@ -15,7 +15,7 @@ Object { "terms": Object { "field": "service.environment", "missing": "ENVIRONMENT_NOT_DEFINED", - "size": 100, + "size": 50, }, }, }, @@ -59,7 +59,7 @@ Object { "terms": Object { "field": "service.environment", "missing": "ENVIRONMENT_NOT_DEFINED", - "size": 100, + "size": 50, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts index 2c1772cc5800..0f27839d9404 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.test.ts @@ -21,9 +21,10 @@ describe('getAllEnvironments', () => { it('fetches all environments', async () => { mock = await inspectSearchParams((setup) => getAllEnvironments({ - serviceName: 'test', searchAggregatedTransactions: false, + serviceName: 'test', setup, + size: 50, }) ); @@ -33,10 +34,11 @@ describe('getAllEnvironments', () => { it('fetches all environments with includeMissing', async () => { mock = await inspectSearchParams((setup) => getAllEnvironments({ + includeMissing: true, + searchAggregatedTransactions: false, serviceName: 'test', setup, - searchAggregatedTransactions: false, - includeMissing: true, + size: 50, }) ); diff --git a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts index f6a198797485..1ddc3f7ed888 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_all_environments.ts @@ -19,22 +19,23 @@ import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregate * It's used in places where we get the list of all possible environments. */ export async function getAllEnvironments({ + includeMissing = false, + searchAggregatedTransactions, serviceName, setup, - searchAggregatedTransactions, - includeMissing = false, + size, }: { + includeMissing?: boolean; + searchAggregatedTransactions: boolean; serviceName?: string; setup: Setup; - searchAggregatedTransactions: boolean; - includeMissing?: boolean; + size: number; }) { const operationName = serviceName ? 'get_all_environments_for_service' : 'get_all_environments_for_all_services'; - const { apmEventClient, config } = setup; - const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; + const { apmEventClient } = setup; // omit filter for service.name if "All" option is selected const serviceNameFilter = serviceName @@ -65,7 +66,7 @@ export async function getAllEnvironments({ environments: { terms: { field: SERVICE_ENVIRONMENT, - size: maxServiceEnvironments, + size, ...(!serviceName ? { min_doc_count: 0 } : {}), missing: includeMissing ? ENVIRONMENT_NOT_DEFINED.value : undefined, }, diff --git a/x-pack/plugins/apm/server/lib/environments/get_environments.test.ts b/x-pack/plugins/apm/server/lib/environments/get_environments.test.ts index 26c4ee85e7d8..472fd9d226e3 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_environments.test.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_environments.test.ts @@ -24,6 +24,7 @@ describe('getEnvironments', () => { setup, serviceName: 'foo', searchAggregatedTransactions: false, + size: 50, start: 0, end: 50000, }) @@ -37,6 +38,7 @@ describe('getEnvironments', () => { getEnvironments({ setup, searchAggregatedTransactions: false, + size: 50, start: 0, end: 50000, }) diff --git a/x-pack/plugins/apm/server/lib/environments/get_environments.ts b/x-pack/plugins/apm/server/lib/environments/get_environments.ts index d87cdbe85e73..08f6f089e8d0 100644 --- a/x-pack/plugins/apm/server/lib/environments/get_environments.ts +++ b/x-pack/plugins/apm/server/lib/environments/get_environments.ts @@ -20,15 +20,17 @@ import { Setup } from '../helpers/setup_request'; * filtered by range. */ export async function getEnvironments({ - setup, - serviceName, searchAggregatedTransactions, + serviceName, + setup, + size, start, end, }: { setup: Setup; serviceName?: string; searchAggregatedTransactions: boolean; + size: number; start: number; end: number; }) { @@ -36,7 +38,7 @@ export async function getEnvironments({ ? 'get_environments_for_service' : 'get_environments'; - const { apmEventClient, config } = setup; + const { apmEventClient } = setup; const filter = rangeQuery(start, end); @@ -46,8 +48,6 @@ export async function getEnvironments({ }); } - const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; - const params = { apm: { events: [ @@ -70,7 +70,7 @@ export async function getEnvironments({ terms: { field: SERVICE_ENVIRONMENT, missing: ENVIRONMENT_NOT_DEFINED.value, - size: maxServiceEnvironments, + size, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts index 3fed3c92c440..b2b2a0b869c8 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/index.ts @@ -5,6 +5,10 @@ * 2.0. */ +import type { + TermsEnumRequest, + TermsEnumResponse, +} from '@elastic/elasticsearch/api/types'; import { ValuesType } from 'utility-types'; import { withApmSpan } from '../../../../utils/with_apm_span'; import { Profile } from '../../../../../typings/es_schemas/ui/profile'; @@ -39,6 +43,10 @@ export type APMEventESSearchRequest = Omit & { }; }; +export type APMEventESTermsEnumRequest = Omit & { + apm: { events: ProcessorEvent[] }; +}; + // These keys shoul all be `ProcessorEvent.x`, but until TypeScript 4.2 we're inlining them here. // See https://github.com/microsoft/TypeScript/issues/37888 type TypeOfProcessorEvent = { @@ -124,5 +132,44 @@ export function createApmEventClient({ requestParams: searchParams, }); }, + + async termsEnum( + operationName: string, + params: APMEventESTermsEnumRequest + ): Promise { + const requestType = 'terms_enum'; + const { index } = unpackProcessorEvents(params, indices); + + return callAsyncWithDebug({ + cb: () => { + const { apm, ...rest } = params; + const termsEnumPromise = withApmSpan(operationName, () => + cancelEsRequestOnAbort( + esClient.termsEnum({ + index: Array.isArray(index) ? index.join(',') : index, + ...rest, + }), + request + ) + ); + + return unwrapEsResponse(termsEnumPromise); + }, + getDebugMessage: () => ({ + body: getDebugBody({ + params, + requestType, + operationName, + }), + title: getDebugTitle(request), + }), + isCalledWithInternalUser: false, + debug, + request, + requestType, + operationName, + requestParams: params, + }); + }, }; } diff --git a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts index 8732ba81f9ae..47a2b3fe7e5c 100644 --- a/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts +++ b/x-pack/plugins/apm/server/lib/helpers/create_es_client/create_apm_event_client/unpack_processor_events.ts @@ -12,7 +12,7 @@ import { ESSearchRequest, ESFilter, } from '../../../../../../../../src/core/types/elasticsearch'; -import { APMEventESSearchRequest } from '.'; +import { APMEventESSearchRequest, APMEventESTermsEnumRequest } from '.'; import { ApmIndicesConfig, ApmIndicesName, @@ -28,7 +28,7 @@ const processorEventIndexMap: Record = { }; export function unpackProcessorEvents( - request: APMEventESSearchRequest, + request: APMEventESSearchRequest | APMEventESTermsEnumRequest, indices: ApmIndicesConfig ) { const { apm, ...params } = request; 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 18ef3f44331d..b6b4f2208d04 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 @@ -99,7 +99,7 @@ Object { "terms": Object { "field": "service.environment", "missing": undefined, - "size": 100, + "size": 50, }, }, }, @@ -127,7 +127,7 @@ Object { "terms": Object { "field": "service.environment", "missing": "ALL_OPTION_VALUE", - "size": 100, + "size": 50, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts index 124a373d3cf0..4fd351f8708a 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts @@ -15,12 +15,13 @@ import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration/all_ export async function getExistingEnvironmentsForService({ serviceName, setup, + size, }: { serviceName: string | undefined; setup: Setup; + size: number; }) { - const { internalClient, indices, config } = setup; - const maxServiceEnvironments = config['xpack.apm.maxServiceEnvironments']; + const { internalClient, indices } = setup; const bool = serviceName ? { filter: [{ term: { [SERVICE_NAME]: serviceName } }] } @@ -36,7 +37,7 @@ export async function getExistingEnvironmentsForService({ terms: { field: SERVICE_ENVIRONMENT, missing: ALL_OPTION_VALUE, - size: maxServiceEnvironments, + size, }, }, }, diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts index 0ab56ac37270..dadb29d156e0 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts @@ -20,15 +20,22 @@ export async function getEnvironments({ serviceName, setup, searchAggregatedTransactions, + size, }: { serviceName: string | undefined; setup: Setup; searchAggregatedTransactions: boolean; + size: number; }) { return withApmSpan('get_environments_for_agent_configuration', async () => { const [allEnvironments, existingEnvironments] = await Promise.all([ - getAllEnvironments({ serviceName, setup, searchAggregatedTransactions }), - getExistingEnvironmentsForService({ serviceName, setup }), + getAllEnvironments({ + searchAggregatedTransactions, + serviceName, + setup, + size, + }), + getExistingEnvironmentsForService({ serviceName, setup, size }), ]); return [ALL_OPTION_VALUE, ...allEnvironments].map((environment) => { diff --git a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index 0786bc6bc277..282eacbec66d 100644 --- a/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -15,15 +15,17 @@ import { getProcessorEventForAggregatedTransactions } from '../../helpers/aggreg export type AgentConfigurationServicesAPIResponse = PromiseReturnType< typeof getServiceNames >; + export async function getServiceNames({ setup, searchAggregatedTransactions, + size, }: { setup: Setup; searchAggregatedTransactions: boolean; + size: number; }) { - const { apmEventClient, config } = setup; - const maxServiceSelection = config['xpack.apm.maxServiceSelection']; + const { apmEventClient } = setup; const params = { apm: { @@ -42,8 +44,8 @@ export async function getServiceNames({ services: { terms: { field: SERVICE_NAME, - size: maxServiceSelection, min_doc_count: 0, + size, }, }, }, 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 17f51e8826d9..4ffc8ed98184 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 @@ -27,9 +27,10 @@ describe('agent configuration queries', () => { it('fetches all environments', async () => { mock = await inspectSearchParams((setup) => getAllEnvironments({ + searchAggregatedTransactions: false, serviceName: 'foo', setup, - searchAggregatedTransactions: false, + size: 50, }) ); @@ -43,6 +44,7 @@ describe('agent configuration queries', () => { getExistingEnvironmentsForService({ serviceName: 'foo', setup, + size: 50, }) ); @@ -56,6 +58,7 @@ describe('agent configuration queries', () => { getServiceNames({ setup, searchAggregatedTransactions: false, + size: 50, }) ); diff --git a/x-pack/plugins/apm/server/lib/suggestions/get_suggestions.ts b/x-pack/plugins/apm/server/lib/suggestions/get_suggestions.ts new file mode 100644 index 000000000000..acd44366ef4c --- /dev/null +++ b/x-pack/plugins/apm/server/lib/suggestions/get_suggestions.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { ProcessorEvent } from '../../../common/processor_event'; +import { getProcessorEventForAggregatedTransactions } from '../helpers/aggregated_transactions'; +import { Setup } from '../helpers/setup_request'; + +export async function getSuggestions({ + field, + searchAggregatedTransactions, + setup, + size, + string, +}: { + field: string; + searchAggregatedTransactions: boolean; + setup: Setup; + size: number; + string: string; +}) { + const { apmEventClient } = setup; + + const response = await apmEventClient.termsEnum('get_suggestions', { + apm: { + events: [ + getProcessorEventForAggregatedTransactions( + searchAggregatedTransactions + ), + ProcessorEvent.error, + ProcessorEvent.metric, + ], + }, + body: { + case_insensitive: true, + field, + size, + string, + }, + }); + + return { terms: response.terms }; +} diff --git a/x-pack/plugins/apm/server/routes/environments.ts b/x-pack/plugins/apm/server/routes/environments.ts index 95ebb2bf1343..4b00320009e2 100644 --- a/x-pack/plugins/apm/server/routes/environments.ts +++ b/x-pack/plugins/apm/server/routes/environments.ts @@ -6,6 +6,7 @@ */ import * as t from 'io-ts'; +import { maxSuggestions } from '../../../observability/common'; import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; import { setupRequest } from '../lib/helpers/setup_request'; import { getEnvironments } from '../lib/environments/get_environments'; @@ -26,7 +27,7 @@ const environmentsRoute = createApmServerRoute({ options: { tags: ['access:apm'] }, handler: async (resources) => { const setup = await setupRequest(resources); - const { params } = resources; + const { context, params } = resources; const { serviceName, start, end } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions({ apmEventClient: setup.apmEventClient, @@ -35,11 +36,14 @@ const environmentsRoute = createApmServerRoute({ end, kuery: '', }); - + const size = await context.core.uiSettings.client.get( + maxSuggestions + ); const environments = await getEnvironments({ setup, serviceName, searchAggregatedTransactions, + size, start, end, }); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index 09756e30d968..7aa520dd5b8a 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -33,6 +33,7 @@ import { traceRouteRepository } from './traces'; import { transactionRouteRepository } from './transactions'; import { APMRouteHandlerResources } from './typings'; import { historicalDataRouteRepository } from './historical_data'; +import { suggestionsRouteRepository } from './suggestions'; const getTypedGlobalApmServerRouteRepository = () => { const repository = createApmServerRouteRepository() @@ -45,6 +46,7 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(serviceMapRouteRepository) .merge(serviceNodeRouteRepository) .merge(serviceRouteRepository) + .merge(suggestionsRouteRepository) .merge(traceRouteRepository) .merge(transactionRouteRepository) .merge(alertsChartPreviewRouteRepository) 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 589ae41bfdc6..a904e5e03b53 100644 --- a/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts +++ b/x-pack/plugins/apm/server/routes/settings/agent_configuration.ts @@ -8,6 +8,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; import { toBooleanRt } from '@kbn/io-ts-utils'; +import { maxSuggestions } from '../../../../observability/common'; 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'; @@ -251,9 +252,13 @@ const listAgentConfigurationServicesRoute = createApmServerRoute({ start, end, }); + const size = await resources.context.core.uiSettings.client.get( + maxSuggestions + ); const serviceNames = await getServiceNames({ - setup, searchAggregatedTransactions, + setup, + size, }); return { serviceNames }; @@ -269,7 +274,7 @@ const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({ options: { tags: ['access:apm'] }, handler: async (resources) => { const setup = await setupRequest(resources); - const { params } = resources; + const { context, params } = resources; const { serviceName, start, end } = params.query; const searchAggregatedTransactions = await getSearchAggregatedTransactions({ @@ -279,11 +284,14 @@ const listAgentConfigurationEnvironmentsRoute = createApmServerRoute({ start, end, }); - + const size = await context.core.uiSettings.client.get( + maxSuggestions + ); const environments = await getEnvironments({ serviceName, setup, searchAggregatedTransactions, + size, }); return { environments }; diff --git a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts index d15f23241181..c7e45eb8c32e 100644 --- a/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts +++ b/x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts @@ -7,6 +7,7 @@ import * as t from 'io-ts'; import Boom from '@hapi/boom'; +import { maxSuggestions } from '../../../../observability/common'; import { isActivePlatinumLicense } from '../../../common/license_check'; import { ML_ERRORS } from '../../../common/anomaly_detection'; import { createApmServerRoute } from '../create_apm_server_route'; @@ -92,11 +93,14 @@ const anomalyDetectionEnvironmentsRoute = createApmServerRoute({ config: setup.config, kuery: '', }); - + const size = await resources.context.core.uiSettings.client.get( + maxSuggestions + ); const environments = await getAllEnvironments({ - setup, - searchAggregatedTransactions, includeMissing: true, + searchAggregatedTransactions, + setup, + size, }); return { environments }; diff --git a/x-pack/plugins/apm/server/routes/suggestions.ts b/x-pack/plugins/apm/server/routes/suggestions.ts new file mode 100644 index 000000000000..8b82601650a4 --- /dev/null +++ b/x-pack/plugins/apm/server/routes/suggestions.ts @@ -0,0 +1,47 @@ +/* + * 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 * as t from 'io-ts'; +import { maxSuggestions } from '../../../observability/common'; +import { getSuggestions } from '../lib/suggestions/get_suggestions'; +import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions'; +import { setupRequest } from '../lib/helpers/setup_request'; +import { createApmServerRoute } from './create_apm_server_route'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; + +const suggestionsRoute = createApmServerRoute({ + endpoint: 'GET /internal/apm/suggestions', + params: t.partial({ + query: t.type({ field: t.string, string: t.string }), + }), + options: { tags: ['access:apm'] }, + handler: async (resources) => { + const setup = await setupRequest(resources); + const { context, params } = resources; + const { field, string } = params.query; + const searchAggregatedTransactions = await getSearchAggregatedTransactions({ + apmEventClient: setup.apmEventClient, + config: setup.config, + kuery: '', + }); + const size = await context.core.uiSettings.client.get( + maxSuggestions + ); + const suggestions = await getSuggestions({ + field, + searchAggregatedTransactions, + setup, + size, + string, + }); + + return suggestions; + }, +}); + +export const suggestionsRouteRepository = + createApmServerRouteRepository().add(suggestionsRoute); diff --git a/x-pack/plugins/apm/server/utils/test_helpers.tsx b/x-pack/plugins/apm/server/utils/test_helpers.tsx index 171af609c256..7b6b549e07c8 100644 --- a/x-pack/plugins/apm/server/utils/test_helpers.tsx +++ b/x-pack/plugins/apm/server/utils/test_helpers.tsx @@ -79,12 +79,6 @@ export async function inspectSearchParams( case 'xpack.apm.metricsInterval': return 30; - - case 'xpack.apm.maxServiceEnvironments': - return 100; - - case 'xpack.apm.maxServiceSelection': - return 50; } }, } diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index baa3d92ffeeb..bfb2eedf6deb 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -6,6 +6,7 @@ */ export type { AsDuration, AsPercent } from './utils/formatters'; +export { enableInspectEsQueries, maxSuggestions } from './ui_settings_keys'; export const casesFeatureId = 'observabilityCases'; diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index bd5364748825..69eb50732871 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -6,3 +6,4 @@ */ export const enableInspectEsQueries = 'observability:enableInspectEsQueries'; +export const maxSuggestions = 'observability:maxSuggestions'; diff --git a/x-pack/plugins/observability/dev_docs/feature_flags.md b/x-pack/plugins/observability/dev_docs/feature_flags.md new file mode 100644 index 000000000000..56c0e4681382 --- /dev/null +++ b/x-pack/plugins/observability/dev_docs/feature_flags.md @@ -0,0 +1,16 @@ +## Feature flags and advanced settings + +To set up a flagged feature or other advanced setting, add the name of the feature key (`observability:myFeature`) to [common/ui_settings_keys.ts](../common/ui_settings_keys.ts) and the feature parameters to [server/ui_settings.ts](../server/ui_settings.ts). + +Test for the feature like: + +```js +import { myFeatureEnabled } from '../ui_settings_keys'; +if (core.uiSettings.get(myFeatureEnabled)) { + doStuff(); +} +``` + +In order for telemetry to be collected, the keys and types need to be added in [src/plugins/kibana_usage_collection/server/collectors/management/schema.ts](../../../../src/plugins/kibana_usage_collection/server/collectors/management/schema.ts) and [src/plugins/kibana_usage_collection/server/collectors/management/types.ts](../../../../src/plugins/kibana_usage_collection/server/collectors/management/types.ts). + +Settings can be managed in Kibana under Stack Management > Advanced Settings > Observability. diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index a8dab0fef8f5..0bd9f99b5b14 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -9,16 +9,16 @@ import { schema } from '@kbn/config-schema'; import { i18n } from '@kbn/i18n'; import { UiSettingsParams } from '../../../../src/core/types'; import { observabilityFeatureId } from '../common'; -import { enableInspectEsQueries } from '../common/ui_settings_keys'; +import { enableInspectEsQueries, maxSuggestions } from '../common/ui_settings_keys'; /** * uiSettings definitions for Observability. */ -export const uiSettings: Record> = { +export const uiSettings: Record> = { [enableInspectEsQueries]: { category: [observabilityFeatureId], name: i18n.translate('xpack.observability.enableInspectEsQueriesExperimentName', { - defaultMessage: 'inspect ES queries', + defaultMessage: 'Inspect ES queries', }), value: false, description: i18n.translate('xpack.observability.enableInspectEsQueriesExperimentDescription', { @@ -26,4 +26,15 @@ export const uiSettings: Record> = { }), schema: schema.boolean(), }, + [maxSuggestions]: { + category: [observabilityFeatureId], + name: i18n.translate('xpack.observability.maxSuggestionsUiSettingName', { + defaultMessage: 'Maximum suggestions', + }), + value: 100, + description: i18n.translate('xpack.observability.maxSuggestionsUiSettingDescription', { + defaultMessage: 'Maximum number of suggestions fetched in autocomplete selection boxes.', + }), + schema: schema.number(), + }, }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index b8d4d8b9f805..ac8616ab2dd8 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6193,7 +6193,6 @@ "xpack.apm.alertAnnotationCriticalTitle": "重大アラート", "xpack.apm.alertAnnotationNoSeverityTitle": "アラート", "xpack.apm.alertAnnotationWarningTitle": "警告アラート", - "xpack.apm.alerting.fields.all_option": "すべて", "xpack.apm.alerting.fields.environment": "環境", "xpack.apm.alerting.fields.service": "サービス", "xpack.apm.alerting.fields.type": "型", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 17c6284ec9c6..b844315c8b6e 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6243,7 +6243,6 @@ "xpack.apm.alertAnnotationCriticalTitle": "紧急告警", "xpack.apm.alertAnnotationNoSeverityTitle": "告警", "xpack.apm.alertAnnotationWarningTitle": "警告告警", - "xpack.apm.alerting.fields.all_option": "全部", "xpack.apm.alerting.fields.environment": "环境", "xpack.apm.alerting.fields.service": "服务", "xpack.apm.alerting.fields.type": "类型", diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index 5ea5ad78d947..d402a74287f9 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -137,6 +137,11 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./settings/custom_link')); }); + // suggestions + describe('suggestions', function () { + loadTestFile(require.resolve('./suggestions/suggestions')); + }); + // traces describe('traces/top_traces', function () { loadTestFile(require.resolve('./traces/top_traces')); diff --git a/x-pack/test/apm_api_integration/tests/suggestions/suggestions.ts b/x-pack/test/apm_api_integration/tests/suggestions/suggestions.ts new file mode 100644 index 000000000000..d551aec632fa --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/suggestions/suggestions.ts @@ -0,0 +1,143 @@ +/* + * 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 { + SERVICE_ENVIRONMENT, + SERVICE_NAME, + TRANSACTION_TYPE, +} from '../../../../plugins/apm/common/elasticsearch_fieldnames'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function suggestionsTests({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const archiveName = 'apm_8.0.0'; + + registry.when( + 'suggestions when data is loaded', + { config: 'basic', archives: [archiveName] }, + () => { + describe('with environment', () => { + describe('with an empty string parameter', () => { + it('returns all environments', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { field: SERVICE_ENVIRONMENT, string: '' } }, + }); + + expectSnapshot(body).toMatchInline(` + Object { + "terms": Array [ + "production", + "testing", + ], + } + `); + }); + }); + + describe('with a string parameter', () => { + it('returns items matching the string parameter', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { field: SERVICE_ENVIRONMENT, string: 'pr' } }, + }); + + expectSnapshot(body).toMatchInline(` + Object { + "terms": Array [ + "production", + ], + } + `); + }); + }); + }); + + describe('with service name', () => { + describe('with an empty string parameter', () => { + it('returns all services', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { field: SERVICE_NAME, string: '' } }, + }); + + expectSnapshot(body).toMatchInline(` + Object { + "terms": Array [ + "auditbeat", + "opbeans-dotnet", + "opbeans-go", + "opbeans-java", + "opbeans-node", + "opbeans-python", + "opbeans-ruby", + "opbeans-rum", + ], + } + `); + }); + }); + + describe('with a string parameter', () => { + it('returns items matching the string parameter', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { field: SERVICE_NAME, string: 'aud' } }, + }); + + expectSnapshot(body).toMatchInline(` + Object { + "terms": Array [ + "auditbeat", + ], + } + `); + }); + }); + }); + + describe('with transaction type', () => { + describe('with an empty string parameter', () => { + it('returns all transaction types', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { field: TRANSACTION_TYPE, string: '' } }, + }); + + expectSnapshot(body).toMatchInline(` + Object { + "terms": Array [ + "Worker", + "celery", + "page-load", + "request", + ], + } + `); + }); + }); + + describe('with a string parameter', () => { + it('returns items matching the string parameter', async () => { + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /internal/apm/suggestions', + params: { query: { field: TRANSACTION_TYPE, string: 'w' } }, + }); + + expectSnapshot(body).toMatchInline(` + Object { + "terms": Array [ + "Worker", + ], + } + `); + }); + }); + }); + } + ); +}