diff --git a/x-pack/plugins/apm/common/environment_filter_values.ts b/x-pack/plugins/apm/common/environment_filter_values.ts index e4779ee9547f0..5b6354880cce5 100644 --- a/x-pack/plugins/apm/common/environment_filter_values.ts +++ b/x-pack/plugins/apm/common/environment_filter_values.ts @@ -6,6 +6,7 @@ */ import { i18n } from '@kbn/i18n'; +import { escapeKuery } from '@kbn/es-query'; import { SERVICE_ENVIRONMENT } from './es_fields/apm'; import { Environment } from './environment_rt'; @@ -43,18 +44,30 @@ export const ENVIRONMENT_NOT_DEFINED = { label: getEnvironmentLabel(ENVIRONMENT_NOT_DEFINED_VALUE), }; -export function getEnvironmentEsField(environment: string) { - if ( +function isEnvironmentDefined(environment: string) { + return ( !environment || environment === ENVIRONMENT_NOT_DEFINED_VALUE || environment === ENVIRONMENT_ALL_VALUE - ) { + ); +} + +export function getEnvironmentEsField(environment: string) { + if (isEnvironmentDefined(environment)) { return {}; } return { [SERVICE_ENVIRONMENT]: environment }; } +export function getEnvironmentKuery(environment: string) { + if (isEnvironmentDefined(environment)) { + return null; + } + + return `${[SERVICE_ENVIRONMENT]}: ${escapeKuery(environment)} `; +} + // returns the environment url param that should be used // based on the requested environment. If the requested // environment is different from the URL parameter, we'll diff --git a/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/alerts_table.cy.ts b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/alerts_table.cy.ts new file mode 100644 index 0000000000000..6a5cd12bc7842 --- /dev/null +++ b/x-pack/plugins/apm/ftr_e2e/cypress/e2e/read_only_user/service_overview/alerts_table.cy.ts @@ -0,0 +1,46 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import url from 'url'; +import { synthtrace } from '../../../../synthtrace'; +import { opbeans } from '../../../fixtures/synthtrace/opbeans'; + +const start = '2021-10-10T00:00:00.000Z'; +const end = '2021-10-10T00:15:00.000Z'; + +const serviceOverviewHref = url.format({ + pathname: '/app/apm/services/opbeans-java/alerts', + query: { rangeFrom: start, rangeTo: end }, +}); + +describe('Errors table', () => { + before(() => { + synthtrace.index( + opbeans({ + from: new Date(start).getTime(), + to: new Date(end).getTime(), + }) + ); + }); + + after(() => { + synthtrace.clean(); + }); + + beforeEach(() => { + cy.loginAsViewerUser(); + }); + + it('Alerts table with the search bar is populated', () => { + cy.visitKibana(serviceOverviewHref); + cy.contains('opbeans-java'); + cy.contains('All'); + cy.contains('Active'); + cy.contains('Recovered'); + cy.getByTestSubj('globalQueryBar').should('exist'); + }); +}); diff --git a/x-pack/plugins/apm/public/components/app/alerts_overview/alerts_table_status_filter.tsx b/x-pack/plugins/apm/public/components/app/alerts_overview/alerts_table_status_filter.tsx deleted file mode 100644 index 49769de6a6b42..0000000000000 --- a/x-pack/plugins/apm/public/components/app/alerts_overview/alerts_table_status_filter.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { EuiButtonGroup, EuiButtonGroupOptionProps } from '@elastic/eui'; -import { i18n } from '@kbn/i18n'; -import React from 'react'; -import { - ALERT_STATUS_ACTIVE, - ALERT_STATUS_RECOVERED, - ALERT_STATUS, -} from '@kbn/rule-data-utils'; -export const ALL_ALERTS_FILTER = 'ALL_ALERTS_FILTER'; - -export type AlertStatusFilterButton = - | typeof ALERT_STATUS_ACTIVE - | typeof ALERT_STATUS_RECOVERED - | typeof ALL_ALERTS_FILTER; - -export interface AlertStatusFilterProps { - status: AlertStatusFilterButton; - onChange: (id: AlertStatusFilterButton) => void; -} - -const options: EuiButtonGroupOptionProps[] = [ - { - id: ALL_ALERTS_FILTER, - value: '', - label: i18n.translate('xpack.apm.alerts.alertStatusFilter.showAll', { - defaultMessage: 'Show all', - }), - 'data-test-subj': 'alert-status-filter-show-all-button', - }, - { - id: ALERT_STATUS_ACTIVE, - value: `${ALERT_STATUS}: "${ALERT_STATUS_RECOVERED}"`, - label: i18n.translate('xpack.apm.alerts.alertStatusFilter.active', { - defaultMessage: 'Active', - }), - 'data-test-subj': 'alert-status-filter-active-button', - }, - { - id: ALERT_STATUS_RECOVERED, - value: `${ALERT_STATUS}: "${ALERT_STATUS_RECOVERED}"`, - label: i18n.translate('xpack.apm.alerts.alertStatusFilter.recovered', { - defaultMessage: 'Recovered', - }), - 'data-test-subj': 'alert-status-filter-recovered-button', - }, -]; - -export function AlertsTableStatusFilter({ - status, - onChange, -}: AlertStatusFilterProps) { - return ( - onChange(id as AlertStatusFilterButton)} - /> - ); -} diff --git a/x-pack/plugins/apm/public/components/app/alerts_overview/index.test.tsx b/x-pack/plugins/apm/public/components/app/alerts_overview/index.test.tsx deleted file mode 100644 index 270d0ba8c4561..0000000000000 --- a/x-pack/plugins/apm/public/components/app/alerts_overview/index.test.tsx +++ /dev/null @@ -1,170 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render, waitFor, act } from '@testing-library/react'; -import { MemoryRouter } from 'react-router-dom'; -import React, { ReactNode } from 'react'; -import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; -import * as useApmParamsHooks from '../../../hooks/use_apm_params'; -import { createKibanaReactContext } from '@kbn/kibana-react-plugin/public'; -import { CoreStart } from '@kbn/core/public'; -import { AlertsOverview } from '.'; - -const getAlertsStateTableMock = jest.fn(); - -function Wrapper({ children }: { children?: ReactNode }) { - const KibanaReactContext = createKibanaReactContext({ - triggersActionsUi: { - getAlertsStateTable: getAlertsStateTableMock.mockReturnValue( -
- ), - alertsTableConfigurationRegistry: '', - }, - } as Partial); - - return ( - - - {children} - - - ); -} - -const renderOptions = { wrapper: Wrapper }; - -describe('AlertsTable', () => { - beforeEach(() => { - jest.spyOn(useApmParamsHooks as any, 'useApmParams').mockReturnValue({ - path: { - serviceName: 'opbeans', - }, - query: { - rangeFrom: 'now-24h', - rangeTo: 'now', - environment: 'testing', - }, - }); - jest.clearAllMocks(); - }); - - it('renders alerts table in service overview', async () => { - const { getByTestId } = render(, renderOptions); - - await waitFor(async () => { - expect(getByTestId('alerts-table')).toBeTruthy(); - }); - }); - it('should call alerts table with correct propts', async () => { - act(() => { - render(, renderOptions); - }); - - await waitFor(async () => { - expect(getAlertsStateTableMock).toHaveBeenCalledWith( - { - alertsTableConfigurationRegistry: '', - id: 'service-overview-alerts', - configurationId: 'observability', - featureIds: ['apm'], - query: { - bool: { - filter: [ - { - term: { 'service.name': 'opbeans' }, - }, - { - term: { 'service.environment': 'testing' }, - }, - ], - }, - }, - showExpandToDetails: false, - }, - {} - ); - }); - }); - - it('should call alerts table with active filter', async () => { - const { getByTestId } = render(, renderOptions); - - await act(async () => { - const inputEl = getByTestId('active'); - inputEl.click(); - }); - - await waitFor(async () => { - expect(getAlertsStateTableMock).toHaveBeenLastCalledWith( - { - alertsTableConfigurationRegistry: '', - id: 'service-overview-alerts', - configurationId: 'observability', - featureIds: ['apm'], - query: { - bool: { - filter: [ - { - term: { 'service.name': 'opbeans' }, - }, - { - term: { 'kibana.alert.status': 'active' }, - }, - { - term: { 'service.environment': 'testing' }, - }, - ], - }, - }, - showExpandToDetails: false, - }, - {} - ); - }); - }); - - it('should call alerts table with recovered filter', async () => { - const { getByTestId } = render(, renderOptions); - - await act(async () => { - const inputEl = getByTestId('recovered'); - inputEl.click(); - }); - - await waitFor(async () => { - expect(getAlertsStateTableMock).toHaveBeenLastCalledWith( - { - alertsTableConfigurationRegistry: '', - id: 'service-overview-alerts', - configurationId: 'observability', - featureIds: ['apm'], - query: { - bool: { - filter: [ - { - term: { 'service.name': 'opbeans' }, - }, - { - term: { 'kibana.alert.status': 'recovered' }, - }, - { - term: { 'service.environment': 'testing' }, - }, - ], - }, - }, - showExpandToDetails: false, - }, - {} - ); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/app/alerts_overview/index.tsx b/x-pack/plugins/apm/public/components/app/alerts_overview/index.tsx index ad885bed46fa1..898762c8350fa 100644 --- a/x-pack/plugins/apm/public/components/app/alerts_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/alerts_overview/index.tsx @@ -6,82 +6,106 @@ */ import React, { useState, useMemo } from 'react'; +import { useHistory } from 'react-router-dom'; +import { ObservabilityAlertSearchBar } from '@kbn/observability-plugin/public'; +import { AlertStatus } from '@kbn/observability-plugin/common/typings'; import { EuiPanel, EuiFlexItem, EuiFlexGroup } from '@elastic/eui'; -import { ALERT_STATUS } from '@kbn/rule-data-utils'; +import { BoolQuery } from '@kbn/es-query'; import { AlertConsumers } from '@kbn/rule-data-utils'; import { useKibana } from '@kbn/kibana-react-plugin/public'; import { ApmPluginStartDeps } from '../../../plugin'; import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; import { SERVICE_NAME } from '../../../../common/es_fields/apm'; -import { environmentQuery } from '../../../../common/utils/environment_query'; -import { - AlertsTableStatusFilter, - ALL_ALERTS_FILTER, - AlertStatusFilterButton, -} from './alerts_table_status_filter'; +import { getEnvironmentKuery } from '../../../../common/environment_filter_values'; +import { push } from '../../shared/links/url_helpers'; + +export const ALERT_STATUS_ALL = 'all'; export function AlertsOverview() { + const history = useHistory(); const { path: { serviceName }, - query: { environment }, + query: { environment, rangeFrom, rangeTo, kuery }, } = useAnyOfApmParams( '/services/{serviceName}/alerts', '/mobile-services/{serviceName}/alerts' ); const { services } = useKibana(); const [alertStatusFilter, setAlertStatusFilter] = - useState(ALL_ALERTS_FILTER); + useState(ALERT_STATUS_ALL); + const [esQuery, setEsQuery] = useState<{ bool: BoolQuery }>(); const { triggersActionsUi: { getAlertsStateTable: AlertsStateTable, + getAlertsSearchBar: AlertsSearchBar, alertsTableConfigurationRegistry, }, + notifications, + data: { + query: { + timefilter: { timefilter: timeFilterService }, + }, + }, } = services; - const alertQuery = useMemo( - () => ({ - bool: { - filter: [ - { - term: { [SERVICE_NAME]: serviceName }, - }, - ...(alertStatusFilter !== ALL_ALERTS_FILTER - ? [ - { - term: { [ALERT_STATUS]: alertStatusFilter }, - }, - ] - : []), - ...environmentQuery(environment), - ], - }, - }), - [serviceName, alertStatusFilter, environment] - ); + const useToasts = () => notifications!.toasts; + + const apmQueries = useMemo(() => { + const environmentKuery = getEnvironmentKuery(environment); + let query = `${SERVICE_NAME}:${serviceName}`; - const alertStateProps = { - alertsTableConfigurationRegistry, - id: 'service-overview-alerts', - configurationId: AlertConsumers.OBSERVABILITY, - featureIds: [AlertConsumers.APM], - query: alertQuery, - showExpandToDetails: false, - }; + if (environmentKuery) { + query += ` AND ${environmentKuery}`; + } + return [ + { + query, + language: 'kuery', + }, + ]; + }, [serviceName, environment]); return ( - + push(history, { query: { rangeFrom: value } }) + } + onRangeToChange={(value) => + push(history, { query: { rangeTo: value } }) + } + onKueryChange={(value) => + push(history, { query: { kuery: value } }) + } + defaultSearchQueries={apmQueries} + onStatusChange={setAlertStatusFilter} + onEsQueryChange={setEsQuery} + rangeTo={rangeTo} + rangeFrom={rangeFrom} status={alertStatusFilter} - onChange={setAlertStatusFilter} + services={{ timeFilterService, AlertsSearchBar, useToasts }} /> - + {esQuery && ( + + )} diff --git a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx index 5a127e768709c..5e98e40525a67 100644 --- a/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/templates/apm_service_template/index.tsx @@ -213,6 +213,9 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { query: queryFromUrl, } = useApmParams(`/services/{serviceName}/${selectedTab}` as const); + const { rangeFrom, rangeTo, environment } = queryFromUrl; + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + const { data: serviceAlertsCount = { alertsCount: 0 } } = useFetcher( (callApmApi) => { return callApmApi( @@ -222,11 +225,16 @@ function useTabs({ selectedTab }: { selectedTab: Tab['key'] }) { path: { serviceName, }, + query: { + start, + end, + environment, + }, }, } ); }, - [serviceName] + [serviceName, start, end, environment] ); const query = omit( diff --git a/x-pack/plugins/apm/server/routes/services/get_services/get_service_alerts.ts b/x-pack/plugins/apm/server/routes/services/get_services/get_service_alerts.ts index 989ea8b9a6628..16675afc9d74a 100644 --- a/x-pack/plugins/apm/server/routes/services/get_services/get_service_alerts.ts +++ b/x-pack/plugins/apm/server/routes/services/get_services/get_service_alerts.ts @@ -9,7 +9,11 @@ import { AggregationsCardinalityAggregate, AggregationsFilterAggregate, } from '@elastic/elasticsearch/lib/api/types'; -import { kqlQuery } from '@kbn/observability-plugin/server'; +import { + kqlQuery, + termQuery, + rangeQuery, +} from '@kbn/observability-plugin/server'; import { ALERT_RULE_PRODUCER, ALERT_STATUS, @@ -19,6 +23,7 @@ import { import { SERVICE_NAME } from '../../../../common/es_fields/apm'; import { ServiceGroup } from '../../../../common/service_groups'; import { ApmAlertsClient } from '../../../lib/helpers/get_apm_alerts_client'; +import { environmentQuery } from '../../../../common/utils/environment_query'; import { serviceGroupQuery } from '../../../lib/service_group_query'; import { MAX_NUMBER_OF_SERVICES } from './get_services_items'; @@ -37,23 +42,31 @@ export async function getServicesAlerts({ maxNumServices = MAX_NUMBER_OF_SERVICES, serviceGroup, serviceName, + start, + end, + environment, }: { apmAlertsClient: ApmAlertsClient; kuery?: string; maxNumServices?: number; serviceGroup?: ServiceGroup | null; serviceName?: string; + start: number; + end: number; + environment?: string; }) { const params = { size: 0, query: { bool: { filter: [ - { term: { [ALERT_RULE_PRODUCER]: 'apm' } }, - { term: { [ALERT_STATUS]: ALERT_STATUS_ACTIVE } }, + ...termQuery(ALERT_RULE_PRODUCER, 'apm'), + ...termQuery(ALERT_STATUS, ALERT_STATUS_ACTIVE), + ...rangeQuery(start, end), ...kqlQuery(kuery), ...serviceGroupQuery(serviceGroup), - ...(serviceName ? [{ term: { [SERVICE_NAME]: serviceName } }] : []), + ...termQuery(SERVICE_NAME, serviceName), + ...environmentQuery(environment), ], }, }, diff --git a/x-pack/plugins/apm/server/routes/services/route.ts b/x-pack/plugins/apm/server/routes/services/route.ts index 0f2e40caf8b04..041783beab5b2 100644 --- a/x-pack/plugins/apm/server/routes/services/route.ts +++ b/x-pack/plugins/apm/server/routes/services/route.ts @@ -1220,6 +1220,7 @@ const serviceAlertsRoute = createApmServerRoute({ path: t.type({ serviceName: t.string, }), + query: t.intersection([rangeRt, environmentRt]), }), options: { tags: ['access:apm'] }, handler: async ( @@ -1229,13 +1230,18 @@ const serviceAlertsRoute = createApmServerRoute({ alertsCount: number; }> => { const { params } = resources; - + const { + query: { start, end, environment }, + } = params; const { serviceName } = params.path; const apmAlertsClient = await getApmAlertsClient(resources); const servicesAlerts = await getServicesAlerts({ serviceName, apmAlertsClient, + environment, + start, + end, }); return servicesAlerts.length > 0 diff --git a/x-pack/plugins/translations/translations/fr-FR.json b/x-pack/plugins/translations/translations/fr-FR.json index 637865388ad51..10f83f79092b0 100644 --- a/x-pack/plugins/translations/translations/fr-FR.json +++ b/x-pack/plugins/translations/translations/fr-FR.json @@ -6966,10 +6966,6 @@ "xpack.apm.alerts.action_variables.transactionType": "Type de transaction pour lequel l'alerte est créée", "xpack.apm.alerts.action_variables.triggerValue": "Valeur ayant dépassé le seuil et déclenché l'alerte", "xpack.apm.alerts.action_variables.viewInAppUrl": "Lien vers la vue ou la fonctionnalité d'Elastic qui peut être utilisée pour examiner l'alerte et son contexte de manière plus approfondie", - "xpack.apm.alerts.alertStatusFilter.active": "Actif", - "xpack.apm.alerts.alertStatusFilter.button.legend": "Filtrer par", - "xpack.apm.alerts.alertStatusFilter.recovered": "Récupéré", - "xpack.apm.alerts.alertStatusFilter.showAll": "Afficher tout", "xpack.apm.alerts.anomalySeverity.criticalLabel": "critique", "xpack.apm.alerts.anomalySeverity.majorLabel": "majeur", "xpack.apm.alerts.anomalySeverity.minor": "mineure", @@ -36523,4 +36519,4 @@ "xpack.painlessLab.title": "Painless Lab", "xpack.painlessLab.walkthroughButtonLabel": "Présentation" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index eb9e2bb17df15..8a2920d765b32 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6956,10 +6956,6 @@ "xpack.apm.alerts.action_variables.transactionType": "アラートが作成されるトランザクションタイプ", "xpack.apm.alerts.action_variables.triggerValue": "しきい値に達し、アラートをトリガーした値", "xpack.apm.alerts.action_variables.viewInAppUrl": "アラートとコンテキストを調査するために使用できる、Elastic内のビューまたは機能へのリンク", - "xpack.apm.alerts.alertStatusFilter.active": "アクティブ", - "xpack.apm.alerts.alertStatusFilter.button.legend": "フィルタリング条件", - "xpack.apm.alerts.alertStatusFilter.recovered": "回復済み", - "xpack.apm.alerts.alertStatusFilter.showAll": "すべて表示", "xpack.apm.alerts.anomalySeverity.criticalLabel": "致命的", "xpack.apm.alerts.anomalySeverity.majorLabel": "メジャー", "xpack.apm.alerts.anomalySeverity.minor": "マイナー", @@ -36491,4 +36487,4 @@ "xpack.painlessLab.title": "Painless Lab", "xpack.painlessLab.walkthroughButtonLabel": "実地検証" } -} \ No newline at end of file +} diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 5468d9820cd69..74f1258797aba 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6969,10 +6969,6 @@ "xpack.apm.alerts.action_variables.transactionType": "创建告警的事务类型", "xpack.apm.alerts.action_variables.triggerValue": "超过阈值并触发告警的值", "xpack.apm.alerts.action_variables.viewInAppUrl": "Elastic 中可用于进一步调查告警及其上下文的视图或功能的链接", - "xpack.apm.alerts.alertStatusFilter.active": "活动", - "xpack.apm.alerts.alertStatusFilter.button.legend": "筛选依据", - "xpack.apm.alerts.alertStatusFilter.recovered": "已恢复", - "xpack.apm.alerts.alertStatusFilter.showAll": "全部显示", "xpack.apm.alerts.anomalySeverity.criticalLabel": "紧急", "xpack.apm.alerts.anomalySeverity.majorLabel": "重大", "xpack.apm.alerts.anomalySeverity.minor": "轻微", @@ -36528,4 +36524,4 @@ "xpack.painlessLab.title": "Painless 实验室", "xpack.painlessLab.walkthroughButtonLabel": "指导" } -} \ No newline at end of file +} diff --git a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts b/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts index 1cb9e01a8fe1e..a8c92dfdd256e 100644 --- a/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts +++ b/x-pack/test/apm_api_integration/tests/services/service_alerts.spec.ts @@ -22,11 +22,22 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { const end = Date.now(); const goService = 'synth-go'; - async function getServiceAlerts(serviceName: string) { + async function getServiceAlerts({ + serviceName, + environment, + }: { + serviceName: string; + environment: string; + }) { return apmApiClient.readUser({ endpoint: 'GET /internal/apm/services/{serviceName}/alerts_count', params: { path: { serviceName }, + query: { + start: new Date(start).toISOString(), + end: new Date(end + 5 * 60 * 1000).toISOString(), + environment, + }, }, }); } @@ -121,7 +132,7 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { }); it('returns the correct number of alerts', async () => { - const response = await getServiceAlerts(goService); + const response = await getServiceAlerts({ serviceName: goService, environment: 'testing' }); expect(response.status).to.be(200); expect(response.body.serviceName).to.be(goService); expect(response.body.alertsCount).to.be(1); @@ -130,7 +141,7 @@ export default function ServiceAlerts({ getService }: FtrProviderContext) { describe('without alerts', () => { it('returns the correct number of alerts', async () => { - const response = await getServiceAlerts(goService); + const response = await getServiceAlerts({ serviceName: goService, environment: 'foo' }); expect(response.status).to.be(200); expect(response.body.serviceName).to.be(goService); expect(response.body.alertsCount).to.be(0);