diff --git a/x-pack/plugins/alerting/server/usage/alerting_usage_collector.ts b/x-pack/plugins/alerting/server/usage/alerting_usage_collector.ts index d03faa3aaf65f..6e6eb3cb43f0d 100644 --- a/x-pack/plugins/alerting/server/usage/alerting_usage_collector.ts +++ b/x-pack/plugins/alerting/server/usage/alerting_usage_collector.ts @@ -110,6 +110,18 @@ const byTaskStatusSchemaByType: MakeSchemaFrom['count_failed_and_ unrecognized: byTypeSchema, }; +const byStatusSchema: MakeSchemaFrom['count_rules_by_execution_status'] = { + success: { type: 'long' }, + error: { type: 'long' }, + warning: { type: 'long' }, +}; + +const byNotifyWhenSchema: MakeSchemaFrom['count_rules_by_notify_when'] = { + on_action_group_change: { type: 'long' }, + on_active_alert: { type: 'long' }, + on_throttle_interval: { type: 'long' }, +}; + export function createAlertingUsageCollector( usageCollection: UsageCollectionSetup, taskManager: Promise @@ -173,6 +185,21 @@ export function createAlertingUsageCollector( count_failed_and_unrecognized_rule_tasks_per_day: 0, count_failed_and_unrecognized_rule_tasks_by_status_per_day: {}, count_failed_and_unrecognized_rule_tasks_by_status_by_type_per_day: {}, + count_rules_by_execution_status: { + success: 0, + warning: 0, + error: 0, + }, + count_rules_by_notify_when: { + on_action_group_change: 0, + on_active_alert: 0, + on_throttle_interval: 0, + }, + count_rules_with_tags: 0, + count_rules_snoozed: 0, + count_rules_muted: 0, + count_rules_with_muted_alerts: 0, + count_connector_types_by_consumers: {}, avg_execution_time_per_day: 0, avg_execution_time_by_type_per_day: {}, avg_es_search_duration_per_day: 0, @@ -249,6 +276,13 @@ export function createAlertingUsageCollector( count_failed_and_unrecognized_rule_tasks_per_day: { type: 'long' }, count_failed_and_unrecognized_rule_tasks_by_status_per_day: byTaskStatusSchema, count_failed_and_unrecognized_rule_tasks_by_status_by_type_per_day: byTaskStatusSchemaByType, + count_rules_by_execution_status: byStatusSchema, + count_rules_with_tags: { type: 'long' }, + count_rules_by_notify_when: byNotifyWhenSchema, + count_rules_snoozed: { type: 'long' }, + count_rules_muted: { type: 'long' }, + count_rules_with_muted_alerts: { type: 'long' }, + count_connector_types_by_consumers: { DYNAMIC_KEY: { DYNAMIC_KEY: { type: 'long' } } }, avg_execution_time_per_day: { type: 'long' }, avg_execution_time_by_type_per_day: byTypeSchema, avg_es_search_duration_per_day: { type: 'long' }, diff --git a/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.test.ts b/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.test.ts index 79b5d473ebe05..d16b76d91afa9 100644 --- a/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.test.ts +++ b/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.test.ts @@ -55,6 +55,90 @@ describe('kibana index telemetry', () => { }, ], }, + by_execution_status: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'unknown', + doc_count: 0, + }, + { + key: 'ok', + doc_count: 1, + }, + { + key: 'active', + doc_count: 2, + }, + { + key: 'pending', + doc_count: 3, + }, + { + key: 'error', + doc_count: 4, + }, + { + key: 'warning', + doc_count: 5, + }, + ], + }, + by_notify_when: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'onActionGroupChange', + doc_count: 5, + }, + { + key: 'onActiveAlert', + doc_count: 6, + }, + { + key: 'onThrottleInterval', + doc_count: 7, + }, + ], + }, + connector_types_by_consumers: { + doc_count_error_upper_bound: 0, + sum_other_doc_count: 0, + buckets: [ + { + key: 'alerts', + actions: { + connector_types: { + buckets: [ + { + key: '.server-log', + doc_count: 2, + }, + { + key: '.email', + doc_count: 3, + }, + ], + }, + }, + }, + { + key: 'siem', + actions: { + connector_types: { + buckets: [ + { + key: '.index', + doc_count: 4, + }, + ], + }, + }, + }, + ], + }, max_throttle_time: { value: 60 }, min_throttle_time: { value: 0 }, avg_throttle_time: { value: 30 }, @@ -64,6 +148,10 @@ describe('kibana index telemetry', () => { max_actions_count: { value: 4 }, min_actions_count: { value: 0 }, avg_actions_count: { value: 2.5 }, + sum_rules_with_tags: { value: 10 }, + sum_rules_snoozed: { value: 11 }, + sum_rules_muted: { value: 12 }, + sum_rules_with_muted_alerts: { value: 13 }, }, }); @@ -109,6 +197,29 @@ describe('kibana index telemetry', () => { max: 60, min: 0, }, + count_rules_by_execution_status: { + success: 3, + error: 4, + warning: 5, + }, + count_rules_with_tags: 10, + count_rules_by_notify_when: { + on_action_group_change: 5, + on_active_alert: 6, + on_throttle_interval: 7, + }, + count_rules_snoozed: 11, + count_rules_muted: 12, + count_rules_with_muted_alerts: 13, + count_connector_types_by_consumers: { + alerts: { + __email: 3, + '__server-log': 2, + }, + siem: { + __index: 4, + }, + }, }); }); @@ -138,6 +249,20 @@ describe('kibana index telemetry', () => { min: 0, }, count_by_type: {}, + count_rules_by_execution_status: { + success: 0, + error: 0, + warning: 0, + }, + count_rules_with_tags: 0, + count_rules_by_notify_when: { + on_action_group_change: 0, + on_active_alert: 0, + on_throttle_interval: 0, + }, + count_rules_snoozed: 0, + count_rules_muted: 0, + count_rules_with_muted_alerts: 0, count_total: 0, schedule_time: { avg: '0s', @@ -159,6 +284,7 @@ describe('kibana index telemetry', () => { max: 0, min: 0, }, + count_connector_types_by_consumers: {}, }); }); }); diff --git a/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.ts b/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.ts index 5443fb91e2e1f..0c6d01016c313 100644 --- a/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.ts +++ b/x-pack/plugins/alerting/server/usage/lib/get_telemetry_from_kibana.ts @@ -12,6 +12,13 @@ import type { AggregationsStringTermsBucketKeys, } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; import { ElasticsearchClient, Logger } from '@kbn/core/server'; + +import { + ConnectorsByConsumersBucket, + groupConnectorsByConsumers, +} from './group_connectors_by_consumers'; +import { groupRulesByNotifyWhen } from './group_rules_by_notify_when'; +import { groupRulesByStatus } from './group_rules_by_status'; import { AlertingUsage } from '../types'; import { NUM_ALERTING_RULE_TYPES } from '../alerting_usage_collector'; import { parseSimpleRuleTypeBucket } from './parse_simple_rule_type_bucket'; @@ -26,6 +33,13 @@ type GetTotalCountsResults = Pick< AlertingUsage, | 'count_total' | 'count_by_type' + | 'count_rules_by_execution_status' + | 'count_rules_by_notify_when' + | 'count_rules_with_tags' + | 'count_rules_snoozed' + | 'count_rules_muted' + | 'count_rules_with_muted_alerts' + | 'count_connector_types_by_consumers' | 'throttle_time' | 'schedule_time' | 'throttle_time_number_s' @@ -145,6 +159,59 @@ export async function getTotalCountAggregations({ `, }, }, + rule_with_tags: { + type: 'long', + script: { + source: ` + def rule = params._source['alert']; + if (rule != null && rule.tags != null) { + if (rule.tags.size() > 0) { + emit(1); + } else { + emit(0); + } + }`, + }, + }, + rule_snoozed: { + type: 'long', + script: { + source: ` + def rule = params._source['alert']; + if (rule != null && rule.snoozeSchedule != null) { + if (rule.snoozeSchedule.size() > 0) { + emit(1); + } else { + emit(0); + } + }`, + }, + }, + rule_muted: { + type: 'long', + script: { + source: ` + if (doc['alert.muteAll'].value == true) { + emit(1); + } else { + emit(0); + }`, + }, + }, + rule_with_muted_alerts: { + type: 'long', + script: { + source: ` + def rule = params._source['alert']; + if (rule != null && rule.mutedInstanceIds != null) { + if (rule.mutedInstanceIds.size() > 0) { + emit(1); + } else { + emit(0); + } + }`, + }, + }, }, aggs: { by_rule_type_id: { @@ -162,6 +229,39 @@ export async function getTotalCountAggregations({ max_actions_count: { max: { field: 'rule_action_count' } }, min_actions_count: { min: { field: 'rule_action_count' } }, avg_actions_count: { avg: { field: 'rule_action_count' } }, + by_execution_status: { + terms: { + field: 'alert.executionStatus.status', + }, + }, + by_notify_when: { + terms: { + field: 'alert.notifyWhen', + }, + }, + connector_types_by_consumers: { + terms: { + field: 'alert.consumer', + }, + aggs: { + actions: { + nested: { + path: 'alert.actions', + }, + aggs: { + connector_types: { + terms: { + field: 'alert.actions.actionTypeId', + }, + }, + }, + }, + }, + }, + sum_rules_with_tags: { sum: { field: 'rule_with_tags' } }, + sum_rules_snoozed: { sum: { field: 'rule_snoozed' } }, + sum_rules_muted: { sum: { field: 'rule_muted' } }, + sum_rules_with_muted_alerts: { sum: { field: 'rule_with_muted_alerts' } }, }, }, }; @@ -182,15 +282,41 @@ export async function getTotalCountAggregations({ max_actions_count: AggregationsSingleMetricAggregateBase; min_actions_count: AggregationsSingleMetricAggregateBase; avg_actions_count: AggregationsSingleMetricAggregateBase; + by_execution_status: AggregationsTermsAggregateBase; + by_notify_when: AggregationsTermsAggregateBase; + connector_types_by_consumers: AggregationsTermsAggregateBase; + sum_rules_with_tags: AggregationsSingleMetricAggregateBase; + sum_rules_snoozed: AggregationsSingleMetricAggregateBase; + sum_rules_muted: AggregationsSingleMetricAggregateBase; + sum_rules_with_muted_alerts: AggregationsSingleMetricAggregateBase; }; const totalRulesCount = typeof results.hits.total === 'number' ? results.hits.total : results.hits.total?.value; + const countRulesByExecutionStatus = groupRulesByStatus( + parseSimpleRuleTypeBucket(aggregations.by_execution_status.buckets) + ); + + const countRulesByNotifyWhen = groupRulesByNotifyWhen( + parseSimpleRuleTypeBucket(aggregations.by_notify_when.buckets) + ); + + const countConnectorTypesByConsumers = groupConnectorsByConsumers( + aggregations.connector_types_by_consumers.buckets + ); + return { hasErrors: false, count_total: totalRulesCount ?? 0, count_by_type: parseSimpleRuleTypeBucket(aggregations.by_rule_type_id.buckets), + count_rules_by_execution_status: countRulesByExecutionStatus, + count_rules_with_tags: aggregations.sum_rules_with_tags.value ?? 0, + count_rules_by_notify_when: countRulesByNotifyWhen, + count_rules_snoozed: aggregations.sum_rules_snoozed.value ?? 0, + count_rules_muted: aggregations.sum_rules_muted.value ?? 0, + count_rules_with_muted_alerts: aggregations.sum_rules_with_muted_alerts.value ?? 0, + count_connector_types_by_consumers: countConnectorTypesByConsumers, throttle_time: { min: `${aggregations.min_throttle_time.value ?? 0}s`, avg: `${aggregations.avg_throttle_time.value ?? 0}s`, @@ -232,6 +358,17 @@ export async function getTotalCountAggregations({ errorMessage, count_total: 0, count_by_type: {}, + count_rules_by_execution_status: { success: 0, error: 0, warning: 0 }, + count_rules_by_notify_when: { + on_throttle_interval: 0, + on_active_alert: 0, + on_action_group_change: 0, + }, + count_rules_with_tags: 0, + count_rules_snoozed: 0, + count_rules_muted: 0, + count_rules_with_muted_alerts: 0, + count_connector_types_by_consumers: {}, throttle_time: { min: '0s', avg: '0s', diff --git a/x-pack/plugins/alerting/server/usage/lib/group_connectors_by_consumers.ts b/x-pack/plugins/alerting/server/usage/lib/group_connectors_by_consumers.ts new file mode 100644 index 0000000000000..b189e2868b418 --- /dev/null +++ b/x-pack/plugins/alerting/server/usage/lib/group_connectors_by_consumers.ts @@ -0,0 +1,27 @@ +/* + * 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 { AggregationsBuckets } from '@elastic/elasticsearch/lib/api/typesWithBodyKey'; +import { replaceDotSymbols } from './replace_dots_with_underscores'; + +export interface ConnectorsByConsumersBucket { + key: string; + actions: { connector_types: { buckets: Array<{ key: string; doc_count: number }> } }; +} + +export function groupConnectorsByConsumers( + consumers: AggregationsBuckets +) { + return (consumers as ConnectorsByConsumersBucket[]).reduce((acc, consumer) => { + return { + ...acc, + [consumer.key]: consumer.actions.connector_types.buckets.reduce((accBucket, bucket) => { + return { ...accBucket, [replaceDotSymbols(bucket.key)]: bucket.doc_count }; + }, {}), + }; + }, {}); +} diff --git a/x-pack/plugins/alerting/server/usage/lib/group_rules_by_notify_when.test.ts b/x-pack/plugins/alerting/server/usage/lib/group_rules_by_notify_when.test.ts new file mode 100644 index 0000000000000..bacb5114b8ddd --- /dev/null +++ b/x-pack/plugins/alerting/server/usage/lib/group_rules_by_notify_when.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { groupRulesByNotifyWhen } from './group_rules_by_notify_when'; + +describe('groupRulesByNotifyWhen', () => { + test('should correctly group rules by combining ok and active statuses', () => { + expect( + groupRulesByNotifyWhen({ + onActionGroupChange: 1, + onActiveAlert: 2, + onThrottleInterval: 3, + foo: 5, + }) + ).toEqual({ + on_action_group_change: 1, + on_active_alert: 2, + on_throttle_interval: 3, + }); + }); + + test('should fallback to 0 if any of the expected statuses are absent', () => { + expect(groupRulesByNotifyWhen({ unknown: 100, bar: 300 })).toEqual({ + on_action_group_change: 0, + on_active_alert: 0, + on_throttle_interval: 0, + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/usage/lib/group_rules_by_notify_when.ts b/x-pack/plugins/alerting/server/usage/lib/group_rules_by_notify_when.ts new file mode 100644 index 0000000000000..3adc9c73f9a72 --- /dev/null +++ b/x-pack/plugins/alerting/server/usage/lib/group_rules_by_notify_when.ts @@ -0,0 +1,18 @@ +/* + * 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 { AlertingUsage } from '../types'; + +export function groupRulesByNotifyWhen( + rulesByNotifyWhen: Record +): AlertingUsage['count_rules_by_notify_when'] { + return { + on_action_group_change: rulesByNotifyWhen.onActionGroupChange ?? 0, + on_active_alert: rulesByNotifyWhen.onActiveAlert ?? 0, + on_throttle_interval: rulesByNotifyWhen.onThrottleInterval ?? 0, + }; +} diff --git a/x-pack/plugins/alerting/server/usage/lib/group_rules_by_status.test.ts b/x-pack/plugins/alerting/server/usage/lib/group_rules_by_status.test.ts new file mode 100644 index 0000000000000..f5f24908a3d12 --- /dev/null +++ b/x-pack/plugins/alerting/server/usage/lib/group_rules_by_status.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 + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { groupRulesByStatus } from './group_rules_by_status'; + +describe('groupRulesByStatus', () => { + test('should correctly group rules by combining ok and active statuses', () => { + expect( + groupRulesByStatus({ ok: 3, active: 3, error: 4, warning: 5, unknown: 100, pending: 300 }) + ).toEqual({ + success: 6, + error: 4, + warning: 5, + }); + }); + + test('should fallback to 0 if any of the expected statuses are absent', () => { + expect(groupRulesByStatus({ unknown: 100, pending: 300 })).toEqual({ + success: 0, + error: 0, + warning: 0, + }); + }); +}); diff --git a/x-pack/plugins/alerting/server/usage/lib/group_rules_by_status.ts b/x-pack/plugins/alerting/server/usage/lib/group_rules_by_status.ts new file mode 100644 index 0000000000000..a4b114b0cb85f --- /dev/null +++ b/x-pack/plugins/alerting/server/usage/lib/group_rules_by_status.ts @@ -0,0 +1,21 @@ +/* + * 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 { AlertingUsage } from '../types'; + +export function groupRulesByStatus( + rulesByStatus: Record +): AlertingUsage['count_rules_by_execution_status'] { + const ok = rulesByStatus.ok || 0; + const active = rulesByStatus.active || 0; + + return { + success: ok + active, + error: rulesByStatus.error || 0, + warning: rulesByStatus.warning || 0, + }; +} diff --git a/x-pack/plugins/alerting/server/usage/task.ts b/x-pack/plugins/alerting/server/usage/task.ts index 0bbfab30f0796..4f835f3c4a64f 100644 --- a/x-pack/plugins/alerting/server/usage/task.ts +++ b/x-pack/plugins/alerting/server/usage/task.ts @@ -141,6 +141,16 @@ export function telemetryTaskRunner( count_active_by_type: totalInUse.countByType, count_active_total: totalInUse.countTotal, count_disabled_total: totalCountAggregations.count_total - totalInUse.countTotal, + count_rules_by_execution_status: + totalCountAggregations.count_rules_by_execution_status, + count_rules_with_tags: totalCountAggregations.count_rules_with_tags, + count_rules_by_notify_when: totalCountAggregations.count_rules_by_notify_when, + count_rules_snoozed: totalCountAggregations.count_rules_snoozed, + count_rules_muted: totalCountAggregations.count_rules_muted, + count_rules_with_muted_alerts: + totalCountAggregations.count_rules_with_muted_alerts, + count_connector_types_by_consumers: + totalCountAggregations.count_connector_types_by_consumers, count_rules_namespaces: totalInUse.countNamespaces, count_rules_executions_per_day: dailyExecutionCounts.countTotalRuleExecutions, count_rules_executions_by_type_per_day: diff --git a/x-pack/plugins/alerting/server/usage/types.ts b/x-pack/plugins/alerting/server/usage/types.ts index a0f45d1932309..29a32de29bc1a 100644 --- a/x-pack/plugins/alerting/server/usage/types.ts +++ b/x-pack/plugins/alerting/server/usage/types.ts @@ -27,6 +27,21 @@ export interface AlertingUsage { string, Record >; + count_rules_by_execution_status: { + success: number; + error: number; + warning: number; + }; + count_rules_with_tags: number; + count_rules_by_notify_when: { + on_action_group_change: number; + on_active_alert: number; + on_throttle_interval: number; + }; + count_connector_types_by_consumers: Record>; + count_rules_snoozed: number; + count_rules_muted: number; + count_rules_with_muted_alerts: number; percentile_num_generated_actions_per_day: { p50: number; p90: number; diff --git a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json index 3cbe97e096ac5..797b290d245c5 100644 --- a/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json +++ b/x-pack/plugins/telemetry_collection_xpack/schema/xpack_plugins.json @@ -1651,6 +1651,55 @@ } } }, + "count_rules_by_execution_status": { + "properties": { + "success": { + "type": "long" + }, + "error": { + "type": "long" + }, + "warning": { + "type": "long" + } + } + }, + "count_rules_with_tags": { + "type": "long" + }, + "count_rules_by_notify_when": { + "properties": { + "on_action_group_change": { + "type": "long" + }, + "on_active_alert": { + "type": "long" + }, + "on_throttle_interval": { + "type": "long" + } + } + }, + "count_rules_snoozed": { + "type": "long" + }, + "count_rules_muted": { + "type": "long" + }, + "count_rules_with_muted_alerts": { + "type": "long" + }, + "count_connector_types_by_consumers": { + "properties": { + "DYNAMIC_KEY": { + "properties": { + "DYNAMIC_KEY": { + "type": "long" + } + } + } + } + }, "avg_execution_time_per_day": { "type": "long" }, diff --git a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts index d77c8858feaf5..7e6aec1347753 100644 --- a/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts +++ b/x-pack/test/alerting_api_integration/security_and_spaces/group2/tests/telemetry/alerting_and_actions_telemetry.ts @@ -502,6 +502,32 @@ export default function createAlertingAndActionsTelemetryTests({ getService }: F expect( telemetry.percentile_num_alerts_by_type_per_day.p99['test__cumulative-firing'] ).to.be.greaterThan(0); + + // rules grouped by execution status + expect(telemetry.count_rules_by_execution_status).to.eql({ + success: 15, + error: 3, + warning: 0, + }); + // number of rules that has tags + expect(telemetry.count_rules_with_tags).to.be(21); + // rules grouped by notify when + expect(telemetry.count_rules_by_notify_when).to.eql({ + on_action_group_change: 0, + on_active_alert: 6, + on_throttle_interval: 15, + }); + // rules snoozed + expect(telemetry.count_rules_snoozed).to.be(0); + // rules muted + expect(telemetry.count_rules_muted).to.be(0); + // rules with muted alerts + expect(telemetry.count_rules_with_muted_alerts).to.be(0); + // Connector types grouped by consumers + expect(telemetry.count_connector_types_by_consumers).to.eql({ + // eslint-disable-next-line @typescript-eslint/naming-convention + alertsFixture: { test__noop: 9, test__throw: 9, __slack: 3 }, + }); } it('should retrieve telemetry data in the expected format', async () => {