diff --git a/x-pack/legacy/plugins/apm/common/agent_configuration_constants.ts b/x-pack/legacy/plugins/apm/common/agent_configuration_constants.ts new file mode 100644 index 00000000000000..4ddc65c14a134a --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/agent_configuration_constants.ts @@ -0,0 +1,25 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +export const ALL_OPTION_VALUE = 'ALL_OPTION_VALUE'; + +// human-readable label for the option. The "All" option should be translated. +// Everything else should be returned verbatim +export function getOptionLabel(value: string | undefined) { + if (value === undefined || value === ALL_OPTION_VALUE) { + return i18n.translate('xpack.apm.settings.agentConf.allOptionLabel', { + defaultMessage: 'All' + }); + } + + return value; +} + +export function omitAllOption(value: string) { + return value === ALL_OPTION_VALUE ? undefined : value; +} diff --git a/x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.test.ts b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.test.ts new file mode 100644 index 00000000000000..b62251b6974d9b --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.test.ts @@ -0,0 +1,28 @@ +/* + * 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 { transactionMaxSpansRt } from './index'; +import { isRight } from 'fp-ts/lib/Either'; + +describe('transactionMaxSpans', () => { + it('does not accept empty values', () => { + expect(isRight(transactionMaxSpansRt.decode(undefined))).toBe(false); + expect(isRight(transactionMaxSpansRt.decode(null))).toBe(false); + expect(isRight(transactionMaxSpansRt.decode(''))).toBe(false); + }); + + it('accepts both strings and numbers as values', () => { + expect(isRight(transactionMaxSpansRt.decode('55'))).toBe(true); + expect(isRight(transactionMaxSpansRt.decode(55))).toBe(true); + }); + + it('checks if the number falls within 0, 32000', () => { + expect(isRight(transactionMaxSpansRt.decode(0))).toBe(true); + expect(isRight(transactionMaxSpansRt.decode(32000))).toBe(true); + expect(isRight(transactionMaxSpansRt.decode(-55))).toBe(false); + expect(isRight(transactionMaxSpansRt.decode(NaN))).toBe(false); + }); +}); diff --git a/x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.ts b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.ts new file mode 100644 index 00000000000000..251161c21babe0 --- /dev/null +++ b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_max_spans_rt/index.ts @@ -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; + * you may not use this file except in compliance with the Elastic License. + */ + +import * as t from 'io-ts'; + +export const transactionMaxSpansRt = new t.Type( + 'transactionMaxSpans', + t.number.is, + (input, context) => { + const value = parseInt(input as string, 10); + return value >= 0 && value <= 32000 + ? t.success(value) + : t.failure(input, context); + }, + t.identity +); diff --git a/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.test.ts b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.test.ts index 85551481e78b62..6930a69f0870ad 100644 --- a/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.test.ts +++ b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.test.ts @@ -7,6 +7,12 @@ import { transactionSampleRateRt } from './index'; import { isRight } from 'fp-ts/lib/Either'; describe('transactionSampleRateRt', () => { + it('does not accept empty values', () => { + expect(isRight(transactionSampleRateRt.decode(undefined))).toBe(false); + expect(isRight(transactionSampleRateRt.decode(null))).toBe(false); + expect(isRight(transactionSampleRateRt.decode(''))).toBe(false); + }); + it('accepts both strings and numbers as values', () => { expect(isRight(transactionSampleRateRt.decode('0.5'))).toBe(true); expect(isRight(transactionSampleRateRt.decode(0.5))).toBe(true); diff --git a/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.ts b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.ts index d6df87e7a5fed2..90c60d16f7b599 100644 --- a/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.ts +++ b/x-pack/legacy/plugins/apm/common/runtime_types/transaction_sample_rate_rt/index.ts @@ -10,8 +10,8 @@ export const transactionSampleRateRt = new t.Type( 'TransactionSampleRate', t.number.is, (input, context) => { - const value = Number(input); - return value >= 0 && value <= 1 && Number(value.toFixed(3)) === value + const value = parseFloat(input as string); + return value >= 0 && value <= 1 && parseFloat(value.toFixed(3)) === value ? t.success(value) : t.failure(input, context); }, diff --git a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx index 9b598ff469b9da..3f009a990afa55 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/ErrorGroupDetails/DetailView/index.tsx @@ -36,7 +36,7 @@ import { logStacktraceTab } from './ErrorTabs'; import { Summary } from '../../../shared/Summary'; -import { TimestampSummaryItem } from '../../../shared/Summary/TimestampSummaryItem'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; import { HttpInfoSummaryItem } from '../../../shared/Summary/HttpInfoSummaryItem'; import { TransactionDetailLink } from '../../../shared/Links/apm/TransactionDetailLink'; import { UserAgentSummaryItem } from '../../../shared/Summary/UserAgentSummaryItem'; @@ -114,7 +114,7 @@ export function DetailView({ errorGroup, urlParams, location }: Props) { , + , errorUrl && method ? ( void; - onSubmit: () => void; - isOpen: boolean; - selectedConfig: Config | null; -} - -export function AddSettingsFlyout({ - onClose, - isOpen, - onSubmit, - selectedConfig -}: Props) { - const { - notifications: { toasts } - } = useKibanaCore(); - const [environment, setEnvironment] = useState( - selectedConfig - ? selectedConfig.service.environment || ENVIRONMENT_NOT_DEFINED - : undefined - ); - const [serviceName, setServiceName] = useState( - selectedConfig ? selectedConfig.service.name : undefined - ); - const [sampleRate, setSampleRate] = useState( - selectedConfig - ? selectedConfig.settings.transaction_sample_rate.toString() - : '' - ); - const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher( - () => - callApmApi({ - pathname: '/api/apm/settings/agent-configuration/services' - }), - [], - { preservePreviousData: false } - ); - const { data: environments = [], status: environmentStatus } = useFetcher( - () => { - if (serviceName) { - return callApmApi({ - pathname: - '/api/apm/settings/agent-configuration/services/{serviceName}/environments', - params: { - path: { serviceName } - } - }); - } - }, - [serviceName], - { preservePreviousData: false } - ); - - const isSampleRateValid = isRight(transactionSampleRateRt.decode(sampleRate)); - - const isSelectedEnvironmentValid = environments.some( - env => - env.name === environment && (Boolean(selectedConfig) || env.available) - ); - - if (!isOpen) { - return null; - } - - return ( - - - - - {selectedConfig ? ( -

- {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.editConfigTitle', - { - defaultMessage: 'Edit configuration' - } - )} -

- ) : ( -

- {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.createConfigTitle', - { - defaultMessage: 'Create configuration' - } - )} -

- )} -
-
- - - {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.betaCallOutText', - { - defaultMessage: - 'Please note only sample rate configuration is supported in this first version. We will extend support for agent configuration in future releases. Please be aware of bugs.' - } - )} - - - { - if (selectedConfig) { - await deleteConfig(selectedConfig, toasts); - } - onSubmit(); - }} - environment={environment} - setEnvironment={setEnvironment} - serviceName={serviceName} - setServiceName={setServiceName} - sampleRate={sampleRate} - setSampleRate={setSampleRate} - serviceNames={serviceNames} - serviceNamesStatus={serviceNamesStatus} - environments={environments} - environmentStatus={environmentStatus} - isSampleRateValid={isSampleRateValid} - isSelectedEnvironmentValid={isSelectedEnvironmentValid} - /> - - - - - - {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.cancelButtonLabel', - { - defaultMessage: 'Cancel' - } - )} - - - - ) => { - event.preventDefault(); - await saveConfig({ - environment, - serviceName, - sampleRate: parseFloat(sampleRate), - configurationId: selectedConfig - ? selectedConfig.id - : undefined, - toasts - }); - onSubmit(); - }} - > - {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.saveConfigurationButtonLabel', - { - defaultMessage: 'Save configuration' - } - )} - - - - -
-
- ); -} - -async function deleteConfig( - selectedConfig: Config, - toasts: NotificationsStart['toasts'] -) { - try { - await callApmApi({ - pathname: '/api/apm/settings/agent-configuration/{configurationId}', - method: 'DELETE', - params: { - path: { configurationId: selectedConfig.id } - } - }); - toasts.addSuccess({ - title: i18n.translate( - 'xpack.apm.settings.agentConf.deleteConfigSucceededTitle', - { - defaultMessage: 'Configuration was deleted' - } - ), - text: ( - - ) - }); - } catch (error) { - toasts.addDanger({ - title: i18n.translate( - 'xpack.apm.settings.agentConf.deleteConfigFailedTitle', - { - defaultMessage: 'Configuration could not be deleted' - } - ), - text: ( - - ) - }); - } -} - -async function saveConfig({ - sampleRate, - serviceName, - environment, - configurationId, - toasts -}: { - sampleRate: number; - serviceName: string | undefined; - environment: string | undefined; - configurationId?: string; - toasts: NotificationsStart['toasts']; -}) { - trackEvent({ app: 'apm', name: 'save_agent_configuration' }); - - try { - if (isNaN(sampleRate) || !serviceName) { - throw new Error('Missing arguments'); - } - - const configuration = { - settings: { - transaction_sample_rate: sampleRate - }, - service: { - name: serviceName, - environment: - environment === ENVIRONMENT_NOT_DEFINED ? undefined : environment - } - }; - - if (configurationId) { - await callApmApi({ - pathname: '/api/apm/settings/agent-configuration/{configurationId}', - method: 'PUT', - params: { - path: { configurationId }, - body: configuration - } - }); - - toasts.addSuccess({ - title: i18n.translate( - 'xpack.apm.settings.agentConf.editConfigSucceededTitle', - { - defaultMessage: 'Configuration edited' - } - ), - text: ( - - ) - }); - } else { - await callApmApi({ - pathname: '/api/apm/settings/agent-configuration/new', - method: 'POST', - params: { - body: configuration - } - }); - toasts.addSuccess({ - title: i18n.translate( - 'xpack.apm.settings.agentConf.createConfigSucceededTitle', - { - defaultMessage: 'Configuration created!' - } - ), - text: ( - - ) - }); - } - } catch (error) { - if (configurationId) { - toasts.addDanger({ - title: i18n.translate( - 'xpack.apm.settings.agentConf.editConfigFailedTitle', - { - defaultMessage: 'Configuration could not be edited' - } - ), - text: ( - - ) - }); - } else { - toasts.addDanger({ - title: i18n.translate( - 'xpack.apm.settings.agentConf.createConfigFailedTitle', - { - defaultMessage: 'Configuration could not be created' - } - ), - text: ( - - ) - }); - } - } -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyoutBody.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyoutBody.tsx deleted file mode 100644 index 090f3fe0d5f912..00000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/AddSettings/AddSettingFlyoutBody.tsx +++ /dev/null @@ -1,277 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React from 'react'; -import { - EuiForm, - EuiFormRow, - EuiButton, - EuiFieldText, - EuiTitle, - EuiSpacer, - EuiHorizontalRule, - EuiText -} from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import { isEmpty } from 'lodash'; -import { FETCH_STATUS } from '../../../../hooks/useFetcher'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; -import { SelectWithPlaceholder } from '../../../shared/SelectWithPlaceholder'; -import { Config } from '..'; - -const selectPlaceholderLabel = `- ${i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.selectPlaceholder', - { - defaultMessage: 'Select' - } -)} -`; - -export function AddSettingFlyoutBody({ - selectedConfig, - onDelete, - environment, - setEnvironment, - serviceName, - setServiceName, - sampleRate, - setSampleRate, - serviceNames, - serviceNamesStatus, - environments, - environmentStatus, - isSampleRateValid, - isSelectedEnvironmentValid -}: { - selectedConfig: Config | null; - onDelete: () => void; - environment?: string; - setEnvironment: (env: string | undefined) => void; - serviceName?: string; - setServiceName: (env: string | undefined) => void; - sampleRate: string; - setSampleRate: (env: string) => void; - serviceNames: string[]; - serviceNamesStatus?: FETCH_STATUS; - environments: Array<{ - name: string; - available: boolean; - }>; - environmentStatus?: FETCH_STATUS; - isSampleRateValid: boolean; - isSelectedEnvironmentValid: boolean; -}) { - const environmentOptions = environments.map(({ name, available }) => ({ - disabled: !available, - text: - name === ENVIRONMENT_NOT_DEFINED - ? i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceEnvironmentNotSetOptionLabel', - { - defaultMessage: 'Not set' - } - ) - : name, - value: name - })); - - return ( - -
- -

- {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.serviceSectionTitle', - { - defaultMessage: 'Service' - } - )} -

-
- - - - - ({ text }))} - value={serviceName} - disabled={Boolean(selectedConfig)} - onChange={e => { - e.preventDefault(); - setServiceName(e.target.value); - setEnvironment(undefined); - }} - /> - - - - { - e.preventDefault(); - setEnvironment(e.target.value); - }} - /> - - - - - -

- {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.configurationSectionTitle', - { - defaultMessage: 'Configuration' - } - )} -

-
- - - - - { - e.preventDefault(); - setSampleRate(e.target.value); - }} - disabled={!(serviceName && environment) && !selectedConfig} - /> - - - {selectedConfig ? ( - <> - - -

- - {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionTitle', - { - defaultMessage: 'Delete configuration' - } - )} - -

-
- - - - -

- {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionText', - { - defaultMessage: - 'If you wish to delete this configuration, please be aware that the agents will continue to use the existing configuration until they sync with the APM Server.' - } - )} -

-
- - - - - {i18n.translate( - 'xpack.apm.settings.agentConf.flyOut.deleteConfigurationButtonLabel', - { - defaultMessage: 'Delete' - } - )} - - - - - ) : null} - -
- ); -} 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 new file mode 100644 index 00000000000000..05d33915a6b86e --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/DeleteButton.tsx @@ -0,0 +1,92 @@ +/* + * 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 React, { useState } from 'react'; +import { EuiButtonEmpty } from '@elastic/eui'; +import { NotificationsStart } from 'kibana/public'; +import { i18n } from '@kbn/i18n'; +import { Config } from '../index'; +import { callApmApi } from '../../../../../services/rest/callApmApi'; +import { getOptionLabel } from '../../../../../../common/agent_configuration_constants'; +import { useKibanaCore } from '../../../../../../../observability/public'; + +interface Props { + onDeleted: () => void; + selectedConfig: Config; +} + +export function DeleteButton({ onDeleted, selectedConfig }: Props) { + const [isDeleting, setIsDeleting] = useState(false); + const { + notifications: { toasts } + } = useKibanaCore(); + + return ( + { + setIsDeleting(true); + await deleteConfig(selectedConfig, toasts); + setIsDeleting(false); + onDeleted(); + }} + > + {i18n.translate( + 'xpack.apm.settings.agentConf.flyout.deleteSection.buttonLabel', + { defaultMessage: 'Delete' } + )} + + ); +} + +async function deleteConfig( + selectedConfig: Config, + toasts: NotificationsStart['toasts'] +) { + try { + await callApmApi({ + pathname: '/api/apm/settings/agent-configuration/{configurationId}', + method: 'DELETE', + params: { + path: { configurationId: selectedConfig.id } + } + }); + toasts.addSuccess({ + title: i18n.translate( + 'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededTitle', + { defaultMessage: 'Configuration was deleted' } + ), + text: i18n.translate( + 'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigSucceededText', + { + defaultMessage: + 'You have successfully deleted a configuration for "{serviceName}". It will take some time to propagate to the agents.', + values: { serviceName: getOptionLabel(selectedConfig.service.name) } + } + ) + }); + } catch (error) { + toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigFailedTitle', + { defaultMessage: 'Configuration could not be deleted' } + ), + text: i18n.translate( + 'xpack.apm.settings.agentConf.flyout.deleteSection.deleteConfigFailedText', + { + defaultMessage: + 'Something went wrong when deleting a configuration for "{serviceName}". Error: "{errorMessage}"', + values: { + serviceName: getOptionLabel(selectedConfig.service.name), + errorMessage: error.message + } + } + ) + }); + } +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx new file mode 100644 index 00000000000000..7c114977d1d72d --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/ServiceSection.tsx @@ -0,0 +1,160 @@ +/* + * 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 { EuiTitle, EuiSpacer, EuiFormRow, EuiText } from '@elastic/eui'; +import React from 'react'; +import { i18n } from '@kbn/i18n'; +import { SelectWithPlaceholder } from '../../../../shared/SelectWithPlaceholder'; +import { useFetcher } from '../../../../../hooks/useFetcher'; +import { callApmApi } from '../../../../../services/rest/callApmApi'; +import { + getOptionLabel, + omitAllOption +} from '../../../../../../common/agent_configuration_constants'; + +const SELECT_PLACEHOLDER_LABEL = `- ${i18n.translate( + 'xpack.apm.settings.agentConf.flyOut.serviceSection.selectPlaceholder', + { defaultMessage: 'Select' } +)} -`; + +interface Props { + isReadOnly: boolean; + serviceName: string; + setServiceName: (env: string) => void; + environment: string; + setEnvironment: (env: string) => void; +} + +export function ServiceSection({ + isReadOnly, + serviceName, + setServiceName, + environment, + setEnvironment +}: Props) { + const { data: serviceNames = [], status: serviceNamesStatus } = useFetcher( + () => { + if (!isReadOnly) { + return callApmApi({ + pathname: '/api/apm/settings/agent-configuration/services', + forceCache: true + }); + } + }, + [isReadOnly], + { preservePreviousData: false } + ); + const { data: environments = [], status: environmentStatus } = useFetcher( + () => { + if (!isReadOnly && serviceName) { + return callApmApi({ + pathname: '/api/apm/settings/agent-configuration/environments', + params: { query: { serviceName: omitAllOption(serviceName) } } + }); + } + }, + [isReadOnly, serviceName], + { preservePreviousData: false } + ); + + const ALREADY_CONFIGURED_TRANSLATED = i18n.translate( + 'xpack.apm.settings.agentConf.flyOut.serviceSection.alreadyConfiguredOption', + { defaultMessage: 'already configured' } + ); + + const serviceNameOptions = serviceNames.map(name => ({ + text: getOptionLabel(name), + value: name + })); + const environmentOptions = environments.map( + ({ name, alreadyConfigured }) => ({ + disabled: alreadyConfigured, + text: `${getOptionLabel(name)} ${ + alreadyConfigured ? `(${ALREADY_CONFIGURED_TRANSLATED})` : '' + }`, + value: name + }) + ); + + return ( + <> + +

+ {i18n.translate( + 'xpack.apm.settings.agentConf.flyOut.serviceSection.title', + { defaultMessage: 'Service' } + )} +

+
+ + + + + {isReadOnly ? ( + {getOptionLabel(serviceName)} + ) : ( + { + e.preventDefault(); + setServiceName(e.target.value); + setEnvironment(''); + }} + /> + )} + + + + {isReadOnly ? ( + {getOptionLabel(environment)} + ) : ( + { + e.preventDefault(); + setEnvironment(e.target.value); + }} + /> + )} + + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/SettingsSection.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/SettingsSection.tsx new file mode 100644 index 00000000000000..24c8222d4cd991 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/SettingsSection.tsx @@ -0,0 +1,174 @@ +/* + * 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 React from 'react'; +import { + EuiFormRow, + EuiFieldText, + EuiTitle, + EuiSpacer, + EuiFieldNumber +} from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { isEmpty } from 'lodash'; +import { SelectWithPlaceholder } from '../../../../shared/SelectWithPlaceholder'; + +interface Props { + isRumService: boolean; + + // sampleRate + sampleRate: string; + setSampleRate: (value: string) => void; + isSampleRateValid?: boolean; + + // captureBody + captureBody: string; + setCaptureBody: (value: string) => void; + + // transactionMaxSpans + transactionMaxSpans: string; + setTransactionMaxSpans: (value: string) => void; + isTransactionMaxSpansValid?: boolean; +} + +export function SettingsSection({ + isRumService, + + // sampleRate + sampleRate, + setSampleRate, + isSampleRateValid, + + // captureBody + captureBody, + setCaptureBody, + + // transactionMaxSpans + transactionMaxSpans, + setTransactionMaxSpans, + isTransactionMaxSpansValid +}: Props) { + return ( + <> + +

+ {i18n.translate( + 'xpack.apm.settings.agentConf.flyOut.settingsSection.title', + { defaultMessage: 'Options' } + )} +

+
+ + + + + { + e.preventDefault(); + setSampleRate(e.target.value); + }} + /> + + + + + {!isRumService && ( + + { + e.preventDefault(); + setCaptureBody(e.target.value); + }} + /> + + )} + + {!isRumService && ( + + { + e.preventDefault(); + setTransactionMaxSpans(e.target.value); + }} + /> + + )} + + ); +} 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 new file mode 100644 index 00000000000000..558f2161c0c945 --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/index.tsx @@ -0,0 +1,256 @@ +/* + * 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 { + EuiButton, + EuiButtonEmpty, + EuiFlexGroup, + EuiFlexItem, + EuiFlyout, + EuiFlyoutBody, + EuiFlyoutFooter, + EuiFlyoutHeader, + EuiForm, + EuiPortal, + EuiTitle, + EuiText, + EuiSpacer +} from '@elastic/eui'; +import { idx } from '@kbn/elastic-idx'; +import React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { isRight } from 'fp-ts/lib/Either'; +import { transactionSampleRateRt } from '../../../../../../common/runtime_types/transaction_sample_rate_rt'; +import { callApmApi } from '../../../../../services/rest/callApmApi'; +import { Config } from '../index'; +import { SettingsSection } from './SettingsSection'; +import { ServiceSection } from './ServiceSection'; +import { DeleteButton } from './DeleteButton'; +import { transactionMaxSpansRt } from '../../../../../../common/runtime_types/transaction_max_spans_rt'; +import { useFetcher } from '../../../../../hooks/useFetcher'; +import { isRumAgentName } from '../../../../../../common/agent_name'; +import { ALL_OPTION_VALUE } from '../../../../../../common/agent_configuration_constants'; +import { saveConfig } from './saveConfig'; +import { useKibanaCore } from '../../../../../../../observability/public'; + +const defaultSettings = { + TRANSACTION_SAMPLE_RATE: '1.0', + CAPTURE_BODY: 'off', + TRANSACTION_MAX_SPANS: '500' +}; + +interface Props { + onClose: () => void; + onSaved: () => void; + onDeleted: () => void; + selectedConfig: Config | null; +} + +export function AddEditFlyout({ + onClose, + onSaved, + onDeleted, + selectedConfig +}: Props) { + const { + notifications: { toasts } + } = useKibanaCore(); + const [isSaving, setIsSaving] = useState(false); + + // config conditions (service) + const [serviceName, setServiceName] = useState( + selectedConfig ? selectedConfig.service.name || ALL_OPTION_VALUE : '' + ); + const [environment, setEnvironment] = useState( + selectedConfig ? selectedConfig.service.environment || ALL_OPTION_VALUE : '' + ); + + const { data: { agentName } = { agentName: undefined } } = useFetcher( + () => { + if (serviceName === ALL_OPTION_VALUE) { + return { agentName: undefined }; + } + + if (serviceName) { + return callApmApi({ + pathname: '/api/apm/settings/agent-configuration/agent_name', + params: { query: { serviceName } } + }); + } + }, + [serviceName], + { preservePreviousData: false } + ); + + // config settings + const [sampleRate, setSampleRate] = useState( + ( + idx(selectedConfig, _ => _.settings.transaction_sample_rate) || + defaultSettings.TRANSACTION_SAMPLE_RATE + ).toString() + ); + const [captureBody, setCaptureBody] = useState( + idx(selectedConfig, _ => _.settings.capture_body) || + defaultSettings.CAPTURE_BODY + ); + const [transactionMaxSpans, setTransactionMaxSpans] = useState( + ( + idx(selectedConfig, _ => _.settings.transaction_max_spans) || + defaultSettings.TRANSACTION_MAX_SPANS + ).toString() + ); + + const isRumService = isRumAgentName(agentName); + const isSampleRateValid = isRight(transactionSampleRateRt.decode(sampleRate)); + const isTransactionMaxSpansValid = isRight( + transactionMaxSpansRt.decode(transactionMaxSpans) + ); + + const isFormValid = + !!serviceName && + !!environment && + isSampleRateValid && + // captureBody and isTransactionMaxSpansValid are required except if service is RUM + (isRumService || (!!captureBody && isTransactionMaxSpansValid)) && + // agent name is required, except if serviceName is "all" + (serviceName === ALL_OPTION_VALUE || agentName !== undefined); + + const handleSubmitEvent = async ( + event: + | React.FormEvent + | React.MouseEvent + ) => { + event.preventDefault(); + setIsSaving(true); + + await saveConfig({ + serviceName, + environment, + sampleRate, + captureBody, + transactionMaxSpans, + configurationId: selectedConfig ? selectedConfig.id : undefined, + agentName, + toasts + }); + setIsSaving(false); + onSaved(); + }; + + return ( + + + + +

+ {selectedConfig + ? i18n.translate( + 'xpack.apm.settings.agentConf.editConfigTitle', + { defaultMessage: 'Edit configuration' } + ) + : i18n.translate( + 'xpack.apm.settings.agentConf.createConfigTitle', + { defaultMessage: 'Create configuration' } + )} +

+
+
+ + + This allows you to fine-tune your agent configuration directly in + Kibana. Best of all, changes are automatically propagated to your + APM agents so there’s no need to redeploy. + + + + + + {/* eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions */} +
{ + const didClickEnter = e.which === 13; + if (didClickEnter) { + handleSubmitEvent(e); + } + }} + > + + + + + + +
+
+ + + + {selectedConfig ? ( + + ) : null} + + + + + + {i18n.translate( + 'xpack.apm.settings.agentConf.cancelButtonLabel', + { defaultMessage: 'Cancel' } + )} + + + + + {i18n.translate( + 'xpack.apm.settings.agentConf.saveConfigurationButtonLabel', + { defaultMessage: 'Save' } + )} + + + + + + +
+
+ ); +} 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 new file mode 100644 index 00000000000000..50e59fc00cffea --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AddEditFlyout/saveConfig.ts @@ -0,0 +1,115 @@ +/* + * 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 { i18n } from '@kbn/i18n'; +import { NotificationsStart } from 'kibana/public'; +import { trackEvent } from '../../../../../../../infra/public/hooks/use_track_metric'; +import { isRumAgentName } from '../../../../../../common/agent_name'; +import { + getOptionLabel, + omitAllOption +} from '../../../../../../common/agent_configuration_constants'; +import { callApmApi } from '../../../../../services/rest/callApmApi'; + +interface Settings { + transaction_sample_rate: number; + capture_body?: string; + transaction_max_spans?: number; +} + +export async function saveConfig({ + serviceName, + environment, + sampleRate, + captureBody, + transactionMaxSpans, + configurationId, + agentName, + toasts +}: { + serviceName: string; + environment: string; + sampleRate: string; + captureBody: string; + transactionMaxSpans: string; + configurationId?: string; + agentName?: string; + toasts: NotificationsStart['toasts']; +}) { + trackEvent({ app: 'apm', name: 'save_agent_configuration' }); + + try { + const settings: Settings = { + transaction_sample_rate: Number(sampleRate) + }; + + if (!isRumAgentName(agentName)) { + settings.capture_body = captureBody; + settings.transaction_max_spans = Number(transactionMaxSpans); + } + + const configuration = { + agent_name: agentName, + service: { + name: omitAllOption(serviceName), + environment: omitAllOption(environment) + }, + 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 + } + }); + } + + toasts.addSuccess({ + title: i18n.translate( + 'xpack.apm.settings.agentConf.saveConfig.succeeded.title', + { defaultMessage: 'Configuration saved' } + ), + text: i18n.translate( + 'xpack.apm.settings.agentConf.saveConfig.succeeded.text', + { + defaultMessage: + 'The configuration for "{serviceName}" was saved. It will take some time to propagate to the agents.', + values: { serviceName: getOptionLabel(serviceName) } + } + ) + }); + } catch (error) { + toasts.addDanger({ + title: i18n.translate( + 'xpack.apm.settings.agentConf.saveConfig.failed.title', + { defaultMessage: 'Configuration could not be saved' } + ), + text: i18n.translate( + 'xpack.apm.settings.agentConf.saveConfig.failed.text', + { + defaultMessage: + 'Something went wrong when saving the configuration for "{serviceName}". Error: "{errorMessage}"', + values: { + serviceName: getOptionLabel(serviceName), + errorMessage: error.message + } + } + ) + }); + } +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/SettingsList.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx similarity index 51% rename from x-pack/legacy/plugins/apm/public/components/app/Settings/SettingsList.tsx rename to x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx index 00fcd09cdcf302..161d3711484785 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/SettingsList.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/AgentConfigurationList.tsx @@ -5,17 +5,26 @@ */ import React from 'react'; -import moment from 'moment'; import { i18n } from '@kbn/i18n'; -import { EuiEmptyPrompt, EuiButton, EuiButtonEmpty } from '@elastic/eui'; +import { + EuiEmptyPrompt, + EuiButton, + EuiButtonEmpty, + EuiHealth, + EuiToolTip +} from '@elastic/eui'; import { isEmpty } from 'lodash'; -import { FETCH_STATUS } from '../../../hooks/useFetcher'; -import { ITableColumn, ManagedTable } from '../../shared/ManagedTable'; -import { LoadingStatePrompt } from '../../shared/LoadingStatePrompt'; -import { AgentConfigurationListAPIResponse } from '../../../../server/lib/settings/agent_configuration/list_configurations'; +import theme from '@elastic/eui/dist/eui_theme_light.json'; +import { FETCH_STATUS } from '../../../../hooks/useFetcher'; +import { ITableColumn, ManagedTable } from '../../../shared/ManagedTable'; +import { LoadingStatePrompt } from '../../../shared/LoadingStatePrompt'; +import { AgentConfigurationListAPIResponse } from '../../../../../server/lib/settings/agent_configuration/list_configurations'; import { Config } from '.'; +import { TimestampTooltip } from '../../../shared/TimestampTooltip'; +import { px, units } from '../../../../style/variables'; +import { getOptionLabel } from '../../../../../common/agent_configuration_constants'; -export function SettingsList({ +export function AgentConfigurationList({ status, data, setIsFlyoutOpen, @@ -23,21 +32,44 @@ export function SettingsList({ }: { status: FETCH_STATUS; data: AgentConfigurationListAPIResponse; - setIsFlyoutOpen: React.Dispatch>; - setSelectedConfig: React.Dispatch>; + setIsFlyoutOpen: (val: boolean) => void; + setSelectedConfig: (val: Config | null) => void; }) { const columns: Array> = [ + { + field: 'applied_by_agent', + align: 'center', + width: px(units.double), + name: '', + sortable: true, + render: (isApplied: boolean) => ( + + + + ) + }, { field: 'service.name', name: i18n.translate( 'xpack.apm.settings.agentConf.configTable.serviceNameColumnLabel', - { - defaultMessage: 'Service name' - } + { defaultMessage: 'Service name' } ), sortable: true, render: (_, config: Config) => ( { @@ -45,7 +77,7 @@ export function SettingsList({ setIsFlyoutOpen(true); }} > - {config.service.name} + {getOptionLabel(config.service.name)} ) }, @@ -53,50 +85,64 @@ export function SettingsList({ field: 'service.environment', name: i18n.translate( 'xpack.apm.settings.agentConf.configTable.environmentColumnLabel', - { - defaultMessage: 'Service environment' - } + { defaultMessage: 'Service environment' } ), sortable: true, - render: (value: string) => value + render: (value: string) => getOptionLabel(value) }, { field: 'settings.transaction_sample_rate', name: i18n.translate( 'xpack.apm.settings.agentConf.configTable.sampleRateColumnLabel', - { - defaultMessage: 'Sample rate' - } + { defaultMessage: 'Sample rate' } + ), + dataType: 'number', + sortable: true, + render: (value: number) => value + }, + { + field: 'settings.capture_body', + name: i18n.translate( + 'xpack.apm.settings.agentConf.configTable.captureBodyColumnLabel', + { defaultMessage: 'Capture body' } ), sortable: true, render: (value: string) => value }, { + field: 'settings.transaction_max_spans', + name: i18n.translate( + 'xpack.apm.settings.agentConf.configTable.transactionMaxSpansColumnLabel', + { defaultMessage: 'Transaction max spans' } + ), + dataType: 'number', + sortable: true, + render: (value: number) => value + }, + { + align: 'right', field: '@timestamp', name: i18n.translate( 'xpack.apm.settings.agentConf.configTable.lastUpdatedColumnLabel', - { - defaultMessage: 'Last updated' - } + { defaultMessage: 'Last updated' } ), sortable: true, - render: (value: number) => (value ? moment(value).fromNow() : null) + render: (value: number) => ( + + ) }, { + width: px(units.double), name: '', actions: [ { name: i18n.translate( 'xpack.apm.settings.agentConf.configTable.editButtonLabel', - { - defaultMessage: 'Edit' - } + { defaultMessage: 'Edit' } ), description: i18n.translate( 'xpack.apm.settings.agentConf.configTable.editButtonDescription', - { - defaultMessage: 'Edit this config' - } + { defaultMessage: 'Edit this config' } ), icon: 'pencil', color: 'primary', @@ -137,10 +183,8 @@ export function SettingsList({ actions={ setIsFlyoutOpen(true)}> {i18n.translate( - 'xpack.apm.settings.agentConf.createConfigButtonLabel', - { - defaultMessage: 'Create configuration' - } + 'xpack.apm.settings.agentConf.configTable.createConfigButtonLabel', + { defaultMessage: 'Create configuration' } )} } @@ -154,7 +198,7 @@ export function SettingsList({ <>

{i18n.translate( - 'xpack.apm.settings.agentConf.configTable.failurePromptText', + 'xpack.apm.settings.agentConf.configTable.configTable.failurePromptText', { defaultMessage: 'The list of agent configurations could not be fetched. Your user may not have the sufficient permissions.' @@ -165,26 +209,23 @@ export function SettingsList({ } /> ); - const hasConfigurations = !isEmpty(data); if (status === 'failure') { return failurePrompt; } - if (status === 'success') { - if (hasConfigurations) { - return ( - } - columns={columns} - items={data} - initialSortField="service.name" - initialSortDirection="asc" - initialPageSize={50} - /> - ); - } else { - return emptyStatePrompt; - } + + if (status === 'success' && isEmpty(data)) { + return emptyStatePrompt; } - return null; + + return ( + } + columns={columns} + items={data} + initialSortField="service.name" + initialSortDirection="asc" + initialPageSize={50} + /> + ); } diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx new file mode 100644 index 00000000000000..002c735da8eb3f --- /dev/null +++ b/x-pack/legacy/plugins/apm/public/components/app/Settings/AgentConfigurations/index.tsx @@ -0,0 +1,140 @@ +/* + * 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 React, { useState } from 'react'; +import { i18n } from '@kbn/i18n'; +import { + EuiTitle, + EuiFlexGroup, + EuiFlexItem, + EuiButtonEmpty, + EuiPanel, + EuiSpacer, + EuiButton +} from '@elastic/eui'; +import { isEmpty } from 'lodash'; +import { useFetcher } from '../../../../hooks/useFetcher'; +import { AgentConfigurationListAPIResponse } from '../../../../../server/lib/settings/agent_configuration/list_configurations'; +import { callApmApi } from '../../../../services/rest/callApmApi'; +import { HomeLink } from '../../../shared/Links/apm/HomeLink'; +import { AgentConfigurationList } from './AgentConfigurationList'; +import { useTrackPageview } from '../../../../../../infra/public'; +import { AddEditFlyout } from './AddEditFlyout'; + +export type Config = AgentConfigurationListAPIResponse[0]; + +export function AgentConfigurations() { + const { data = [], status, refetch } = useFetcher( + () => callApmApi({ pathname: `/api/apm/settings/agent-configuration` }), + [], + { preservePreviousData: false } + ); + const [selectedConfig, setSelectedConfig] = useState(null); + const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); + + useTrackPageview({ app: 'apm', path: 'agent_configuration' }); + useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 }); + + const hasConfigurations = !isEmpty(data); + + const onClose = () => { + setSelectedConfig(null); + setIsFlyoutOpen(false); + }; + + return ( + <> + {isFlyoutOpen && ( + { + onClose(); + refetch(); + }} + onDeleted={() => { + onClose(); + refetch(); + }} + /> + )} + + + + +

+ {i18n.translate('xpack.apm.settings.agentConf.pageTitle', { + defaultMessage: 'Settings' + })} +

+ + + + + + {i18n.translate( + 'xpack.apm.settings.agentConf.returnToOverviewLinkLabel', + { defaultMessage: 'Return to overview' } + )} + + + + + + + + + + + +

+ {i18n.translate( + 'xpack.apm.settings.agentConf.configurationsPanelTitle', + { defaultMessage: 'Agent remote configuration' } + )} +

+
+
+ + {hasConfigurations ? ( + setIsFlyoutOpen(true)} /> + ) : null} +
+ + + + +
+ + ); +} + +function CreateConfigurationButton({ onClick }: { onClick: () => void }) { + return ( + + + + + {i18n.translate( + 'xpack.apm.settings.agentConf.createConfigButtonLabel', + { defaultMessage: 'Create configuration' } + )} + + + + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx deleted file mode 100644 index b75d3cf6ff458c..00000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/Settings/index.tsx +++ /dev/null @@ -1,187 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import React, { useState } from 'react'; -import { i18n } from '@kbn/i18n'; -import { FormattedMessage } from '@kbn/i18n/react'; -import { - EuiTitle, - EuiFlexGroup, - EuiFlexItem, - EuiButtonEmpty, - EuiPanel, - EuiBetaBadge, - EuiSpacer, - EuiCallOut, - EuiButton, - EuiLink -} from '@elastic/eui'; -import { isEmpty } from 'lodash'; -import { useFetcher } from '../../../hooks/useFetcher'; -import { AgentConfigurationListAPIResponse } from '../../../../server/lib/settings/agent_configuration/list_configurations'; -import { AddSettingsFlyout } from './AddSettings/AddSettingFlyout'; -import { callApmApi } from '../../../services/rest/callApmApi'; -import { HomeLink } from '../../shared/Links/apm/HomeLink'; -import { SettingsList } from './SettingsList'; -import { useTrackPageview } from '../../../../../infra/public'; - -export type Config = AgentConfigurationListAPIResponse[0]; - -export function Settings() { - const { data = [], status, refresh } = useFetcher( - () => - callApmApi({ - pathname: `/api/apm/settings/agent-configuration` - }), - [] - ); - const [selectedConfig, setSelectedConfig] = useState(null); - const [isFlyoutOpen, setIsFlyoutOpen] = useState(false); - - useTrackPageview({ app: 'apm', path: 'agent_configuration' }); - useTrackPageview({ app: 'apm', path: 'agent_configuration', delay: 15000 }); - - const RETURN_TO_OVERVIEW_LINK_LABEL = i18n.translate( - 'xpack.apm.settings.agentConf.returnToOverviewLinkLabel', - { - defaultMessage: 'Return to overview' - } - ); - - const hasConfigurations = !isEmpty(data); - - return ( - <> - { - setSelectedConfig(null); - setIsFlyoutOpen(false); - }} - onSubmit={() => { - setSelectedConfig(null); - setIsFlyoutOpen(false); - refresh(); - }} - /> - - - - -

- {i18n.translate('xpack.apm.settings.agentConf.pageTitle', { - defaultMessage: 'Settings' - })} -

-
-
- - - - {RETURN_TO_OVERVIEW_LINK_LABEL} - - - -
- - - - - - - -

- {i18n.translate( - 'xpack.apm.settings.agentConf.configurationsPanelTitle', - { - defaultMessage: 'Configurations' - } - )} -

-
-
- - - - {hasConfigurations ? ( - - - - setIsFlyoutOpen(true)} - > - {i18n.translate( - 'xpack.apm.settings.agentConf.createConfigButtonLabel', - { - defaultMessage: 'Create configuration' - } - )} - - - - - ) : null} -
- - - - -

- - {i18n.translate( - 'xpack.apm.settings.agentConf.agentConfigDocsLinkLabel', - { defaultMessage: 'Learn more in our docs.' } - )} - - ) - }} - /> -

-
- - - - -
- - ); -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/StickyTransactionProperties.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/StickyTransactionProperties.tsx deleted file mode 100644 index 4a8796da631561..00000000000000 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/StickyTransactionProperties.tsx +++ /dev/null @@ -1,191 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { idx } from '@kbn/elastic-idx'; -import styled from 'styled-components'; -import { - TRANSACTION_DURATION, - TRANSACTION_RESULT, - URL_FULL, - USER_ID, - TRANSACTION_PAGE_URL -} from '../../../../../common/elasticsearch_fieldnames'; -import { NOT_AVAILABLE_LABEL } from '../../../../../common/i18n'; -import { Transaction } from '../../../../../typings/es_schemas/ui/Transaction'; -import { asTime } from '../../../../utils/formatters'; -import { - IStickyProperty, - StickyProperties -} from '../../../shared/StickyProperties'; -import { ErrorCountBadge } from './ErrorCountBadge'; -import { isRumAgentName } from '../../../../../common/agent_name'; -import { fontSize } from '../../../../style/variables'; -import { PercentOfParent } from './PercentOfParent'; - -interface Props { - transaction: Transaction; - totalDuration?: number; - errorCount: number; -} - -const ErrorTitle = styled.span` - font-size: ${fontSize}; -`; - -export function StickyTransactionProperties({ - transaction, - totalDuration, - errorCount -}: Props) { - const timestamp = transaction['@timestamp']; - - const isRumAgent = isRumAgentName(transaction.agent.name); - const { urlFieldName, urlValue } = isRumAgent - ? { - urlFieldName: TRANSACTION_PAGE_URL, - urlValue: idx(transaction, _ => _.transaction.page.url) - } - : { - urlFieldName: URL_FULL, - urlValue: idx(transaction, _ => _.url.full) - }; - - const duration = transaction.transaction.duration.us; - - const noErrorsText = i18n.translate( - 'xpack.apm.transactionDetails.errorsNone', - { - defaultMessage: 'None' - } - ); - - const stickyProperties: IStickyProperty[] = [ - { - label: i18n.translate('xpack.apm.transactionDetails.timestampLabel', { - defaultMessage: 'Timestamp' - }), - fieldName: '@timestamp', - val: timestamp, - truncated: true, - width: '50%' - }, - { - fieldName: urlFieldName, - label: 'URL', - val: urlValue || NOT_AVAILABLE_LABEL, - truncated: true, - width: '50%' - }, - { - label: i18n.translate('xpack.apm.transactionDetails.durationLabel', { - defaultMessage: 'Duration' - }), - fieldName: TRANSACTION_DURATION, - val: asTime(duration), - width: '25%' - }, - { - label: i18n.translate( - 'xpack.apm.transactionDetails.percentOfTraceLabel', - { - defaultMessage: '% of trace' - } - ), - val: ( - - ), - width: '25%' - }, - { - label: i18n.translate('xpack.apm.transactionDetails.resultLabel', { - defaultMessage: 'Result' - }), - fieldName: TRANSACTION_RESULT, - val: idx(transaction, _ => _.transaction.result) || NOT_AVAILABLE_LABEL, - width: '14%' - }, - { - label: i18n.translate( - 'xpack.apm.transactionDetails.errorsOverviewLabel', - { - defaultMessage: 'Errors' - } - ), - val: errorCount ? ( - <> - {errorCount} - -   - {i18n.translate('xpack.apm.transactionDetails.errorsOverviewLink', { - values: { errorCount }, - defaultMessage: - '{errorCount, plural, one {Related error} other {Related errors}}' - })} - - - ) : ( - noErrorsText - ), - width: '18%' - }, - { - label: i18n.translate('xpack.apm.transactionDetails.userIdLabel', { - defaultMessage: 'User ID' - }), - fieldName: USER_ID, - val: idx(transaction, _ => _.user.id) || NOT_AVAILABLE_LABEL, - truncated: true, - width: '18%' - } - ]; - - const { user_agent: userAgent } = transaction; - - if (userAgent) { - const { os, device } = userAgent; - const width = '25%'; - stickyProperties.push({ - label: i18n.translate('xpack.apm.transactionDetails.userAgentLabel', { - defaultMessage: 'User agent' - }), - val: [userAgent.name, userAgent.version].filter(Boolean).join(' '), - truncated: true, - width - }); - - if (os) { - stickyProperties.push({ - label: i18n.translate('xpack.apm.transactionDetails.userAgentOsLabel', { - defaultMessage: 'User agent OS' - }), - val: os.full || os.name, - truncated: true, - width - }); - } - - if (device) { - stickyProperties.push({ - label: i18n.translate( - 'xpack.apm.transactionDetails.userAgentDeviceLabel', - { - defaultMessage: 'User agent device' - } - ), - val: device.name, - width - }); - } - } - - return ; -} diff --git a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx index 43edd3b7eb5594..7e9171197251d5 100644 --- a/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/app/TransactionDetails/WaterfallWithSummmary/WaterfallContainer/Waterfall/SpanFlyout/index.tsx @@ -24,7 +24,7 @@ import styled from 'styled-components'; import { idx } from '@kbn/elastic-idx'; import { px, units } from '../../../../../../../style/variables'; import { Summary } from '../../../../../../shared/Summary'; -import { TimestampSummaryItem } from '../../../../../../shared/Summary/TimestampSummaryItem'; +import { TimestampTooltip } from '../../../../../../shared/TimestampTooltip'; import { DurationSummaryItem } from '../../../../../../shared/Summary/DurationSummaryItem'; import { Span } from '../../../../../../../../typings/es_schemas/ui/Span'; import { Transaction } from '../../../../../../../../typings/es_schemas/ui/Transaction'; @@ -143,7 +143,7 @@ export function SpanFlyout({ , + , { + clearCache(); refreshTimeRange({ rangeFrom: start, rangeTo: end }); }} onRefreshChange={onRefreshChange} diff --git a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js b/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js index f1d306c57ee8af..b6acb6904f865b 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js +++ b/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/StickyProperties.test.js @@ -15,11 +15,6 @@ describe('StickyProperties', () => { it('should render entire component', () => { const stickyProperties = [ - { - label: 'Timestamp', - fieldName: '@timestamp', - val: 1536405447640 - }, { fieldName: URL_FULL, label: 'URL', @@ -51,22 +46,6 @@ describe('StickyProperties', () => { }); describe('values', () => { - it('should render timestamp when fieldName is `@timestamp`', () => { - const stickyProperties = [ - { - label: 'My Timestamp', - fieldName: '@timestamp', - val: 1536405447640 - } - ]; - - const wrapper = shallow( - - ).find('TimestampValue'); - - expect(wrapper).toMatchSnapshot(); - }); - it('should render numbers', () => { const stickyProperties = [ { diff --git a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap b/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap index 860a7a3be7ddf7..020d5952c03e45 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap +++ b/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/__snapshots__/StickyProperties.test.js.snap @@ -20,35 +20,6 @@ exports[`StickyProperties should render entire component 1`] = ` "padding": "1em 1em 1em 0", } } - > - - - @timestamp - - } - delay="regular" - position="top" - > - - Timestamp - - - - - - `; - -exports[`StickyProperties values should render timestamp when fieldName is \`@timestamp\` 1`] = ` - -`; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/index.tsx b/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/index.tsx index 5354ca87a1b4f6..c0c7922c42ea24 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/index.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/StickyProperties/index.tsx @@ -7,10 +7,8 @@ import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui'; import { EuiToolTip } from '@elastic/eui'; import theme from '@elastic/eui/dist/eui_theme_light.json'; -import moment from 'moment'; import React from 'react'; import styled from 'styled-components'; -import { NOT_AVAILABLE_LABEL } from '../../../../common/i18n'; import { fontFamilyCode, fontSizes, @@ -42,10 +40,6 @@ const PropertyLabel = styled.div` `; PropertyLabel.displayName = 'PropertyLabel'; -const PropertyValueDimmed = styled.span` - color: ${theme.euiColorMediumShade}; -`; - const propertyValueLineHeight = 1.2; const PropertyValue = styled.div` display: inline-block; @@ -59,20 +53,6 @@ const PropertyValueTruncated = styled.span` ${truncate('100%')}; `; -function TimestampValue({ timestamp }: { timestamp: Date }) { - const time = moment(timestamp); - const timeAgo = timestamp ? time.fromNow() : NOT_AVAILABLE_LABEL; - const timestampFull = timestamp - ? time.format('MMMM Do YYYY, HH:mm:ss.SSS') - : NOT_AVAILABLE_LABEL; - - return ( - - {timeAgo} ({timestampFull}) - - ); -} - function getPropertyLabel({ fieldName, label }: Partial) { if (fieldName) { return ( @@ -92,10 +72,6 @@ function getPropertyValue({ fieldName, truncated = false }: Partial) { - if (fieldName === '@timestamp') { - return ; - } - if (truncated) { return ( diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TimestampSummaryItem.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TimestampSummaryItem.tsx deleted file mode 100644 index 8d619d94067cce..00000000000000 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TimestampSummaryItem.tsx +++ /dev/null @@ -1,26 +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; - * you may not use this file except in compliance with the Elastic License. - */ -import React from 'react'; -import { EuiToolTip } from '@elastic/eui'; -import moment from 'moment-timezone'; - -interface Props { - time: number; -} - -const TimestampSummaryItem = (props: Props) => { - const time = moment.tz(props.time, moment.tz.guess()); - const relativeTimeLabel = time.fromNow(); - const absoluteTimeLabel = time.format('MMM Do YYYY HH:mm:ss.SSS zz'); - - return ( - - <>{relativeTimeLabel} - - ); -}; - -export { TimestampSummaryItem }; diff --git a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx index 60ee39875fe37c..8f91b8cc5e2af1 100644 --- a/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx +++ b/x-pack/legacy/plugins/apm/public/components/shared/Summary/TransactionSummary.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { idx } from '@kbn/elastic-idx'; import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; import { Summary } from './'; -import { TimestampSummaryItem } from './TimestampSummaryItem'; +import { TimestampTooltip } from '../TimestampTooltip'; import { DurationSummaryItem } from './DurationSummaryItem'; import { ErrorCountSummaryItem } from './ErrorCountSummaryItem'; import { isRumAgentName } from '../../../../common/agent_name'; @@ -48,7 +48,7 @@ const TransactionSummary = ({ errorCount }: Props) => { const items = [ - , + , + <>{relativeTimeLabel} + + ); +} diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx index 4932992eca5e22..f30a818d048ffc 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.test.tsx @@ -52,7 +52,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: undefined, error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'loading' }); }); @@ -63,7 +63,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: undefined, error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'loading' }); }); @@ -75,7 +75,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: 'response from hook', error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'success' }); }); @@ -96,7 +96,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: undefined, error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'loading' }); }); @@ -107,7 +107,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: undefined, error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'loading' }); }); @@ -119,7 +119,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: undefined, error: expect.any(Error), - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'failure' }); }); @@ -142,7 +142,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: undefined, error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'loading' }); @@ -152,7 +152,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: 'first response', error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'success' }); @@ -171,7 +171,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: 'first response', error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'loading' }); @@ -182,7 +182,7 @@ describe('useFetcher', () => { expect(hook.result.current).toEqual({ data: 'second response', error: undefined, - refresh: expect.any(Function), + refetch: expect.any(Function), status: 'success' }); }); diff --git a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx index f5c726029ba194..d9fa73b209cb45 100644 --- a/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx +++ b/x-pack/legacy/plugins/apm/public/hooks/useFetcher.tsx @@ -31,7 +31,7 @@ export function useFetcher( options?: { preservePreviousData?: boolean; } -): Result & { refresh: () => void }; +): Result & { refetch: () => void }; // To avoid infinite rescursion when infering the type of `TState` `initialState` must be given if `prevResult` is consumed export function useFetcher( @@ -41,7 +41,7 @@ export function useFetcher( preservePreviousData?: boolean; initialState: TState; } -): Result & { refresh: () => void }; +): Result & { refetch: () => void }; export function useFetcher( fn: Function, @@ -146,14 +146,13 @@ export function useFetcher( /* eslint-enable react-hooks/exhaustive-deps */ ]); - return useMemo( - () => ({ + return useMemo(() => { + return { ...result, - refresh: () => { + refetch: () => { // this will invalidate the deps to `useEffect` and will result in a new request setCounter(count => count + 1); } - }), - [result] - ); + }; + }, [result]); } diff --git a/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts b/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts index f344452a81693a..9fb6c4d341825f 100644 --- a/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts +++ b/x-pack/legacy/plugins/apm/public/services/__test__/callApi.test.ts @@ -6,7 +6,7 @@ import * as kfetchModule from 'ui/kfetch'; import { mockNow } from '../../utils/testHelpers'; -import { _clearCache, callApi } from '../rest/callApi'; +import { clearCache, callApi } from '../rest/callApi'; import { SessionStorageMock } from './SessionStorageMock'; jest.mock('ui/kfetch'); @@ -24,7 +24,7 @@ describe('callApi', () => { afterEach(() => { kfetchSpy.mockClear(); - _clearCache(); + clearCache(); }); describe('apm_debug', () => { diff --git a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts index 27842a19a322af..3cae0604e52fbb 100644 --- a/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts +++ b/x-pack/legacy/plugins/apm/public/services/rest/callApi.ts @@ -31,12 +31,12 @@ function fetchOptionsWithDebug(fetchOptions: KFetchOptions) { const cache = new LRU({ max: 100, maxAge: 1000 * 60 * 60 }); -export function _clearCache() { +export function clearCache() { cache.reset(); } export async function callApi( - fetchOptions: KFetchOptions, + fetchOptions: KFetchOptions & { forceCache?: boolean }, options?: KFetchKibanaOptions ): Promise { const cacheKey = getCacheKey(fetchOptions); @@ -57,7 +57,11 @@ export async function callApi( // only cache items that has a time range with `start` and `end` params, // and where `end` is not a timestamp in the future -function isCachable(fetchOptions: KFetchOptions) { +function isCachable(fetchOptions: KFetchOptions & { forceCache?: boolean }) { + if (fetchOptions.forceCache) { + return true; + } + if ( !(fetchOptions.query && fetchOptions.query.start && fetchOptions.query.end) ) { diff --git a/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap index ddd800dc22bf31..71217ccc36f7b6 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/services/__snapshots__/queries.test.ts.snap @@ -110,7 +110,7 @@ Object { "myIndex", "myIndex", ], - "terminate_after": 1, + "terminateAfter": 1, } `; diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts index ebe0ba9827b534..3c6f6843899702 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_service_agent_name.ts @@ -9,18 +9,14 @@ import { SERVICE_AGENT_NAME, SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../typings/common'; import { rangeFilter } from '../helpers/range_filter'; import { Setup } from '../helpers/setup_request'; -export type ServiceAgentNameAPIResponse = PromiseReturnType< - typeof getServiceAgentName ->; export async function getServiceAgentName(serviceName: string, setup: Setup) { const { start, end, client, config } = setup; const params = { - terminate_after: 1, + terminateAfter: 1, index: [ config.get('apm_oss.errorIndices'), config.get('apm_oss.transactionIndices'), diff --git a/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts b/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts index c8053c57776db8..a1f035da9dc1ac 100644 --- a/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts +++ b/x-pack/legacy/plugins/apm/server/lib/services/get_service_transaction_types.ts @@ -9,13 +9,9 @@ import { SERVICE_NAME, TRANSACTION_TYPE } from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../typings/common'; import { rangeFilter } from '../helpers/range_filter'; import { Setup } from '../helpers/setup_request'; -export type ServiceTransactionTypesAPIResponse = PromiseReturnType< - typeof getServiceTransactionTypes ->; export async function getServiceTransactionTypes( serviceName: string, setup: Setup diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap index 4c53563aa41dfb..d64907691ef9ae 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/__snapshots__/queries.test.ts.snap @@ -7,7 +7,6 @@ Object { "environments": Object { "terms": Object { "field": "service.environment", - "missing": "ENVIRONMENT_NOT_DEFINED", "size": 100, }, }, @@ -53,21 +52,47 @@ Object { "body": Object { "query": Object { "bool": Object { - "filter": Array [ + "minimum_should_match": 2, + "should": Array [ Object { "term": Object { - "service.name": "foo", + "service.name": Object { + "value": "foo", + }, }, }, Object { "term": Object { - "service.environment": "bar", + "service.environment": Object { + "value": "bar", + }, + }, + }, + Object { + "bool": Object { + "must_not": Array [ + Object { + "exists": Object { + "field": "service.name", + }, + }, + ], + }, + }, + Object { + "bool": Object { + "must_not": Array [ + Object { + "exists": Object { + "field": "service.environment", + }, + }, + ], }, }, ], }, }, - "size": 1, }, "index": "myIndex", } @@ -78,25 +103,40 @@ Object { "body": Object { "query": Object { "bool": Object { - "filter": Array [ + "minimum_should_match": 2, + "should": Array [ Object { "term": Object { - "service.name": "foo", + "service.name": Object { + "value": "foo", + }, }, }, Object { "bool": Object { - "must_not": Object { - "exists": Object { - "field": "service.environment", + "must_not": Array [ + Object { + "exists": Object { + "field": "service.name", + }, }, - }, + ], + }, + }, + Object { + "bool": Object { + "must_not": Array [ + Object { + "exists": Object { + "field": "service.environment", + }, + }, + ], }, }, ], }, }, - "size": 1, }, "index": "myIndex", } @@ -109,7 +149,7 @@ Object { "services": Object { "terms": Object { "field": "service.name", - "size": 100, + "size": 50, }, }, }, @@ -145,8 +185,8 @@ Object { "environments": Object { "terms": Object { "field": "service.environment", - "missing": "ENVIRONMENT_NOT_DEFINED", - "size": 100, + "missing": "ALL_OPTION_VALUE", + "size": 50, }, }, }, diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts index 9ef8c62472d1dc..ea8f50c90c1d3a 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/configuration_types.d.ts @@ -4,16 +4,18 @@ * you may not use this file except in compliance with the Elastic License. */ -export interface AgentConfigurationIntake { - settings: { - transaction_sample_rate: number; - }; +export interface AgentConfiguration { + '@timestamp': number; + applied_by_agent?: boolean; + etag?: string; + agent_name?: string; service: { - name: string; + name?: string; environment?: string; }; -} - -export interface AgentConfiguration extends AgentConfigurationIntake { - '@timestamp': number; + settings: { + transaction_sample_rate?: number; + capture_body?: string; + transaction_max_spans?: number; + }; } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts index b1b0faa07e49fb..4226c073b1de02 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_agent_config_index.ts @@ -5,7 +5,7 @@ */ import { InternalCoreSetup } from 'src/core/server'; -import Boom from 'boom'; +import { CallCluster } from '../../../../../../../../src/legacy/core_plugins/elasticsearch'; export async function createApmAgentConfigurationIndex( core: InternalCoreSetup @@ -19,57 +19,84 @@ export async function createApmAgentConfigurationIndex( 'admin' ); const indexExists = await callWithInternalUser('indices.exists', { index }); + const result = indexExists + ? await updateExistingIndex(index, callWithInternalUser) + : await createNewIndex(index, callWithInternalUser); - if (!indexExists) { - const result = await callWithInternalUser('indices.create', { - index, - body: { - settings: { - 'index.auto_expand_replicas': '0-1' - }, - mappings: { - properties: { - '@timestamp': { - type: 'date' - }, - settings: { - properties: { - transaction_sample_rate: { - type: 'scaled_float', - scaling_factor: 1000, - ignore_malformed: true, - coerce: false - } - } - }, - service: { - properties: { - name: { - type: 'keyword', - ignore_above: 1024 - }, - environment: { - type: 'keyword', - ignore_above: 1024 - } - } - } - } - } - } - }); - - if (!result.acknowledged) { - const err = new Error( - `Unable to create APM Agent Configuration index '${index}'` - ); - // eslint-disable-next-line - console.error(err.stack); - throw Boom.boomify(err, { statusCode: 500 }); - } + if (!result.acknowledged) { + const resultError = + result && result.error && JSON.stringify(result.error); + throw new Error( + `Unable to create APM Agent Configuration index '${index}': ${resultError}` + ); } } catch (e) { // eslint-disable-next-line no-console console.error('Could not create APM Agent configuration:', e.message); } } + +function createNewIndex(index: string, callWithInternalUser: CallCluster) { + return callWithInternalUser('indices.create', { + index, + body: { + settings: { 'index.auto_expand_replicas': '0-1' }, + mappings: { properties: mappingProperties } + } + }); +} + +// Necessary for migration reasons +// Added in 7.5: `capture_body`, `transaction_max_spans`, `applied_by_agent`, `agent_name` and `etag` +function updateExistingIndex(index: string, callWithInternalUser: CallCluster) { + return callWithInternalUser('indices.putMapping', { + index, + body: { properties: mappingProperties } + }); +} + +const mappingProperties = { + '@timestamp': { + type: 'date' + }, + service: { + properties: { + name: { + type: 'keyword', + ignore_above: 1024 + }, + environment: { + type: 'keyword', + ignore_above: 1024 + } + } + }, + settings: { + properties: { + transaction_sample_rate: { + type: 'scaled_float', + scaling_factor: 1000, + ignore_malformed: true, + coerce: false + }, + capture_body: { + type: 'keyword', + ignore_above: 1024 + }, + transaction_max_spans: { + type: 'short' + } + } + }, + applied_by_agent: { + type: 'boolean' + }, + agent_name: { + type: 'keyword', + ignore_above: 1024 + }, + etag: { + type: 'keyword', + ignore_above: 1024 + } +}; diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts new file mode 100644 index 00000000000000..6450040098cd40 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_or_update_configuration.ts @@ -0,0 +1,49 @@ +/* + * 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 hash from 'object-hash'; +import { IndexDocumentParams } from 'elasticsearch'; +import { Setup } from '../../helpers/setup_request'; +import { AgentConfiguration } from './configuration_types'; + +export async function createOrUpdateConfiguration({ + configurationId, + configuration, + setup +}: { + configurationId?: string; + configuration: Omit< + AgentConfiguration, + '@timestamp' | 'applied_by_agent' | 'etag' + >; + setup: Setup; +}) { + const { client, config } = setup; + + const params: IndexDocumentParams = { + type: '_doc', + refresh: true, + index: config.get('apm_oss.apmAgentConfigurationIndex'), + body: { + agent_name: configuration.agent_name, + service: { + name: configuration.service.name, + environment: configuration.service.environment + }, + settings: configuration.settings, + '@timestamp': Date.now(), + applied_by_agent: false, + etag: hash(configuration) + } + }; + + // by specifying an id elasticsearch will delete the previous doc and insert the updated doc + if (configurationId) { + params.id = configurationId; + } + + return client.index(params); +} diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts new file mode 100644 index 00000000000000..a18873e86c5a8e --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_agent_name_by_service.ts @@ -0,0 +1,54 @@ +/* + * 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 { idx } from '@kbn/elastic-idx'; +import { Setup } from '../../helpers/setup_request'; +import { + PROCESSOR_EVENT, + SERVICE_NAME +} from '../../../../common/elasticsearch_fieldnames'; +import { SERVICE_AGENT_NAME } from '../../../../common/elasticsearch_fieldnames'; + +export async function getAgentNameByService({ + serviceName, + setup +}: { + serviceName: string; + setup: Setup; +}) { + const { client, config } = setup; + + const params = { + terminateAfter: 1, + index: [ + config.get('apm_oss.metricsIndices'), + config.get('apm_oss.errorIndices'), + config.get('apm_oss.transactionIndices') + ], + body: { + size: 0, + query: { + bool: { + filter: [ + { + terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } + }, + { term: { [SERVICE_NAME]: serviceName } } + ] + } + }, + aggs: { + agent_names: { + terms: { field: SERVICE_AGENT_NAME, size: 1 } + } + } + } + }; + + const { aggregations } = await client.search(params); + const agentName = idx(aggregations, _ => _.agent_names.buckets[0].key); + return { agentName }; +} diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_all_environments.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_all_environments.ts index 0b27e036d29674..76ebf75aada290 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_all_environments.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_all_environments.ts @@ -11,17 +11,22 @@ import { SERVICE_NAME, SERVICE_ENVIRONMENT } from '../../../../../common/elasticsearch_fieldnames'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; +import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration_constants'; export async function getAllEnvironments({ serviceName, setup }: { - serviceName: string; + serviceName: string | undefined; setup: Setup; }) { const { client, config } = setup; + // omit filter for service.name if "All" option is selected + const serviceNameFilter = serviceName + ? [{ term: { [SERVICE_NAME]: serviceName } }] + : []; + const params = { index: [ config.get('apm_oss.metricsIndices'), @@ -36,7 +41,7 @@ export async function getAllEnvironments({ { terms: { [PROCESSOR_EVENT]: ['transaction', 'error', 'metric'] } }, - { term: { [SERVICE_NAME]: serviceName } } + ...serviceNameFilter ] } }, @@ -44,7 +49,6 @@ export async function getAllEnvironments({ environments: { terms: { field: SERVICE_ENVIRONMENT, - missing: ENVIRONMENT_NOT_DEFINED, size: 100 } } @@ -54,5 +58,6 @@ export async function getAllEnvironments({ const resp = await client.search(params); const buckets = idx(resp.aggregations, _ => _.environments.buckets) || []; - return buckets.map(bucket => bucket.key); + const environments = buckets.map(bucket => bucket.key); + return [ALL_OPTION_VALUE, ...environments]; } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_unavailable_environments.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts similarity index 69% rename from x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_unavailable_environments.ts rename to x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts index fae12549d4dbdb..120cc62cc3bc99 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_unavailable_environments.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/get_existing_environments_for_service.ts @@ -10,32 +10,32 @@ import { SERVICE_NAME, SERVICE_ENVIRONMENT } from '../../../../../common/elasticsearch_fieldnames'; -import { ENVIRONMENT_NOT_DEFINED } from '../../../../../common/environment_filter_values'; +import { ALL_OPTION_VALUE } from '../../../../../common/agent_configuration_constants'; -export async function getUnavailableEnvironments({ +export async function getExistingEnvironmentsForService({ serviceName, setup }: { - serviceName: string; + serviceName: string | undefined; setup: Setup; }) { const { client, config } = setup; + const bool = serviceName + ? { filter: [{ term: { [SERVICE_NAME]: serviceName } }] } + : { must_not: [{ exists: { field: SERVICE_NAME } }] }; + const params = { index: config.get('apm_oss.apmAgentConfigurationIndex'), body: { size: 0, - query: { - bool: { - filter: [{ term: { [SERVICE_NAME]: serviceName } }] - } - }, + query: { bool }, aggs: { environments: { terms: { field: SERVICE_ENVIRONMENT, - missing: ENVIRONMENT_NOT_DEFINED, - size: 100 + missing: ALL_OPTION_VALUE, + size: 50 } } } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts index e4f986ed4184ce..c05b4e113deb51 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_environments/index.ts @@ -7,7 +7,7 @@ import { getAllEnvironments } from './get_all_environments'; import { Setup } from '../../../helpers/setup_request'; import { PromiseReturnType } from '../../../../../typings/common'; -import { getUnavailableEnvironments } from './get_unavailable_environments'; +import { getExistingEnvironmentsForService } from './get_existing_environments_for_service'; export type AgentConfigurationEnvironmentsAPIResponse = PromiseReturnType< typeof getEnvironments @@ -17,18 +17,18 @@ export async function getEnvironments({ serviceName, setup }: { - serviceName: string; + serviceName: string | undefined; setup: Setup; }) { - const [allEnvironments, unavailableEnvironments] = await Promise.all([ + const [allEnvironments, existingEnvironments] = await Promise.all([ getAllEnvironments({ serviceName, setup }), - getUnavailableEnvironments({ serviceName, setup }) + getExistingEnvironmentsForService({ serviceName, setup }) ]); return allEnvironments.map(environment => { return { name: environment, - available: !unavailableEnvironments.includes(environment) + alreadyConfigured: existingEnvironments.includes(environment) }; }); } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts index e84334cb7db566..55af96acbc7192 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/get_service_names.ts @@ -11,6 +11,7 @@ import { PROCESSOR_EVENT, SERVICE_NAME } from '../../../../common/elasticsearch_fieldnames'; +import { ALL_OPTION_VALUE } from '../../../../common/agent_configuration_constants'; export type AgentConfigurationServicesAPIResponse = PromiseReturnType< typeof getServiceNames @@ -37,7 +38,7 @@ export async function getServiceNames({ setup }: { setup: Setup }) { services: { terms: { field: SERVICE_NAME, - size: 100 + size: 50 } } } @@ -46,5 +47,6 @@ export async function getServiceNames({ setup }: { setup: Setup }) { const resp = await client.search(params); const buckets = idx(resp.aggregations, _ => _.services.buckets) || []; - return buckets.map(bucket => bucket.key).sort(); + const serviceNames = buckets.map(bucket => bucket.key).sort(); + return [ALL_OPTION_VALUE, ...serviceNames]; } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_configuration.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts similarity index 53% rename from x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_configuration.ts rename to x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts index 42d324dda82f25..ea2b15e6985d5a 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/create_configuration.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/mark_applied_by_agent.ts @@ -5,30 +5,28 @@ */ import { Setup } from '../../helpers/setup_request'; -import { PromiseReturnType } from '../../../../typings/common'; -import { AgentConfigurationIntake } from './configuration_types'; +import { AgentConfiguration } from './configuration_types'; -export type CreateAgentConfigurationAPIResponse = PromiseReturnType< - typeof createConfiguration ->; -export async function createConfiguration({ - configuration, +export async function markAppliedByAgent({ + id, + body, setup }: { - configuration: AgentConfigurationIntake; + id: string; + body: AgentConfiguration; setup: Setup; }) { const { client, config } = setup; const params = { type: '_doc', - refresh: true, index: config.get('apm_oss.apmAgentConfigurationIndex'), + id, // by specifying the `id` elasticsearch will do an "upsert" body: { - '@timestamp': Date.now(), - ...configuration + ...body, + applied_by_agent: true } }; - return client.index(params); + return client.index(params); } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts index 768651c8acec73..31b0937a1b9576 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/queries.test.ts @@ -5,7 +5,7 @@ */ import { getAllEnvironments } from './get_environments/get_all_environments'; -import { getUnavailableEnvironments } from './get_environments/get_unavailable_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'; @@ -34,7 +34,7 @@ describe('agent configuration queries', () => { it('fetches unavailable environments', async () => { mock = await inspectSearchParams(setup => - getUnavailableEnvironments({ + getExistingEnvironmentsForService({ serviceName: 'foo', setup }) diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts new file mode 100644 index 00000000000000..982077e2e66655 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.mocks.ts @@ -0,0 +1,74 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License; + * you may not use this file except in compliance with the Elastic License. + */ + +export const searchMocks = { + took: 1, + timed_out: false, + _shards: { + total: 1, + successful: 1, + skipped: 0, + failed: 0 + }, + hits: { + total: { + value: 3, + relation: 'eq' + }, + max_score: 0.9808292, + hits: [ + { + _index: '.apm-agent-configuration', + _type: '_doc', + _id: '-aQHsm0BxZLczArvNQYW', + _score: 0.9808292, + _source: { + service: { + environment: 'production' + }, + settings: { + transaction_sample_rate: 0.3 + }, + '@timestamp': 1570649879829, + applied_by_agent: false, + etag: 'c511f4c1df457371c4446c9c4925662e18726f51' + } + }, + { + _index: '.apm-agent-configuration', + _type: '_doc', + _id: '-KQHsm0BxZLczArvNAb0', + _score: 0.18232156, + _source: { + service: { + name: 'my_service' + }, + settings: { + transaction_sample_rate: 0.2 + }, + '@timestamp': 1570649879795, + applied_by_agent: false, + etag: 'a13cd8fee5a2fcc2ae773a60a4deaf7f76b90a65' + } + }, + { + _index: '.apm-agent-configuration', + _type: '_doc', + _id: '96QHsm0BxZLczArvNAbD', + _score: 0.0, + _source: { + service: {}, + settings: { + transaction_sample_rate: 0.1 + }, + '@timestamp': 1570649879743, + applied_by_agent: false, + etag: 'c7f4ba16f00a9c9bf3c49024c5b6d4632ff05ff5' + } + } + ] + } +}; diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts new file mode 100644 index 00000000000000..e8db37891e7ae5 --- /dev/null +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.test.ts @@ -0,0 +1,39 @@ +/* + * 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 { searchConfigurations } from './search'; +import { searchMocks } from './search.mocks'; +import { Setup } from '../../helpers/setup_request'; + +describe('search configurations', () => { + it('should return configuration by matching on service.name', async () => { + const res = await searchConfigurations({ + serviceName: 'my_service', + environment: 'production', + setup: ({ + config: { get: () => '' }, + client: { search: async () => searchMocks } + } as unknown) as Setup + }); + + expect(res!._source.service).toEqual({ name: 'my_service' }); + expect(res!._source.settings).toEqual({ transaction_sample_rate: 0.2 }); + }); + + it('should return configuration by matching on "production" env', async () => { + const res = await searchConfigurations({ + serviceName: 'non_existing_service', + environment: 'production', + setup: ({ + config: { get: () => '' }, + client: { search: async () => searchMocks } + } as unknown) as Setup + }); + + expect(res!._source.service).toEqual({ environment: 'production' }); + expect(res!._source.settings).toEqual({ transaction_sample_rate: 0.3 }); + }); +}); diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts index 0cf6987112b0f2..664bcb9325472e 100644 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts +++ b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/search.ts @@ -4,8 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -import { ESFilter } from 'elasticsearch'; -import { PromiseReturnType } from '../../../../typings/common'; +import { ESSearchHit } from 'elasticsearch'; import { SERVICE_NAME, SERVICE_ENVIRONMENT @@ -13,9 +12,6 @@ import { import { Setup } from '../../helpers/setup_request'; import { AgentConfiguration } from './configuration_types'; -export type SearchAgentConfigurationsAPIResponse = PromiseReturnType< - typeof searchConfigurations ->; export async function searchConfigurations({ serviceName, environment, @@ -27,26 +23,61 @@ export async function searchConfigurations({ }) { const { client, config } = setup; - const filters: ESFilter[] = [{ term: { [SERVICE_NAME]: serviceName } }]; + // sorting order + // 1. exact match: service.name AND service.environment (eg. opbeans-node / production) + // 2. Partial match: service.name and no service.environment (eg. opbeans-node / All) + // 3. Partial match: service.environment and no service.name (eg. All / production) + // 4. Catch all: no service.name and no service.environment (eg. All / All) - if (environment) { - filters.push({ term: { [SERVICE_ENVIRONMENT]: environment } }); - } else { - filters.push({ - bool: { must_not: { exists: { field: SERVICE_ENVIRONMENT } } } - }); - } + const environmentFilter = environment + ? [{ term: { [SERVICE_ENVIRONMENT]: { value: environment } } }] + : []; const params = { index: config.get('apm_oss.apmAgentConfigurationIndex'), body: { - size: 1, query: { - bool: { filter: filters } + bool: { + minimum_should_match: 2, + should: [ + { term: { [SERVICE_NAME]: { value: serviceName } } }, + ...environmentFilter, + { bool: { must_not: [{ exists: { field: SERVICE_NAME } }] } }, + { bool: { must_not: [{ exists: { field: SERVICE_ENVIRONMENT } }] } } + ] + } } } }; const resp = await client.search(params); - return resp.hits.hits[0]; + const { hits } = resp.hits; + + const exactMatch = hits.find( + hit => + hit._source.service.name === serviceName && + hit._source.service.environment === environment + ); + + if (exactMatch) { + return exactMatch; + } + + const matchWithServiceName = hits.find( + hit => hit._source.service.name === serviceName + ); + + if (matchWithServiceName) { + return matchWithServiceName; + } + + const matchWithEnvironment = hits.find( + hit => hit._source.service.environment === environment + ); + + if (matchWithEnvironment) { + return matchWithEnvironment; + } + + return resp.hits.hits[0] as ESSearchHit | undefined; } diff --git a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/update_configuration.ts b/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/update_configuration.ts deleted file mode 100644 index ad7c789efa94cd..00000000000000 --- a/x-pack/legacy/plugins/apm/server/lib/settings/agent_configuration/update_configuration.ts +++ /dev/null @@ -1,37 +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; - * you may not use this file except in compliance with the Elastic License. - */ - -import { Setup } from '../../helpers/setup_request'; -import { PromiseReturnType } from '../../../../typings/common'; -import { AgentConfigurationIntake } from './configuration_types'; - -export type UpdateAgentConfigurationAPIResponse = PromiseReturnType< - typeof updateConfiguration ->; -export async function updateConfiguration({ - configurationId, - configuration, - setup -}: { - configurationId: string; - configuration: AgentConfigurationIntake; - setup: Setup; -}) { - const { client, config } = setup; - - const params = { - type: '_doc', - id: configurationId, - refresh: true, - index: config.get('apm_oss.apmAgentConfigurationIndex'), - body: { - '@timestamp': Date.now(), - ...configuration - } - }; - - return client.index(params); -} diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts index b15f4bf677927d..c440ee9c1ecbee 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/avg_duration_by_country/index.ts @@ -11,14 +11,9 @@ import { TRANSACTION_DURATION, TRANSACTION_TYPE } from '../../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../../typings/common'; import { Setup } from '../../helpers/setup_request'; import { rangeFilter } from '../../helpers/range_filter'; -export type TransactionAvgDurationByCountryAPIResponse = PromiseReturnType< - typeof getTransactionAvgDurationByCountry ->; - export async function getTransactionAvgDurationByCountry({ setup, serviceName diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts index 1d120e82bc6a2e..b3c1c6603f3157 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/breakdown/index.ts @@ -16,16 +16,11 @@ import { TRANSACTION_BREAKDOWN_COUNT, PROCESSOR_EVENT } from '../../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../../typings/common'; import { Setup } from '../../helpers/setup_request'; import { rangeFilter } from '../../helpers/range_filter'; import { getMetricsDateHistogramParams } from '../../helpers/metrics'; import { MAX_KPIS, COLORS } from './constants'; -export type TransactionBreakdownAPIResponse = PromiseReturnType< - typeof getTransactionBreakdown ->; - export async function getTransactionBreakdown({ setup, serviceName, diff --git a/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts b/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts index 60d9a38b5e662e..cdaddc3af3e956 100644 --- a/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts +++ b/x-pack/legacy/plugins/apm/server/lib/transactions/get_transaction/index.ts @@ -10,12 +10,10 @@ import { TRACE_ID, TRANSACTION_ID } from '../../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../../typings/common'; import { Transaction } from '../../../../typings/es_schemas/ui/Transaction'; import { rangeFilter } from '../../helpers/range_filter'; import { Setup } from '../../helpers/setup_request'; -export type TransactionAPIResponse = PromiseReturnType; export async function getTransaction( transactionId: string, traceId: string, diff --git a/x-pack/legacy/plugins/apm/server/lib/ui_filters/get_environments.ts b/x-pack/legacy/plugins/apm/server/lib/ui_filters/get_environments.ts index 93a82bf6db85bd..3b48bfc7a98697 100644 --- a/x-pack/legacy/plugins/apm/server/lib/ui_filters/get_environments.ts +++ b/x-pack/legacy/plugins/apm/server/lib/ui_filters/get_environments.ts @@ -11,14 +11,10 @@ import { SERVICE_ENVIRONMENT, SERVICE_NAME } from '../../../common/elasticsearch_fieldnames'; -import { PromiseReturnType } from '../../../typings/common'; import { rangeFilter } from '../helpers/range_filter'; import { Setup } from '../helpers/setup_request'; import { ENVIRONMENT_NOT_DEFINED } from '../../../common/environment_filter_values'; -export type EnvironmentUIFilterAPIResponse = PromiseReturnType< - typeof getEnvironments ->; export async function getEnvironments(setup: Setup, serviceName?: string) { const { start, end, client, config } = setup; diff --git a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts index 92cc0503cb005d..682ebf27207c42 100644 --- a/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts +++ b/x-pack/legacy/plugins/apm/server/routes/create_apm_api.ts @@ -23,7 +23,8 @@ import { deleteAgentConfigurationRoute, listAgentConfigurationEnvironmentsRoute, listAgentConfigurationServicesRoute, - updateAgentConfigurationRoute + updateAgentConfigurationRoute, + agentConfigurationAgentNameRoute } from './settings'; import { metricsChartsRoute } from './metrics'; import { serviceNodesRoute } from './service_nodes'; @@ -50,14 +51,22 @@ import { serviceMapRoute } from './services'; const createApmApi = () => { const api = createApi() + // index pattern .add(indexPatternRoute) + + // Errors .add(errorDistributionRoute) .add(errorGroupsRoute) .add(errorsRoute) + + // Services .add(serviceAgentNameRoute) .add(serviceTransactionTypesRoute) .add(servicesRoute) .add(serviceNodeMetadataRoute) + + // Agent configuration + .add(agentConfigurationAgentNameRoute) .add(agentConfigurationRoute) .add(agentConfigurationSearchRoute) .add(createAgentConfigurationRoute) @@ -65,15 +74,23 @@ const createApmApi = () => { .add(listAgentConfigurationEnvironmentsRoute) .add(listAgentConfigurationServicesRoute) .add(updateAgentConfigurationRoute) + + // Metrics .add(metricsChartsRoute) .add(serviceNodesRoute) + + // Traces .add(tracesRoute) .add(tracesByIdRoute) + + // Transaction groups .add(transactionGroupsBreakdownRoute) .add(transactionGroupsChartsRoute) .add(transactionGroupsDistributionRoute) .add(transactionGroupsRoute) .add(transactionGroupsAvgDurationByCountry) + + // UI filters .add(errorGroupsLocalFiltersRoute) .add(metricsLocalFiltersRoute) .add(servicesLocalFiltersRoute) diff --git a/x-pack/legacy/plugins/apm/server/routes/settings.ts b/x-pack/legacy/plugins/apm/server/routes/settings.ts index 2c4a151a986210..8956e7add09e2d 100644 --- a/x-pack/legacy/plugins/apm/server/routes/settings.ts +++ b/x-pack/legacy/plugins/apm/server/routes/settings.ts @@ -7,14 +7,16 @@ import * as t from 'io-ts'; import { setupRequest } from '../lib/helpers/setup_request'; import { getServiceNames } from '../lib/settings/agent_configuration/get_service_names'; -import { createConfiguration } from '../lib/settings/agent_configuration/create_configuration'; -import { updateConfiguration } from '../lib/settings/agent_configuration/update_configuration'; +import { createOrUpdateConfiguration } from '../lib/settings/agent_configuration/create_or_update_configuration'; import { searchConfigurations } from '../lib/settings/agent_configuration/search'; 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'; // get list of configurations export const agentConfigurationRoute = createRoute(core => ({ @@ -56,36 +58,46 @@ export const listAgentConfigurationServicesRoute = createRoute(() => ({ } })); -const agentPayloadRt = t.type({ - settings: t.type({ - transaction_sample_rate: transactionSampleRateRt +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 }) + ]) }), - service: t.intersection([ - t.type({ - name: t.string - }), - t.partial({ - environments: t.array(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/services/{serviceName}/environments', + path: '/api/apm/settings/agent-configuration/environments', params: { - path: t.type({ - serviceName: t.string - }) + query: t.partial({ serviceName: t.string }) }, - handler: async (req, { path }) => { + handler: async (req, { query }) => { const setup = await setupRequest(req); - const { serviceName } = path; - return await getEnvironments({ - serviceName, - setup - }); + const { serviceName } = query; + return await getEnvironments({ serviceName, setup }); + } +})); + +// get agentName for service +export const agentConfigurationAgentNameRoute = createRoute(() => ({ + path: '/api/apm/settings/agent-configuration/agent_name', + params: { + query: t.type({ serviceName: t.string }) + }, + handler: async (req, { query }) => { + const setup = await setupRequest(req); + const { serviceName } = query; + return await getAgentNameByService({ serviceName, setup }); } })); @@ -97,16 +109,13 @@ export const createAgentConfigurationRoute = createRoute(() => ({ }, handler: async (req, { body }) => { const setup = await setupRequest(req); - return await createConfiguration({ - configuration: body, - setup - }); + return await createOrUpdateConfiguration({ configuration: body, setup }); } })); export const updateAgentConfigurationRoute = createRoute(() => ({ method: 'PUT', - path: `/api/apm/settings/agent-configuration/{configurationId}`, + path: '/api/apm/settings/agent-configuration/{configurationId}', params: { path: t.type({ configurationId: t.string @@ -116,7 +125,7 @@ export const updateAgentConfigurationRoute = createRoute(() => ({ handler: async (req, { path, body }) => { const setup = await setupRequest(req); const { configurationId } = path; - return await updateConfiguration({ + return await createOrUpdateConfiguration({ configurationId, configuration: body, setup @@ -124,7 +133,7 @@ export const updateAgentConfigurationRoute = createRoute(() => ({ } })); -// Lookup single configuration +// Lookup single configuration (used by APM Server) export const agentConfigurationSearchRoute = createRoute(core => ({ method: 'POST', path: '/api/apm/settings/agent-configuration/search', @@ -133,7 +142,8 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ service: t.intersection([ t.type({ name: t.string }), t.partial({ environment: t.string }) - ]) + ]), + etag: t.string }) }, handler: async (req, { body }, h) => { @@ -148,6 +158,11 @@ export const agentConfigurationSearchRoute = createRoute(core => ({ return h.response().code(404); } + // update `applied_by_agent` field if etags match + if (body.etag === config._source.etag && !config._source.applied_by_agent) { + markAppliedByAgent({ id: config._id, body: config._source, setup }); + } + return config; } })); diff --git a/x-pack/legacy/plugins/apm/server/routes/typings.ts b/x-pack/legacy/plugins/apm/server/routes/typings.ts index 12bcb3e414d84a..a82e8c317902f3 100644 --- a/x-pack/legacy/plugins/apm/server/routes/typings.ts +++ b/x-pack/legacy/plugins/apm/server/routes/typings.ts @@ -115,6 +115,7 @@ export type Client = < : undefined >( options: Omit & { + forceCache?: boolean; pathname: TPath; } & (TMethod extends 'GET' ? { method?: TMethod } : { method: TMethod }) & // Makes sure params can only be set when types were defined diff --git a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts index f2e84be6661177..c1a8a7f9b39852 100644 --- a/x-pack/legacy/plugins/apm/typings/elasticsearch.ts +++ b/x-pack/legacy/plugins/apm/typings/elasticsearch.ts @@ -6,15 +6,11 @@ import { StringMap, IndexAsString } from './common'; -export interface BoolQuery { - must_not: Array>; - should: Array>; - filter: Array>; -} - declare module 'elasticsearch' { // extending SearchResponse to be able to have typed aggregations + type ESSearchHit = SearchResponse['hits']['hits'][0]; + type AggregationType = | 'date_histogram' | 'histogram' diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index 434597c1e857af..8bc861e5823d0b 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -3420,14 +3420,9 @@ "xpack.apm.transactionActionMenu.showTraceLogsLinkLabel": "トレースログを表示", "xpack.apm.transactionActionMenu.viewInUptime": "監視ステータスを表示", "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "サンプルドキュメントを表示", - "xpack.apm.transactionDetails.durationLabel": "期間", - "xpack.apm.transactionDetails.errorsNone": "なし", - "xpack.apm.transactionDetails.errorsOverviewLabel": "エラー", - "xpack.apm.transactionDetails.errorsOverviewLink": "{errorCount, plural, one {関連エラー} other {関連エラー}}", "xpack.apm.transactionDetails.errorsOverviewLinkTooltip": "{errorCount, plural, one {1 件の関連エラーを表示} other {# 件の関連エラーを表示}}", "xpack.apm.transactionDetails.notFoundLabel": "トランザクションが見つかりませんでした。", "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "トレースの親が見つかりませんでした", - "xpack.apm.transactionDetails.percentOfTraceLabel": "トレースの %", "xpack.apm.transactionDetails.resultLabel": "結果", "xpack.apm.transactionDetails.serviceLabel": "サービス", "xpack.apm.transactionDetails.servicesTitle": "サービス", @@ -3437,7 +3432,6 @@ "xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel": "ナビゲーションタイミング", "xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel": "スタックトレース", "xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel": "ディスカバリでスパンを表示", - "xpack.apm.transactionDetails.timestampLabel": "タイムスタンプ", "xpack.apm.transactionDetails.transactionLabel": "トランザクション", "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip": "このバケットに利用可能なサンプルがありません", "xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel": "{transCount, plural, =0 {# request} 1 {# 件のリクエスト} other {# 件のリクエスト}}", @@ -3449,7 +3443,6 @@ "xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage": "このトランザクションを報告した APM エージェントが、構成に基づき {dropped} 個以上のスパンをドロップしました。", "xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText": "ドロップされたスパンの詳細。", "xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle": "トランザクションの詳細", - "xpack.apm.transactionDetails.userIdLabel": "ユーザー ID", "xpack.apm.transactionDetails.viewFullTraceButtonLabel": "完全なトレースを表示", "xpack.apm.transactionDetails.viewingFullTraceButtonTooltip": "現在完全なトレースが表示されています", "xpack.apm.transactions.chart.95thPercentileLabel": "95 パーセンタイル", @@ -3490,10 +3483,6 @@ "xpack.apm.metrics.pageLoadCharts.avgPageLoadByCountryLabel": "国ごとの平均ページ読み込み時間の分布", "xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.avgPageLoadDuration": "平均ページ読み込み時間:", "xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.countPageLoads": "{docCount} ページの読み込み", - "xpack.apm.settings.agentConf.agentConfigDocsLinkLabel": "詳細については、当社のドキュメントをご覧ください。", - "xpack.apm.settings.agentConf.betaBadgeLabel": "ベータ", - "xpack.apm.settings.agentConf.betaBadgeText": "この機能は開発中です。フィードバックがある場合は、ディスカッションフォーラムをご利用ください。", - "xpack.apm.settings.agentConf.betaCallOutTitle": "APM エージェント構成 (ベータ)", "xpack.apm.settings.agentConf.configTable.editButtonDescription": "この構成を編集します", "xpack.apm.settings.agentConf.configTable.editButtonLabel": "編集", "xpack.apm.settings.agentConf.configTable.emptyPromptText": "変更しましょう。直接 Kibana からエージェント構成を微調整できます。再展開する必要はありません。まず、最初の構成を作成します。", @@ -3504,40 +3493,6 @@ "xpack.apm.settings.agentConf.configTable.serviceNameColumnLabel": "サービス名", "xpack.apm.settings.agentConf.configurationsPanelTitle": "構成", "xpack.apm.settings.agentConf.createConfigButtonLabel": "構成の作成", - "xpack.apm.settings.agentConf.createConfigFailedText": "{serviceName}の構成を作成するときに問題が発生しました。エラー: {errorMessage}", - "xpack.apm.settings.agentConf.createConfigFailedTitle": "構成を作成できませんでした", - "xpack.apm.settings.agentConf.createConfigSucceededText": "{serviceName}の構成を正常に作成しました。エージェントに反映するには、少し時間がかかります。", - "xpack.apm.settings.agentConf.createConfigSucceededTitle": "構成が作成されました。", - "xpack.apm.settings.agentConf.deleteConfigFailedText": "{serviceName}の構成を削除するときに問題が発生しました。エラー: {errorMessage}", - "xpack.apm.settings.agentConf.deleteConfigFailedTitle": "構成を削除できませんでした", - "xpack.apm.settings.agentConf.deleteConfigSucceededText": "{serviceName}の構成を正常に削除しました。エージェントに反映するには、少し時間がかかります。", - "xpack.apm.settings.agentConf.deleteConfigSucceededTitle": "構成が削除されました", - "xpack.apm.settings.agentConf.editConfigFailedText": "{serviceName}の構成を編集するときに問題が発生しました。エラー: {errorMessage}", - "xpack.apm.settings.agentConf.editConfigFailedTitle": "構成を編集できませんでした", - "xpack.apm.settings.agentConf.editConfigSucceededText": "{serviceName}の構成を正常に編集しました。エージェントに反映するには、少し時間がかかります。", - "xpack.apm.settings.agentConf.editConfigSucceededTitle": "構成が編集されました", - "xpack.apm.settings.agentConf.flyOut.betaCallOutText": "この最初のバージョンでは、サンプルレート構成のみがサポートされます。今後のリリースで、エージェントのサポートを拡張します。不具合があることを認識してください。", - "xpack.apm.settings.agentConf.flyOut.betaCallOutTitle": "APM エージェント構成 (ベータ)", - "xpack.apm.settings.agentConf.flyOut.cancelButtonLabel": "キャンセル", - "xpack.apm.settings.agentConf.flyOut.configurationSectionTitle": "構成", - "xpack.apm.settings.agentConf.flyOut.createConfigTitle": "構成の作成", - "xpack.apm.settings.agentConf.flyOut.deleteConfigurationButtonLabel": "削除", - "xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionText": "この構成を削除する場合、APM サーバーと同期するまで、エージェントは、既存の構成を使用し続けます。", - "xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionTitle": "構成の削除", - "xpack.apm.settings.agentConf.flyOut.editConfigTitle": "構成の編集", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputErrorText": "サンプルレートは 0.000 ~ 1 の範囲でなければなりません", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputHelpText": "0.000 ~ 1.0 の範囲のレートを選択してください。既定の構成は 1.0 (100% のトレース) です。", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputLabel": "トランザクションサンプルレート", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputPlaceholderText": "サンプルレートの設定", - "xpack.apm.settings.agentConf.flyOut.saveConfigurationButtonLabel": "構成の保存", - "xpack.apm.settings.agentConf.flyOut.selectPlaceholder": "選択してください", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentNotSetOptionLabel": "未設定", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectErrorText": "構成を保存するには、有効な環境を選択する必要があります。", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectHelpText": "構成ごとに 1 つの環境のみがサポートされます。", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectLabel": "環境", - "xpack.apm.settings.agentConf.flyOut.serviceNameSelectHelpText": "構成するサービスを選択してください。", - "xpack.apm.settings.agentConf.flyOut.serviceNameSelectLabel": "名前", - "xpack.apm.settings.agentConf.flyOut.serviceSectionTitle": "サービス", "xpack.apm.settings.agentConf.pageTitle": "設定", "xpack.apm.settings.agentConf.returnToOverviewLinkLabel": "概要に戻る", "xpack.apm.transactionDetails.traceNotFound": "選択されたトレースが見つかりません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 971eabfdd83fbd..8b8dd9a0c40dcc 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -3421,14 +3421,9 @@ "xpack.apm.transactionActionMenu.showTraceLogsLinkLabel": "显示跟踪日志", "xpack.apm.transactionActionMenu.viewInUptime": "查看监测状态", "xpack.apm.transactionActionMenu.viewSampleDocumentLinkLabel": "查看样例文档", - "xpack.apm.transactionDetails.durationLabel": "持续时间", - "xpack.apm.transactionDetails.errorsNone": "无", - "xpack.apm.transactionDetails.errorsOverviewLabel": "错误", - "xpack.apm.transactionDetails.errorsOverviewLink": "{errorCount, plural, one {相关错误} other {相关错误}}", "xpack.apm.transactionDetails.errorsOverviewLinkTooltip": "{errorCount, plural, one {查看 1 个相关错误} other {查看 # 个相关错误}}", "xpack.apm.transactionDetails.notFoundLabel": "未找到任何事务。", "xpack.apm.transactionDetails.noTraceParentButtonTooltip": "找不到上级追溯", - "xpack.apm.transactionDetails.percentOfTraceLabel": "追溯的 %", "xpack.apm.transactionDetails.resultLabel": "结果", "xpack.apm.transactionDetails.serviceLabel": "服务", "xpack.apm.transactionDetails.servicesTitle": "服务", @@ -3438,7 +3433,6 @@ "xpack.apm.transactionDetails.spanFlyout.spanType.navigationTimingLabel": "导航定时", "xpack.apm.transactionDetails.spanFlyout.stackTraceTabLabel": "堆栈追溯", "xpack.apm.transactionDetails.spanFlyout.viewSpanInDiscoverButtonLabel": "在 Discover 中查看跨度", - "xpack.apm.transactionDetails.timestampLabel": "时间戳", "xpack.apm.transactionDetails.transactionLabel": "事务", "xpack.apm.transactionDetails.transactionsDurationDistributionChart.noSampleTooltip": "此存储桶没有可用样例", "xpack.apm.transactionDetails.transactionsDurationDistributionChart.requestTypeUnitLongLabel": "{transCount, plural, =0 {# 个请求} one {# 个请求} other {# 个请求}}", @@ -3450,7 +3444,6 @@ "xpack.apm.transactionDetails.transFlyout.callout.agentDroppedSpansMessage": "报告此事务的 APM 代理基于其配置丢弃了 {dropped} 个跨度。", "xpack.apm.transactionDetails.transFlyout.callout.learnMoreAboutDroppedSpansLinkText": "详细了解丢弃的跨度。", "xpack.apm.transactionDetails.transFlyout.transactionDetailsTitle": "事务详情", - "xpack.apm.transactionDetails.userIdLabel": "用户 ID", "xpack.apm.transactionDetails.viewFullTraceButtonLabel": "查看完整追溯信息", "xpack.apm.transactionDetails.viewingFullTraceButtonTooltip": "当前正在查看完整追溯信息", "xpack.apm.transactions.chart.95thPercentileLabel": "第 95 个百分位", @@ -3491,11 +3484,6 @@ "xpack.apm.metrics.pageLoadCharts.avgPageLoadByCountryLabel": "页面加载平均时长分布(按国家/地区)", "xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.avgPageLoadDuration": "页面加载平均时长:", "xpack.apm.metrics.pageLoadCharts.RegionMapChart.ToolTip.countPageLoads": "{docCount} 个页面加载", - "xpack.apm.settings.agentConf.agentConfigDocsLinkLabel": "在我们的文档中详细了解。", - "xpack.apm.settings.agentConf.betaBadgeLabel": "公测版", - "xpack.apm.settings.agentConf.betaBadgeText": "此功能仍在开发之中。如果您有反馈,请在我们的“讨论”论坛中提供。", - "xpack.apm.settings.agentConf.betaCallOutText": "我们很高兴让您第一时间了解 APM 代理配置。{agentConfigDocsLink}", - "xpack.apm.settings.agentConf.betaCallOutTitle": "APM 代理配置(公测版)", "xpack.apm.settings.agentConf.configTable.editButtonDescription": "编辑此配置", "xpack.apm.settings.agentConf.configTable.editButtonLabel": "编辑", "xpack.apm.settings.agentConf.configTable.emptyPromptText": "让我们改动一下!可以直接从 Kibana 微调代理配置,无需重新部署。首先创建您的第一个配置。", @@ -3506,40 +3494,6 @@ "xpack.apm.settings.agentConf.configTable.serviceNameColumnLabel": "服务名称", "xpack.apm.settings.agentConf.configurationsPanelTitle": "配置", "xpack.apm.settings.agentConf.createConfigButtonLabel": "创建配置", - "xpack.apm.settings.agentConf.createConfigFailedText": "为 {serviceName} 创建配置时出现问题。错误:{errorMessage}", - "xpack.apm.settings.agentConf.createConfigFailedTitle": "配置无法创建", - "xpack.apm.settings.agentConf.createConfigSucceededText": "您已成功为 {serviceName} 创建配置。将花费一些时间才能传播到代理。", - "xpack.apm.settings.agentConf.createConfigSucceededTitle": "配置已创建!", - "xpack.apm.settings.agentConf.deleteConfigFailedText": "为 {serviceName} 删除配置时出现问题。错误:{errorMessage}", - "xpack.apm.settings.agentConf.deleteConfigFailedTitle": "配置无法删除", - "xpack.apm.settings.agentConf.deleteConfigSucceededText": "您已成功为 {serviceName} 删除配置。将花费一些时间才能传播到代理。", - "xpack.apm.settings.agentConf.deleteConfigSucceededTitle": "配置已删除", - "xpack.apm.settings.agentConf.editConfigFailedText": "编辑 {serviceName} 的配置时出现问题。错误:{errorMessage}", - "xpack.apm.settings.agentConf.editConfigFailedTitle": "配置无法编辑", - "xpack.apm.settings.agentConf.editConfigSucceededText": "您已成功编辑 {serviceName} 的配置。将花费一些时间才能传播到代理。", - "xpack.apm.settings.agentConf.editConfigSucceededTitle": "配置已编辑", - "xpack.apm.settings.agentConf.flyOut.betaCallOutText": "请注意,在此第一版中仅支持采样速率配置。我们将在未来的版本中提供代理配置的支持。请注意故障。", - "xpack.apm.settings.agentConf.flyOut.betaCallOutTitle": "APM 代理配置(公测版)", - "xpack.apm.settings.agentConf.flyOut.cancelButtonLabel": "取消", - "xpack.apm.settings.agentConf.flyOut.configurationSectionTitle": "配置", - "xpack.apm.settings.agentConf.flyOut.createConfigTitle": "创建配置", - "xpack.apm.settings.agentConf.flyOut.deleteConfigurationButtonLabel": "删除", - "xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionText": "如果您希望删除此配置,请注意,代理将继续使用现有配置,直至它们与 APM Server 同步。", - "xpack.apm.settings.agentConf.flyOut.deleteConfigurationSectionTitle": "删除配置", - "xpack.apm.settings.agentConf.flyOut.editConfigTitle": "编辑配置", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputErrorText": "采样速率必须介于 0.000 和 1 之间", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputHelpText": "选择 0.000 和 1.0 之间的速率。默认配置为 1.0(跟踪的 100%)。", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputLabel": "事务采样速率", - "xpack.apm.settings.agentConf.flyOut.sampleRateConfigurationInputPlaceholderText": "设置采样速率", - "xpack.apm.settings.agentConf.flyOut.saveConfigurationButtonLabel": "保存配置", - "xpack.apm.settings.agentConf.flyOut.selectPlaceholder": "选择", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentNotSetOptionLabel": "未设置", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectErrorText": "必须选择有效的环境,才能保存配置。", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectHelpText": "每个配置仅支持单个环境。", - "xpack.apm.settings.agentConf.flyOut.serviceEnvironmentSelectLabel": "环境", - "xpack.apm.settings.agentConf.flyOut.serviceNameSelectHelpText": "选择要配置的服务。", - "xpack.apm.settings.agentConf.flyOut.serviceNameSelectLabel": "名称", - "xpack.apm.settings.agentConf.flyOut.serviceSectionTitle": "服务", "xpack.apm.settings.agentConf.pageTitle": "设置", "xpack.apm.settings.agentConf.returnToOverviewLinkLabel": "返回至概览", "xpack.apm.transactionDetails.traceNotFound": "找不到所选跟踪", diff --git a/x-pack/test/api_integration/apis/apm/agent_configuration.ts b/x-pack/test/api_integration/apis/apm/agent_configuration.ts new file mode 100644 index 00000000000000..43ba8616e6872f --- /dev/null +++ b/x-pack/test/api_integration/apis/apm/agent_configuration.ts @@ -0,0 +1,174 @@ +/* + * 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 expect from '@kbn/expect'; +import { FtrProviderContext } from '../../ftr_provider_context'; + +export default function agentConfigurationTests({ getService }: FtrProviderContext) { + const supertest = getService('supertest'); + const log = getService('log'); + + function searchConfigurations(configuration: any) { + return supertest + .post(`/api/apm/settings/agent-configuration/search`) + .send(configuration) + .set('kbn-xsrf', 'foo'); + } + + let createdConfigIds: any[] = []; + async function createConfiguration(configuration: any) { + const res = await supertest + .post(`/api/apm/settings/agent-configuration/new`) + .send(configuration) + .set('kbn-xsrf', 'foo'); + + createdConfigIds.push(res.body._id); + + return res; + } + + function deleteCreatedConfigurations() { + const promises = Promise.all(createdConfigIds.map(deleteConfiguration)); + createdConfigIds = []; + return promises; + } + + function deleteConfiguration(configurationId: string) { + return supertest + .delete(`/api/apm/settings/agent-configuration/${configurationId}`) + .set('kbn-xsrf', 'foo'); + } + + describe('agent configuration', () => { + describe('when creating four configurations', () => { + before(async () => { + log.debug('creating agent configuration'); + + // all / all + await createConfiguration({ + 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' }, + settings: { transaction_sample_rate: 0.3 }, + }); + + // all / production + await createConfiguration({ + service: { environment: 'development' }, + settings: { transaction_sample_rate: 0.4 }, + }); + + // my_service / production + await createConfiguration({ + service: { name: 'my_service', environment: 'development' }, + settings: { transaction_sample_rate: 0.5 }, + }); + }); + + after(async () => { + log.debug('deleting agent configurations'); + await deleteCreatedConfigurations(); + }); + + const agentsRequests = [ + { + service: { name: 'non_existing_service', environment: 'non_existing_env' }, + expectedSettings: { transaction_sample_rate: 0.1 }, + }, + { + service: { name: 'my_service', environment: 'production' }, + expectedSettings: { transaction_sample_rate: 0.2 }, + }, + { + service: { name: 'non_existing_service', environment: 'production' }, + expectedSettings: { transaction_sample_rate: 0.3 }, + }, + { + service: { name: 'non_existing_service', environment: 'development' }, + expectedSettings: { transaction_sample_rate: 0.4 }, + }, + { + service: { name: 'my_service', environment: 'development' }, + expectedSettings: { transaction_sample_rate: 0.5 }, + }, + ]; + + for (const agentRequest of agentsRequests) { + it(`${agentRequest.service.name} / ${agentRequest.service.environment}`, async () => { + const { statusCode, body } = await searchConfigurations({ + service: agentRequest.service, + etag: 'abc', + }); + + expect(statusCode).to.equal(200); + expect(body._source.settings).to.eql(agentRequest.expectedSettings); + }); + } + }); + + describe('when an agent retrieves a configuration', () => { + before(async () => { + log.debug('creating agent configuration'); + + await createConfiguration({ + service: { name: 'myservice', environment: 'development' }, + settings: { transaction_sample_rate: 0.9 }, + }); + }); + + after(async () => { + log.debug('deleting agent configurations'); + await deleteCreatedConfigurations(); + }); + + it(`should have 'applied_by_agent=false' on first request`, async () => { + const { body } = await searchConfigurations({ + service: { name: 'myservice', environment: 'development' }, + etag: '7312bdcc34999629a3d39df24ed9b2a7553c0c39', + }); + + expect(body._source.applied_by_agent).to.be(false); + }); + + it(`should have 'applied_by_agent=true' on second request`, async () => { + async function getAppliedByAgent() { + const { body } = await searchConfigurations({ + service: { name: 'myservice', environment: 'development' }, + etag: '7312bdcc34999629a3d39df24ed9b2a7553c0c39', + }); + + return body._source.applied_by_agent; + } + + // wait until `applied_by_agent` has been updated in elasticsearch + expect(await waitFor(getAppliedByAgent)).to.be(true); + }); + }); + }); +} + +async function waitFor(cb: () => Promise, retries = 50): Promise { + if (retries === 0) { + throw new Error(`Maximum number of retries reached`); + } + + const res = await cb(); + if (!res) { + await new Promise(resolve => setTimeout(resolve, 100)); + return waitFor(cb, retries - 1); + } + return res; +} 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 6677928a199798..7f841747cdcabf 100644 --- a/x-pack/test/api_integration/apis/apm/feature_controls.ts +++ b/x-pack/test/api_integration/apis/apm/feature_controls.ts @@ -133,7 +133,7 @@ export default function featureControlsTests({ getService }: FtrProviderContext) req: { method: 'post', url: `/api/apm/settings/agent-configuration/search`, - body: { service: { name: 'test-service' } }, + body: { service: { name: 'test-service' }, etag: 'abc' }, }, expectForbidden: expect404, expectResponse: expect200, diff --git a/x-pack/test/api_integration/apis/apm/index.ts b/x-pack/test/api_integration/apis/apm/index.ts index 6364c7304118ff..c49d5775370485 100644 --- a/x-pack/test/api_integration/apis/apm/index.ts +++ b/x-pack/test/api_integration/apis/apm/index.ts @@ -9,5 +9,6 @@ import { FtrProviderContext } from '../../ftr_provider_context'; export default function apmApiIntegrationTests({ loadTestFile }: FtrProviderContext) { describe('APM', () => { loadTestFile(require.resolve('./feature_controls')); + loadTestFile(require.resolve('./agent_configuration')); }); }