diff --git a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts index 4b4cc42e08089..ed9971307bf64 100644 --- a/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts +++ b/x-pack/plugins/apm/public/components/alerting/rule_types/register_apm_rule_types.ts @@ -33,7 +33,8 @@ export function registerApmRuleTypes( return { reason: fields[ALERT_REASON]!, link: getAlertUrlErrorCount( - String(fields[SERVICE_NAME][0]!), + // TODO:fix SERVICE_NAME when we move it to initializeIndex + String(fields[SERVICE_NAME]![0]), fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]) ), }; @@ -46,6 +47,12 @@ export function registerApmRuleTypes( validate: () => ({ errors: [], }), + alertDetailsAppSection: lazy( + () => + import( + '../ui_components/alert_details_app_section/alert_details_app_section' + ) + ), requiresAppContext: false, defaultActionMessage: i18n.translate( 'xpack.apm.alertTypes.errorCount.defaultActionMessage', @@ -73,9 +80,10 @@ export function registerApmRuleTypes( return { reason: fields[ALERT_REASON]!, link: getAlertUrlTransaction( - String(fields[SERVICE_NAME][0]!), + // TODO:fix SERVICE_NAME when we move it to initializeIndex + String(fields[SERVICE_NAME]![0]), fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]), - String(fields[TRANSACTION_TYPE][0]!) + String(fields[TRANSACTION_TYPE]![0]) ), }; }, @@ -89,6 +97,12 @@ export function registerApmRuleTypes( validate: () => ({ errors: [], }), + alertDetailsAppSection: lazy( + () => + import( + '../ui_components/alert_details_app_section/alert_details_app_section' + ) + ), requiresAppContext: false, defaultActionMessage: i18n.translate( 'xpack.apm.alertTypes.transactionDuration.defaultActionMessage', @@ -116,9 +130,10 @@ export function registerApmRuleTypes( format: ({ fields, formatters: { asPercent } }) => ({ reason: fields[ALERT_REASON]!, link: getAlertUrlTransaction( - String(fields[SERVICE_NAME][0]!), + // TODO:fix SERVICE_NAME when we move it to initializeIndex + String(fields[SERVICE_NAME]![0]), fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]), - String(fields[TRANSACTION_TYPE][0]!) + String(fields[TRANSACTION_TYPE]![0]) ), }), iconClass: 'bell', @@ -131,6 +146,12 @@ export function registerApmRuleTypes( validate: () => ({ errors: [], }), + alertDetailsAppSection: lazy( + () => + import( + '../ui_components/alert_details_app_section/alert_details_app_section' + ) + ), requiresAppContext: false, defaultActionMessage: i18n.translate( 'xpack.apm.alertTypes.transactionErrorRate.defaultActionMessage', @@ -155,9 +176,10 @@ export function registerApmRuleTypes( format: ({ fields }) => ({ reason: fields[ALERT_REASON]!, link: getAlertUrlTransaction( - String(fields[SERVICE_NAME][0]!), + // TODO:fix SERVICE_NAME when we move it to initializeIndex + String(fields[SERVICE_NAME]![0]), fields[SERVICE_ENVIRONMENT] && String(fields[SERVICE_ENVIRONMENT][0]), - String(fields[TRANSACTION_TYPE][0]!) + String(fields[TRANSACTION_TYPE]![0]) ), }), iconClass: 'bell', @@ -170,6 +192,12 @@ export function registerApmRuleTypes( validate: () => ({ errors: [], }), + alertDetailsAppSection: lazy( + () => + import( + '../ui_components/alert_details_app_section/alert_details_app_section' + ) + ), requiresAppContext: false, defaultActionMessage: i18n.translate( 'xpack.apm.alertTypes.transactionDurationAnomaly.defaultActionMessage', diff --git a/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/alert_details_app_section.tsx b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/alert_details_app_section.tsx new file mode 100644 index 0000000000000..92cd229333062 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/alert_details_app_section.tsx @@ -0,0 +1,419 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import React, { useMemo } from 'react'; +import { EuiFlexGroup } from '@elastic/eui'; +import { EuiFlexItem } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; +import { EuiPanel } from '@elastic/eui'; +import { EuiTitle } from '@elastic/eui'; +import { EuiIconTip } from '@elastic/eui'; +import { ALERT_DURATION, ALERT_END } from '@kbn/rule-data-utils'; +import moment from 'moment'; +import { ENVIRONMENT_ALL } from '../../../../../common/environment_filter_values'; +import { getTransactionType } from '../../../../context/apm_service/apm_service_context'; +import { useServiceAgentFetcher } from '../../../../context/apm_service/use_service_agent_fetcher'; +import { useServiceTransactionTypesFetcher } from '../../../../context/apm_service/use_service_transaction_types_fetcher'; +import { asPercent } from '../../../../../common/utils/formatters'; +import { APIReturnType } from '../../../../services/rest/create_call_apm_api'; +import { getDurationFormatter } from '../../../../../common/utils/formatters/duration'; +import { useFetcher } from '../../../../hooks/use_fetcher'; +import { useTimeRange } from '../../../../hooks/use_time_range'; +import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options'; +import { getComparisonChartTheme } from '../../../shared/time_comparison/get_comparison_chart_theme'; +import { getLatencyChartSelector } from '../../../../selectors/latency_chart_selectors'; +import { TimeseriesChart } from '../../../shared/charts/timeseries_chart'; +import { + getMaxY, + getResponseTimeTickFormatter, +} from '../../../shared/charts/transaction_charts/helper'; +import { ChartPointerEventContextProvider } from '../../../../context/chart_pointer_event/chart_pointer_event_context'; +import { + ChartType, + getTimeSeriesColor, +} from '../../../shared/charts/helper/get_timeseries_color'; +import { + AlertDetailsAppSectionProps, + SERVICE_NAME, + TRANSACTION_TYPE, +} from './types'; +import { getAggsTypeFromRule } from './helpers'; +import { filterNil } from '../../../shared/charts/latency_chart'; +import { errorRateI18n } from '../../../shared/charts/failed_transaction_rate_chart'; + +export function AlertDetailsAppSection({ + rule, + alert, + timeZone, +}: AlertDetailsAppSectionProps) { + const params = rule.params; + const environment = String(params.environment) || ENVIRONMENT_ALL.value; + const latencyAggregationType = getAggsTypeFromRule( + params.aggregationType as string + ); + + // duration is us, convert it to MS + const alertDurationMS = alert.fields[ALERT_DURATION]! / 1000; + + const serviceName = String(alert.fields[SERVICE_NAME]); + + // Currently, we don't use comparisonEnabled nor offset. + // But providing them as they are required for the chart. + const comparisonEnabled = false; + const offset = '1d'; + const ruleWindowSizeMS = moment + .duration(rule.params.windowSize, rule.params.windowUnit) + .asMilliseconds(); + + const TWENTY_TIMES_RULE_WINDOW_MS = 20 * ruleWindowSizeMS; + /** + * This is part or the requirements (RFC). + * If the alert is less than 20 units of `FOR THE LAST ` then we should draw a time range of 20 units. + * IE. The user set "FOR THE LAST 5 minutes" at a minimum we should show 100 minutes. + */ + const rangeFrom = + alertDurationMS < TWENTY_TIMES_RULE_WINDOW_MS + ? moment(alert.start) + .subtract(TWENTY_TIMES_RULE_WINDOW_MS, 'millisecond') + .toISOString() + : moment(alert.start) + .subtract(ruleWindowSizeMS, 'millisecond') + .toISOString(); + + const rangeTo = alert.active + ? 'now' + : moment(alert.fields[ALERT_END]) + .add(ruleWindowSizeMS, 'millisecond') + .toISOString(); + + const { start, end } = useTimeRange({ rangeFrom, rangeTo }); + const { agentName } = useServiceAgentFetcher({ + serviceName, + start, + end, + }); + const transactionTypes = useServiceTransactionTypesFetcher({ + serviceName, + start, + end, + }); + + const transactionType = getTransactionType({ + transactionType: String(alert.fields[TRANSACTION_TYPE]), + transactionTypes, + agentName, + }); + + const comparisonChartTheme = getComparisonChartTheme(); + const INITIAL_STATE = { + currentPeriod: [], + previousPeriod: [], + }; + + /* Latency Chart */ + const { data, status } = useFetcher( + (callApmApi) => { + if ( + serviceName && + start && + end && + transactionType && + latencyAggregationType + ) { + return callApmApi( + `GET /internal/apm/services/{serviceName}/transactions/charts/latency`, + { + params: { + path: { serviceName }, + query: { + environment, + kuery: '', + start, + end, + transactionType, + transactionName: undefined, + latencyAggregationType, + }, + }, + } + ); + } + }, + [ + end, + environment, + latencyAggregationType, + serviceName, + start, + transactionType, + ] + ); + + const memoizedData = useMemo( + () => + getLatencyChartSelector({ + latencyChart: data, + latencyAggregationType, + previousPeriodLabel: '', + }), + // It should only update when the data has changed + // eslint-disable-next-line react-hooks/exhaustive-deps + [data] + ); + const { currentPeriod, previousPeriod } = memoizedData; + + const timeseriesLatency = [ + currentPeriod, + comparisonEnabled && isTimeComparison(offset) ? previousPeriod : undefined, + ].filter(filterNil); + + const latencyMaxY = getMaxY(timeseriesLatency); + const latencyFormatter = getDurationFormatter(latencyMaxY); + + /* Latency Chart */ + + /* Throughput Chart */ + const { data: dataThroughput = INITIAL_STATE, status: statusThroughput } = + useFetcher( + (callApmApi) => { + if (serviceName && transactionType && start && end) { + return callApmApi( + 'GET /internal/apm/services/{serviceName}/throughput', + { + params: { + path: { + serviceName, + }, + query: { + environment, + kuery: '', + start, + end, + transactionType, + transactionName: undefined, + }, + }, + } + ); + } + }, + [environment, serviceName, start, end, transactionType] + ); + const { currentPeriodColor, previousPeriodColor } = getTimeSeriesColor( + ChartType.THROUGHPUT + ); + const timeseriesThroughput = [ + { + data: dataThroughput.currentPeriod, + type: 'linemark', + color: currentPeriodColor, + title: i18n.translate('xpack.apm.serviceOverview.throughtputChartTitle', { + defaultMessage: 'Throughput', + }), + }, + ...(comparisonEnabled + ? [ + { + data: dataThroughput.previousPeriod, + type: 'area', + color: previousPeriodColor, + title: '', + }, + ] + : []), + ]; + + /* Throughput Chart */ + + /* Error Rate */ + type ErrorRate = + APIReturnType<'GET /internal/apm/services/{serviceName}/transactions/charts/error_rate'>; + + const INITIAL_STATE_ERROR_RATE: ErrorRate = { + currentPeriod: { + timeseries: [], + average: null, + }, + previousPeriod: { + timeseries: [], + average: null, + }, + }; + function yLabelFormat(y?: number | null) { + return asPercent(y || 0, 1); + } + + const { + data: dataErrorRate = INITIAL_STATE_ERROR_RATE, + status: statusErrorRate, + } = useFetcher( + (callApmApi) => { + if (transactionType && serviceName && start && end) { + return callApmApi( + 'GET /internal/apm/services/{serviceName}/transactions/charts/error_rate', + { + params: { + path: { + serviceName, + }, + query: { + environment, + kuery: '', + start, + end, + transactionType, + transactionName: undefined, + }, + }, + } + ); + } + }, + [environment, serviceName, start, end, transactionType] + ); + + const { currentPeriodColor: currentPeriodColorErrorRate } = + getTimeSeriesColor(ChartType.FAILED_TRANSACTION_RATE); + + const timeseriesErrorRate = [ + { + data: dataErrorRate.currentPeriod.timeseries, + type: 'linemark', + color: currentPeriodColorErrorRate, + title: i18n.translate('xpack.apm.errorRate.chart.errorRate', { + defaultMessage: 'Failed transaction rate (avg.)', + }), + }, + ]; + + /* Error Rate */ + + return ( + + + + + + + +

+ {i18n.translate( + 'xpack.apm.dependencyLatencyChart.chartTitle', + { + defaultMessage: 'Latency', + } + )} +

+
+
+
+ +
+
+ + + + + + + + +

+ {i18n.translate( + 'xpack.apm.serviceOverview.throughtputChartTitle', + { defaultMessage: 'Throughput' } + )} +

+
+
+ + + + +
+ + +
+
+ + + + + +

+ {i18n.translate('xpack.apm.errorRate', { + defaultMessage: 'Failed transaction rate', + })} +

+
+
+ + + + +
+ + +
+
+
+
+
+
+ ); +} + +// eslint-disable-next-line import/no-default-export +export default AlertDetailsAppSection; diff --git a/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/helpers.ts b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/helpers.ts new file mode 100644 index 0000000000000..a095f8caa4574 --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/helpers.ts @@ -0,0 +1,16 @@ +/* + * 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 { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; + +export const getAggsTypeFromRule = ( + ruleAggType: string +): LatencyAggregationType => { + if (ruleAggType === '95th') return LatencyAggregationType.p95; + if (ruleAggType === '99th') return LatencyAggregationType.p99; + return LatencyAggregationType.avg; +}; diff --git a/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts new file mode 100644 index 0000000000000..0094d9332009a --- /dev/null +++ b/x-pack/plugins/apm/public/components/alerting/ui_components/alert_details_app_section/types.ts @@ -0,0 +1,24 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { Rule } from '@kbn/alerting-plugin/common'; +import { TopAlert } from '@kbn/observability-plugin/public/pages/alerts'; +import { TIME_UNITS } from '@kbn/triggers-actions-ui-plugin/public'; +import { LatencyAggregationType } from '../../../../../common/latency_aggregation_types'; + +export const SERVICE_NAME = 'service.name' as const; +export const TRANSACTION_TYPE = 'transaction.type' as const; +export interface AlertDetailsAppSectionProps { + rule: Rule<{ + environment: string; + aggregationType: LatencyAggregationType; + windowSize: number; + windowUnit: TIME_UNITS; + }>; + alert: TopAlert<{ [SERVICE_NAME]: string; [TRANSACTION_TYPE]: string }>; + timeZone: string; +} diff --git a/x-pack/plugins/apm/public/components/app/metrics/serverless_metrics/serverless_active_instances.tsx b/x-pack/plugins/apm/public/components/app/metrics/serverless_metrics/serverless_active_instances.tsx index 9c44d472c5b70..a0a72526e3632 100644 --- a/x-pack/plugins/apm/public/components/app/metrics/serverless_metrics/serverless_active_instances.tsx +++ b/x-pack/plugins/apm/public/components/app/metrics/serverless_metrics/serverless_active_instances.tsx @@ -27,7 +27,7 @@ import { useApmParams } from '../../../../hooks/use_apm_params'; import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; import { useTimeRange } from '../../../../hooks/use_time_range'; import { APIReturnType } from '../../../../services/rest/create_call_apm_api'; -import { TimeseriesChart } from '../../../shared/charts/timeseries_chart'; +import { TimeseriesChartWithContext } from '../../../shared/charts/timeseries_chart_with_context'; import { ListMetric } from '../../../shared/list_metric'; import { ServerlessFunctionNameLink } from './serverless_function_name_link'; @@ -201,7 +201,7 @@ export function ServerlessActiveInstances({ serverlessId }: Props) { - - - + - = [ { value: LatencyAggregationType.p99, text: '99th percentile' }, ]; -function filterNil(value: T | null | undefined): value is T { +export function filterNil(value: T | null | undefined): value is T { return value != null; } @@ -126,7 +126,7 @@ export function LatencyChart({ height, kuery }: Props) { - )} - >; - /** - * Formatter for y-axis tick values - */ - yLabelFormat: (y: number) => string; - /** - * Formatter for legend and tooltip values - */ - yTickFormat?: (y: number) => string; - showAnnotations?: boolean; - yDomain?: YDomainRange; - anomalyTimeseries?: AnomalyTimeseries; - customTheme?: Record; - anomalyTimeseriesColor?: string; -} +import { TimeseriesChartWithContextProps } from './timeseries_chart_with_context'; const END_ZONE_LABEL = i18n.translate('xpack.apm.timeseries.endzone', { defaultMessage: 'The selected time range does not include this entire bucket. It might contain partial data.', }); - +interface TimeseriesChartProps extends TimeseriesChartWithContextProps { + comparisonEnabled: boolean; + offset?: string; + timeZone: string; +} export function TimeseriesChart({ id, height = unit * 16, @@ -91,30 +64,22 @@ export function TimeseriesChart({ yDomain, anomalyTimeseries, customTheme = {}, -}: Props) { + comparisonEnabled, + offset, + timeZone, +}: TimeseriesChartProps) { const history = useHistory(); - const { core } = useApmPluginContext(); const { annotations } = useAnnotationsContext(); const { chartRef, updatePointerEvent } = useChartPointerEventContext(); const theme = useTheme(); const chartTheme = useChartTheme(); - const { - query: { comparisonEnabled, offset }, - } = useAnyOfApmParams( - '/services', - '/dependencies/*', - '/services/{serviceName}' - ); - const anomalyChartTimeseries = getChartAnomalyTimeseries({ anomalyTimeseries, theme, anomalyTimeseriesColor: anomalyTimeseries?.color, }); - const isEmpty = isTimeseriesEmpty(timeseries); const annotationColor = theme.eui.euiColorSuccess; - const isComparingExpectedBounds = comparisonEnabled && isExpectedBoundsComparison(offset); const allSeries = [ @@ -134,20 +99,14 @@ export function TimeseriesChart({ ); const xValues = timeseries.flatMap(({ data }) => data.map(({ x }) => x)); - const xValuesExpectedBounds = anomalyChartTimeseries?.boundaries?.flatMap(({ data }) => data.map(({ x }) => x) ) ?? []; - - const timeZone = getTimeZone(core.uiSettings); - const min = Math.min(...xValues); const max = Math.max(...xValues, ...xValuesExpectedBounds); const xFormatter = niceTimeFormatter([min, max]); - const xDomain = isEmpty ? { min: 0, max: 1 } : { min, max }; - // Using custom legendSort here when comparing expected bounds // because by default elastic-charts will show legends for expected bounds first // but for consistency, we are making `Expected bounds` last diff --git a/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart_with_context.tsx b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart_with_context.tsx new file mode 100644 index 0000000000000..5c9aac5d28bdf --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/charts/timeseries_chart_with_context.tsx @@ -0,0 +1,84 @@ +/* + * 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 { LegendItemListener, YDomainRange } from '@elastic/charts'; +import React from 'react'; +import { useAnyOfApmParams } from '../../../hooks/use_apm_params'; +import { ServiceAnomalyTimeseries } from '../../../../common/anomaly_detection/service_anomaly_timeseries'; +import { Coordinate, TimeSeries } from '../../../../typings/timeseries'; +import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { FETCH_STATUS } from '../../../hooks/use_fetcher'; +import { unit } from '../../../utils/style'; +import { getTimeZone } from './helper/timezone'; +import { TimeseriesChart } from './timeseries_chart'; + +interface AnomalyTimeseries extends ServiceAnomalyTimeseries { + color?: string; +} +export interface TimeseriesChartWithContextProps { + id: string; + fetchStatus: FETCH_STATUS; + height?: number; + onToggleLegend?: LegendItemListener; + timeseries: Array>; + /** + * Formatter for y-axis tick values + */ + yLabelFormat: (y: number) => string; + /** + * Formatter for legend and tooltip values + */ + yTickFormat?: (y: number) => string; + showAnnotations?: boolean; + yDomain?: YDomainRange; + anomalyTimeseries?: AnomalyTimeseries; + customTheme?: Record; + anomalyTimeseriesColor?: string; +} + +export function TimeseriesChartWithContext({ + id, + height = unit * 16, + fetchStatus, + onToggleLegend, + timeseries, + yLabelFormat, + yTickFormat, + showAnnotations = true, + yDomain, + anomalyTimeseries, + customTheme = {}, +}: TimeseriesChartWithContextProps) { + const { + query: { comparisonEnabled, offset }, + } = useAnyOfApmParams( + '/services', + '/dependencies/*', + '/services/{serviceName}' + ); + const { core } = useApmPluginContext(); + const timeZone = getTimeZone(core.uiSettings); + + return ( + + ); +} diff --git a/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx b/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx index a3143bb7b6849..3dfc22a1c2809 100644 --- a/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/charts/transaction_coldstart_rate_chart/index.tsx @@ -20,7 +20,7 @@ import { APIReturnType } from '../../../../services/rest/create_call_apm_api'; import { asPercent } from '../../../../../common/utils/formatters'; import { useFetcher } from '../../../../hooks/use_fetcher'; import { useTheme } from '../../../../hooks/use_theme'; -import { TimeseriesChart } from '../timeseries_chart'; +import { TimeseriesChartWithContext } from '../timeseries_chart_with_context'; import { useApmServiceContext } from '../../../../context/apm_service/use_apm_service_context'; import { getComparisonChartTheme } from '../../time_comparison/get_comparison_chart_theme'; import { useApmParams } from '../../../../hooks/use_apm_params'; @@ -161,7 +161,7 @@ export function TransactionColdstartRateChart({ /> - !!transactionType && transactionTypes.includes(transactionType); + +const isNoAgentAndNoTransactionTypes = ({ + transactionTypes, + agentName, +}: { + transactionTypes: string[]; + agentName?: string; +}): boolean => !agentName || transactionTypes.length === 0; + +export function getTransactionType({ transactionType, transactionTypes, agentName, - history, }: { transactionType?: string; transactionTypes: string[]; agentName?: string; - history: History; -}) { - if (transactionType && transactionTypes.includes(transactionType)) { - return transactionType; - } +}): string | undefined { + const isTransactionTypeExists = isTypeExistsInTransactionTypesList({ + transactionType, + transactionTypes, + }); - if (!agentName || transactionTypes.length === 0) { - return; - } + if (isTransactionTypeExists) return transactionType; + + const isNoAgentAndNoTransactionTypesExists = isNoAgentAndNoTransactionTypes({ + transactionTypes, + agentName, + }); + + if (isNoAgentAndNoTransactionTypesExists) return undefined; // The default transaction type is "page-load" for RUM agents and "request" for all others const defaultTransactionType = isRumAgentName(agentName) @@ -127,7 +147,42 @@ export function getOrRedirectToTransactionType({ ? defaultTransactionType : transactionTypes[0]; + return currentTransactionType; +} + +export function getOrRedirectToTransactionType({ + transactionType, + transactionTypes, + agentName, + history, +}: { + transactionType?: string; + transactionTypes: string[]; + agentName?: string; + history: History; +}) { + const isTransactionTypeExists = isTypeExistsInTransactionTypesList({ + transactionType, + transactionTypes, + }); + + if (isTransactionTypeExists) return transactionType; + + const isNoAgentAndNoTransactionTypesExists = isNoAgentAndNoTransactionTypes({ + transactionTypes, + agentName, + }); + + if (isNoAgentAndNoTransactionTypesExists) return undefined; + + const currentTransactionType = getTransactionType({ + transactionTypes, + transactionType, + agentName, + }); + // Replace transactionType in the URL in case it is not one of the types returned by the API - replace(history, { query: { transactionType: currentTransactionType } }); + replace(history, { query: { transactionType: currentTransactionType! } }); + return currentTransactionType; } diff --git a/x-pack/plugins/observability/public/hooks/use_fetch_rule.ts b/x-pack/plugins/observability/public/hooks/use_fetch_rule.ts index 07f13b4c80e7e..65773198b6843 100644 --- a/x-pack/plugins/observability/public/hooks/use_fetch_rule.ts +++ b/x-pack/plugins/observability/public/hooks/use_fetch_rule.ts @@ -16,8 +16,10 @@ export function useFetchRule({ ruleId, http }: FetchRuleProps) { rule: undefined, errorRule: undefined, }); + const fetchRuleSummary = useCallback(async () => { try { + if (!ruleId) return; const rule = await loadRule({ http, ruleId, diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.test.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.test.tsx index 20dc5caf43d70..eb1865c6f3100 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.test.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.test.tsx @@ -5,14 +5,12 @@ * 2.0. */ -import React from 'react'; +import React, { Fragment } from 'react'; import * as useUiSettingHook from '@kbn/kibana-react-plugin/public/ui_settings/use_ui_setting'; import { useParams } from 'react-router-dom'; import { Chance } from 'chance'; import { waitFor } from '@testing-library/react'; import { casesPluginMock } from '@kbn/cases-plugin/public/mocks'; -import { triggersActionsUiMock } from '@kbn/triggers-actions-ui-plugin/public/mocks'; - import { render } from '../../../utils/test_helper'; import { useKibana } from '../../../utils/kibana_react'; import { kibanaStartMock } from '../../../utils/kibana_react.mock'; @@ -21,6 +19,8 @@ import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs'; import { AlertDetails } from './alert_details'; import { ConfigSchema } from '../../../plugin'; import { alert, alertWithNoData } from '../mock/alert'; +import { ruleTypeRegistryMock } from '@kbn/triggers-actions-ui-plugin/public/application/rule_type_registry.mock'; +import { RuleTypeModel, ValidationResult } from '@kbn/triggers-actions-ui-plugin/public'; jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), @@ -28,6 +28,17 @@ jest.mock('react-router-dom', () => ({ })); jest.mock('../../../utils/kibana_react'); +const validationMethod = (): ValidationResult => ({ errors: {} }); +const ruleType: RuleTypeModel = { + id: 'logs.alert.document.count', + iconClass: 'test', + description: 'Testing', + documentationUrl: 'https://...', + requiresAppContext: false, + validate: validationMethod, + ruleParamsExpression: () => , +}; +const ruleTypeRegistry = ruleTypeRegistryMock.create(); const useKibanaMock = useKibana as jest.Mock; @@ -41,12 +52,25 @@ const mockKibana = () => { prepend: jest.fn(), }, }, - triggersActionsUi: triggersActionsUiMock.createStart(), + triggersActionsUi: { + ruleTypeRegistry, + }, }, }); }; jest.mock('../../../hooks/use_fetch_alert_detail'); +jest.mock('../../../hooks/use_fetch_rule', () => { + return { + useFetchRule: () => ({ + reloadRule: jest.fn(), + rule: { + id: 'ruleId', + name: 'ruleName', + }, + }), + }; +}); jest.mock('../../../hooks/use_breadcrumbs'); jest.mock('../../../hooks/use_get_user_cases_permissions', () => ({ useGetUserCasesPermissions: () => ({ @@ -89,6 +113,9 @@ describe('Alert details', () => { jest.clearAllMocks(); useParamsMock.mockReturnValue(params); useBreadcrumbsMock.mockReturnValue([]); + ruleTypeRegistry.list.mockReturnValue([ruleType]); + ruleTypeRegistry.get.mockReturnValue(ruleType); + ruleTypeRegistry.has.mockReturnValue(true); mockKibana(); }); diff --git a/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.tsx b/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.tsx index f1515823442be..58a32eb89f130 100644 --- a/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.tsx +++ b/x-pack/plugins/observability/public/pages/alert_details/components/alert_details.tsx @@ -5,11 +5,15 @@ * 2.0. */ -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { i18n } from '@kbn/i18n'; import { useParams } from 'react-router-dom'; -import { EuiEmptyPrompt, EuiPanel } from '@elastic/eui'; +import { EuiEmptyPrompt, EuiPanel, EuiSpacer } from '@elastic/eui'; +import { ALERT_RULE_TYPE_ID, ALERT_RULE_UUID } from '@kbn/rule-data-utils'; +import { RuleTypeModel } from '@kbn/triggers-actions-ui-plugin/public'; +import { getTimeZone } from '../../../utils/get_time_zone'; +import { useFetchRule } from '../../../hooks/use_fetch_rule'; import { isAlertDetailsEnabledPerApp } from '../../../utils/is_alert_details_enabled'; import { useKibana } from '../../../utils/kibana_react'; import { usePluginContext } from '../../../hooks/use_plugin_context'; @@ -27,19 +31,31 @@ import { paths } from '../../../config/paths'; export function AlertDetails() { const { + uiSettings, http, cases: { helpers: { canUseCases }, ui: { getCasesContext }, }, + triggersActionsUi: { ruleTypeRegistry }, } = useKibana().services; + const { ObservabilityPageTemplate, config } = usePluginContext(); const { alertId } = useParams(); const [isLoading, alert] = useFetchAlertDetail(alertId); - + const [ruleTypeModel, setRuleTypeModel] = useState(null); const CasesContext = getCasesContext(); const userCasesPermissions = canUseCases(); + const { rule } = useFetchRule({ + ruleId: alert?.fields[ALERT_RULE_UUID], + http, + }); + useEffect(() => { + if (alert) { + setRuleTypeModel(ruleTypeRegistry.get(alert?.fields[ALERT_RULE_TYPE_ID]!)); + } + }, [alert, ruleTypeRegistry]); useBreadcrumbs([ { href: http.basePath.prepend(paths.observability.alerts), @@ -81,7 +97,8 @@ export function AlertDetails() { /> ); - + const AlertDetailsAppSection = ruleTypeModel ? ruleTypeModel.alertDetailsAppSection : null; + const timeZone = getTimeZone(uiSettings); return ( + + {AlertDetailsAppSection && rule && ( + + )} ); } diff --git a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/types.ts b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/types.ts index d94025fe79b80..6f3a308c0b92a 100644 --- a/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/types.ts +++ b/x-pack/plugins/observability/public/pages/alerts/containers/alerts_page/types.ts @@ -16,8 +16,8 @@ export interface RuleStatsState { snoozed: number; } -export interface TopAlert { - fields: ParsedTechnicalFields & ParsedExperimentalFields; +export interface TopAlert = {}> { + fields: ParsedTechnicalFields & ParsedExperimentalFields & TAdditionalMetaFields; start: number; lastUpdated: number; reason: string; diff --git a/x-pack/plugins/observability/public/pages/rule_details/types.ts b/x-pack/plugins/observability/public/pages/rule_details/types.ts index ead002266e53e..07407ca217444 100644 --- a/x-pack/plugins/observability/public/pages/rule_details/types.ts +++ b/x-pack/plugins/observability/public/pages/rule_details/types.ts @@ -19,7 +19,7 @@ export interface PageHeaderProps { } export interface FetchRuleProps { - ruleId: string; + ruleId?: string; http: HttpSetup; } diff --git a/x-pack/plugins/observability/public/utils/get_time_zone.ts b/x-pack/plugins/observability/public/utils/get_time_zone.ts new file mode 100644 index 0000000000000..eccd86d837804 --- /dev/null +++ b/x-pack/plugins/observability/public/utils/get_time_zone.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { IUiSettingsClient } from '@kbn/core-ui-settings-browser'; +import { UI_SETTINGS } from '../hooks/use_kibana_ui_settings'; + +export function getTimeZone(uiSettings?: IUiSettingsClient) { + const kibanaTimeZone = uiSettings?.get<'Browser' | string>(UI_SETTINGS.DATEFORMAT_TZ); + + if (!kibanaTimeZone || kibanaTimeZone === 'Browser') { + return 'local'; + } + + return kibanaTimeZone; +} diff --git a/x-pack/plugins/triggers_actions_ui/public/types.ts b/x-pack/plugins/triggers_actions_ui/public/types.ts index 3b362b2454af1..f0d2b8e2e2aea 100644 --- a/x-pack/plugins/triggers_actions_ui/public/types.ts +++ b/x-pack/plugins/triggers_actions_ui/public/types.ts @@ -364,6 +364,9 @@ export interface RuleTypeModel { requiresAppContext: boolean; defaultActionMessage?: string; defaultRecoveryMessage?: string; + alertDetailsAppSection?: + | React.FunctionComponent + | React.LazyExoticComponent>; } export interface IErrorObject {