[RAM] add observability feature for server less (#168636)

## Summary

FIX => https://github.com/elastic/kibana/issues/168034


### Checklist

- [ ] [Unit or functional
tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html)
were updated or added to match the most common scenarios

---------

Co-authored-by: mgiota <panagiota.mitsopoulou@elastic.co>
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Xavier Mouligneau 2023-10-31 17:27:53 -04:00 committed by GitHub
parent fea32812c8
commit a35f91e3a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
64 changed files with 787 additions and 85 deletions

View file

@ -25,6 +25,11 @@ xpack.serverless.plugin.developer.projectSwitcher.currentType: 'observability'
## Disable adding the component template `.fleet_agent_id_verification-1` to every index template for each datastream for each integration
xpack.fleet.agentIdVerificationEnabled: false
## Enable the capability for the observability feature ID in the serverless environment to take ownership of the rules.
## The value need to be a featureId observability Or stackAlerts Or siem
xpack.alerting.rules.overwriteProducer: 'observability'
xpack.observability.createO11yGenericFeatureId: true
## APM Serverless Onboarding flow
xpack.apm.serverlessOnboarding: true

View file

@ -7,3 +7,10 @@
*/
export const OBSERVABILITY_THRESHOLD_RULE_TYPE_ID = 'observability.rules.custom_threshold';
export enum ApmRuleType {
ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat.
TransactionErrorRate = 'apm.transaction_error_rate',
TransactionDuration = 'apm.transaction_duration',
Anomaly = 'apm.anomaly',
}

View file

@ -39,6 +39,13 @@ const rulesSchema = schema.object({
enforce: schema.boolean({ defaultValue: false }), // if enforce is false, only warnings will be shown
}),
maxScheduledPerMinute: schema.number({ defaultValue: 10000, max: 10000, min: 0 }),
overwriteProducer: schema.maybe(
schema.oneOf([
schema.literal('observability'),
schema.literal('siem'),
schema.literal('stackAlerts'),
])
),
run: schema.object({
timeout: schema.maybe(schema.string({ validate: validateDurationSchema })),
actions: schema.object({

View file

@ -15,6 +15,7 @@ import { ILicenseState } from './license_state';
import { licenseStateMock } from './license_state.mock';
import { schema } from '@kbn/config-schema';
import { createGetAlertIndicesAliasFn } from './create_get_alert_indices_alias';
import { AlertingConfig } from '../config';
describe('createGetAlertIndicesAliasFn', () => {
const logger = loggingSystemMock.create().get();
@ -23,6 +24,7 @@ describe('createGetAlertIndicesAliasFn', () => {
const inMemoryMetrics = inMemoryMetricsMock.create();
const ruleTypeRegistryParams: ConstructorOptions = {
config: {} as AlertingConfig,
logger,
taskManager,
taskRunnerFactory: new TaskRunnerFactory(),

View file

@ -296,6 +296,7 @@ export class AlertingPlugin {
}
const ruleTypeRegistry = new RuleTypeRegistry({
config: this.config,
logger: this.logger,
taskManager: plugins.taskManager,
taskRunnerFactory: this.taskRunnerFactory,

View file

@ -17,6 +17,7 @@ import { inMemoryMetricsMock } from './monitoring/in_memory_metrics.mock';
import { alertsServiceMock } from './alerts_service/alerts_service.mock';
import { schema } from '@kbn/config-schema';
import { RecoveredActionGroupId } from '../common';
import { AlertingConfig } from './config';
const logger = loggingSystemMock.create().get();
let mockedLicenseState: jest.Mocked<ILicenseState>;
@ -30,6 +31,7 @@ beforeEach(() => {
jest.clearAllMocks();
mockedLicenseState = licenseStateMock.create();
ruleTypeRegistryParams = {
config: {} as AlertingConfig,
logger,
taskManager,
taskRunnerFactory: new TaskRunnerFactory(),
@ -582,6 +584,63 @@ describe('Create Lifecycle', () => {
expect(alertsService.register).not.toHaveBeenCalled();
});
test('registers rule with no overwrite on producer', () => {
const ruleType: RuleType<never, never, never, never, never, 'default', 'recovered', {}> = {
id: 'test',
name: 'Test',
actionGroups: [
{
id: 'default',
name: 'Default',
},
],
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
executor: jest.fn(),
category: 'test',
producer: 'alerts',
ruleTaskTimeout: '20m',
validate: {
params: { validate: (params) => params },
},
};
const registry = new RuleTypeRegistry(ruleTypeRegistryParams);
registry.register(ruleType);
expect(registry.get('test').producer).toEqual('alerts');
});
});
describe('register() with overwriteProducer', () => {
test('registers rule and overwrite producer', () => {
const ruleType: RuleType<never, never, never, never, never, 'default', 'recovered', {}> = {
id: 'test',
name: 'Test',
actionGroups: [
{
id: 'default',
name: 'Default',
},
],
defaultActionGroupId: 'default',
minimumLicenseRequired: 'basic',
isExportable: true,
executor: jest.fn(),
category: 'test',
producer: 'alerts',
ruleTaskTimeout: '20m',
validate: {
params: { validate: (params) => params },
},
};
const registry = new RuleTypeRegistry({
...ruleTypeRegistryParams,
config: { rules: { overwriteProducer: 'observability' } } as unknown as AlertingConfig,
});
registry.register(ruleType);
expect(registry.get('test').producer).toEqual('observability');
});
});
describe('get()', () => {

View file

@ -39,8 +39,10 @@ import { InMemoryMetrics } from './monitoring';
import { AlertingRulesConfig } from '.';
import { AlertsService } from './alerts_service/alerts_service';
import { getRuleTypeIdValidLegacyConsumers } from './rule_type_registry_deprecated_consumers';
import { AlertingConfig } from './config';
export interface ConstructorOptions {
config: AlertingConfig;
logger: Logger;
taskManager: TaskManagerSetupContract;
taskRunnerFactory: TaskRunnerFactory;
@ -148,6 +150,7 @@ export type UntypedNormalizedRuleType = NormalizedRuleType<
>;
export class RuleTypeRegistry {
private readonly config: AlertingConfig;
private readonly logger: Logger;
private readonly taskManager: TaskManagerSetupContract;
private readonly ruleTypes: Map<string, UntypedNormalizedRuleType> = new Map();
@ -159,6 +162,7 @@ export class RuleTypeRegistry {
private readonly alertsService: AlertsService | null;
constructor({
config,
logger,
taskManager,
taskRunnerFactory,
@ -168,6 +172,7 @@ export class RuleTypeRegistry {
inMemoryMetrics,
alertsService,
}: ConstructorOptions) {
this.config = config;
this.logger = logger;
this.taskManager = taskManager;
this.taskRunnerFactory = taskRunnerFactory;
@ -277,7 +282,7 @@ export class RuleTypeRegistry {
ActionGroupIds,
RecoveryActionGroupId,
AlertData
>(ruleType);
>(ruleType, this.config);
this.ruleTypes.set(
ruleTypeIdSchema.validate(ruleType.id),
@ -457,7 +462,8 @@ function augmentActionGroupsWithReserved<
ActionGroupIds,
RecoveryActionGroupId,
AlertData
>
>,
config: AlertingConfig
): NormalizedRuleType<
Params,
ExtractedParams,
@ -505,6 +511,7 @@ function augmentActionGroupsWithReserved<
return {
...ruleType,
...(config?.rules?.overwriteProducer ? { producer: config.rules.overwriteProducer } : {}),
actionGroups: [...actionGroups, ...reservedActionGroups],
recoveryActionGroup: recoveryActionGroup ?? RecoveredActionGroup,
validLegacyConsumers: getRuleTypeIdValidLegacyConsumers(id),

View file

@ -15,6 +15,7 @@ import { licensingMock } from '@kbn/licensing-plugin/server/mocks';
import { isRuleExportable } from './is_rule_exportable';
import { inMemoryMetricsMock } from '../monitoring/in_memory_metrics.mock';
import { loggingSystemMock } from '@kbn/core/server/mocks';
import { AlertingConfig } from '../config';
let ruleTypeRegistryParams: ConstructorOptions;
let logger: MockedLogger;
@ -27,6 +28,7 @@ beforeEach(() => {
mockedLicenseState = licenseStateMock.create();
logger = loggerMock.create();
ruleTypeRegistryParams = {
config: {} as AlertingConfig,
logger: loggingSystemMock.create().get(),
taskManager,
alertsService: null,

View file

@ -16,6 +16,7 @@ import type { ActionGroup } from '@kbn/alerting-plugin/common';
import { formatDurationFromTimeUnitChar } from '@kbn/observability-plugin/common';
import { ML_ANOMALY_SEVERITY } from '@kbn/ml-anomaly-utils/anomaly_severity';
import { ML_ANOMALY_THRESHOLD } from '@kbn/ml-anomaly-utils/anomaly_threshold';
import { ApmRuleType } from '@kbn/rule-data-utils';
import {
ERROR_GROUP_ID,
ERROR_GROUP_NAME,
@ -28,13 +29,6 @@ import { getEnvironmentLabel } from '../environment_filter_values';
export const APM_SERVER_FEATURE_ID = 'apm';
export enum ApmRuleType {
ErrorCount = 'apm.error_rate', // ErrorRate was renamed to ErrorCount but the key is kept as `error_rate` for backwards-compat.
TransactionErrorRate = 'apm.transaction_error_rate',
TransactionDuration = 'apm.transaction_duration',
Anomaly = 'apm.anomaly',
}
export enum AggregationType {
Avg = 'avg',
P95 = '95th',

View file

@ -6,7 +6,7 @@
*/
import { union } from 'lodash';
import { ApmRuleType } from './apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import {
SERVICE_ENVIRONMENT,
SERVICE_NAME,

View file

@ -7,7 +7,8 @@
import { schema, TypeOf } from '@kbn/config-schema';
import { ML_ANOMALY_SEVERITY } from '@kbn/ml-anomaly-utils/anomaly_severity';
import { AggregationType, ApmRuleType } from './apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { AggregationType } from './apm_rule_types';
export const searchConfigurationSchema = schema.object({
query: schema.object({

View file

@ -7,13 +7,12 @@
import { i18n } from '@kbn/i18n';
import { lazy } from 'react';
import { ALERT_REASON } from '@kbn/rule-data-utils';
import { ALERT_REASON, ApmRuleType } from '@kbn/rule-data-utils';
import type { ObservabilityRuleTypeRegistry } from '@kbn/observability-plugin/public';
import {
getAlertUrlErrorCount,
getAlertUrlTransaction,
} from '../../../../common/utils/formatters';
import { ApmRuleType } from '../../../../common/rules/apm_rule_types';
import {
anomalyMessage,
errorCountMessage,

View file

@ -7,10 +7,8 @@
import React, { useCallback, useMemo } from 'react';
import { useKibana } from '@kbn/kibana-react-plugin/public';
import {
ApmRuleType,
APM_SERVER_FEATURE_ID,
} from '../../../../../common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { APM_SERVER_FEATURE_ID } from '../../../../../common/rules/apm_rule_types';
import { getInitialAlertValues } from '../../utils/get_initial_alert_values';
import { ApmPluginStartDeps } from '../../../../plugin';
import { useServiceName } from '../../../../hooks/use_service_name';
@ -70,6 +68,7 @@ export function AlertingFlyout(props: Props) {
start,
end,
} as AlertMetadata,
useRuleProducer: true,
}),
/* eslint-disable-next-line react-hooks/exhaustive-deps */
[

View file

@ -6,10 +6,8 @@
*/
import { getInitialAlertValues } from './get_initial_alert_values';
import {
ApmRuleType,
RULE_TYPES_CONFIG,
} from '../../../../common/rules/apm_rule_types';
import { RULE_TYPES_CONFIG } from '../../../../common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
test('handles null rule type and undefined service name', () => {
expect(getInitialAlertValues(null, undefined)).toEqual({ tags: ['apm'] });

View file

@ -5,10 +5,8 @@
* 2.0.
*/
import {
ApmRuleType,
RULE_TYPES_CONFIG,
} from '../../../../common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { RULE_TYPES_CONFIG } from '../../../../common/rules/apm_rule_types';
export function getInitialAlertValues(
ruleType: ApmRuleType | null,

View file

@ -110,7 +110,7 @@ export function AlertsOverview() {
}
id={'service-overview-alerts'}
configurationId={AlertConsumers.OBSERVABILITY}
featureIds={[AlertConsumers.APM]}
featureIds={[AlertConsumers.APM, AlertConsumers.OBSERVABILITY]}
query={esQuery}
showAlertStatusWithFlapping
/>

View file

@ -13,7 +13,7 @@ import {
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useState } from 'react';
import { ApmRuleType } from '../../../../../common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { AlertingFlyout } from '../../../alerting/ui_components/alerting_flyout';
import { useApmPluginContext } from '../../../../context/apm_plugin/use_apm_plugin_context';

View file

@ -14,10 +14,8 @@ import {
} from '@kbn/licensing-plugin/server';
import { APM_INDEX_SETTINGS_SAVED_OBJECT_TYPE } from '@kbn/apm-data-access-plugin/server/saved_objects/apm_indices';
import {
ApmRuleType,
APM_SERVER_FEATURE_ID,
} from '../common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { APM_SERVER_FEATURE_ID } from '../common/rules/apm_rule_types';
const ruleTypes = Object.values(ApmRuleType);

View file

@ -21,6 +21,7 @@ import {
ALERT_EVALUATION_VALUE,
ALERT_REASON,
ALERT_SEVERITY,
ApmRuleType,
} from '@kbn/rule-data-utils';
import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
@ -43,7 +44,7 @@ import {
} from '../../../../../common/environment_filter_values';
import {
ANOMALY_ALERT_SEVERITY_TYPES,
ApmRuleType,
APM_SERVER_FEATURE_ID,
formatAnomalyReason,
RULE_TYPES_CONFIG,
} from '../../../../../common/rules/apm_rule_types';
@ -94,7 +95,7 @@ export function registerAnomalyRuleType({
],
},
category: DEFAULT_APP_CATEGORIES.observability.id,
producer: 'apm',
producer: APM_SERVER_FEATURE_ID,
minimumLicenseRequired: 'basic',
isExportable: true,
executor: async ({

View file

@ -11,6 +11,7 @@ import {
termQuery,
} from '@kbn/observability-plugin/server';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import { ApmRuleType } from '@kbn/rule-data-utils';
import {
ERROR_GROUP_ID,
PROCESSOR_EVENT,
@ -21,7 +22,6 @@ import { environmentQuery } from '../../../../../common/utils/environment_query'
import { APMEventClient } from '../../../../lib/helpers/create_es_client/create_apm_event_client';
import { getGroupByTerms } from '../utils/get_groupby_terms';
import { getAllGroupByFields } from '../../../../../common/rules/get_all_groupby_fields';
import { ApmRuleType } from '../../../../../common/rules/apm_rule_types';
import {
BarSeriesDataMap,
getFilteredBarSeries,

View file

@ -18,6 +18,7 @@ import {
ALERT_EVALUATION_THRESHOLD,
ALERT_EVALUATION_VALUE,
ALERT_REASON,
ApmRuleType,
} from '@kbn/rule-data-utils';
import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server';
import {
@ -34,7 +35,6 @@ import {
SERVICE_NAME,
} from '../../../../../common/es_fields/apm';
import {
ApmRuleType,
APM_SERVER_FEATURE_ID,
formatErrorCountReason,
RULE_TYPES_CONFIG,

View file

@ -11,10 +11,8 @@ import {
rangeQuery,
termQuery,
} from '@kbn/observability-plugin/server';
import {
AggregationType,
ApmRuleType,
} from '../../../../../common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { AggregationType } from '../../../../../common/rules/apm_rule_types';
import {
SERVICE_NAME,
TRANSACTION_TYPE,

View file

@ -24,6 +24,7 @@ import {
ALERT_EVALUATION_THRESHOLD,
ALERT_EVALUATION_VALUE,
ALERT_REASON,
ApmRuleType,
} from '@kbn/rule-data-utils';
import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
@ -38,7 +39,6 @@ import {
TRANSACTION_TYPE,
} from '../../../../../common/es_fields/apm';
import {
ApmRuleType,
APM_SERVER_FEATURE_ID,
formatTransactionDurationReason,
RULE_TYPES_CONFIG,
@ -87,9 +87,9 @@ export const transactionDurationActionVariables = [
export function registerTransactionDurationRuleType({
alerting,
apmConfig,
ruleDataClient,
getApmIndices,
apmConfig,
logger,
basePath,
}: RegisterRuleDependencies) {

View file

@ -10,7 +10,7 @@ import {
rangeQuery,
termQuery,
} from '@kbn/observability-plugin/server';
import { ApmRuleType } from '../../../../../common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import {
SERVICE_NAME,
TRANSACTION_TYPE,

View file

@ -23,6 +23,7 @@ import {
ALERT_EVALUATION_THRESHOLD,
ALERT_EVALUATION_VALUE,
ALERT_REASON,
ApmRuleType,
} from '@kbn/rule-data-utils';
import { createLifecycleRuleTypeFactory } from '@kbn/rule-registry-plugin/server';
import { addSpaceIdToPath } from '@kbn/spaces-plugin/common';
@ -39,7 +40,6 @@ import {
} from '../../../../../common/es_fields/apm';
import { EventOutcome } from '../../../../../common/event_outcome';
import {
ApmRuleType,
APM_SERVER_FEATURE_ID,
formatTransactionErrorRateReason,
RULE_TYPES_CONFIG,
@ -83,9 +83,9 @@ export const transactionErrorRateActionVariables = [
export function registerTransactionErrorRateRuleType({
alerting,
alertsLocator,
apmConfig,
basePath,
getApmIndices,
apmConfig,
logger,
ruleDataClient,
}: RegisterRuleDependencies) {

View file

@ -56,7 +56,9 @@ export const createRuleTypeMocks = () => {
publicBaseUrl: 'http://localhost:5601/eyr',
serverBasePath: '/eyr',
} as IBasePath,
apmConfig: { searchAggregatedTransactions: true } as any as APMConfig,
apmConfig: {
searchAggregatedTransactions: true,
} as any as APMConfig,
getApmIndices: async () => ({
error: 'apm-*',
transaction: 'apm-*',

View file

@ -60,4 +60,5 @@ export const observabilityAlertFeatureIds: ValidFeatureId[] = [
export const observabilityRuleCreationValidConsumers: RuleCreationValidConsumer[] = [
AlertConsumers.INFRASTRUCTURE,
AlertConsumers.LOGS,
AlertConsumers.OBSERVABILITY,
];

View file

@ -39,6 +39,7 @@ export function AlertFlyout(props: Props) {
series: props.series,
},
validConsumers: observabilityRuleCreationValidConsumers,
useRuleProducer: true,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[triggersActionsUI, onCloseFlyout]

View file

@ -9,6 +9,7 @@ import { useQuery } from '@tanstack/react-query';
import { BASE_RAC_ALERTS_API_PATH } from '@kbn/rule-registry-plugin/common';
import { ALL_VALUE, SLOResponse } from '@kbn/slo-schema';
import { AlertConsumers } from '@kbn/rule-registry-plugin/common/technical_rule_data_field_names';
import { useKibana } from '../../utils/kibana_react';
import { sloKeys } from './query_key_factory';
@ -80,7 +81,7 @@ export function useFetchActiveAlerts({ sloIdsAndInstanceIds = [] }: Params): Use
try {
const response = await http.post<FindApiResponse>(`${BASE_RAC_ALERTS_API_PATH}/find`, {
body: JSON.stringify({
feature_ids: ['slo'],
feature_ids: [AlertConsumers.SLO, AlertConsumers.OBSERVABILITY],
size: 0,
query: {
bool: {

View file

@ -250,6 +250,7 @@ export function HeaderControl({ isLoading, slo }: Props) {
canChangeTrigger={false}
onClose={onCloseRuleFlyout}
initialValues={{ name: `${slo.name} burn rate`, params: { sloId: slo.id } }}
useRuleProducer
/>
) : null}

View file

@ -32,7 +32,7 @@ export function SloDetailsAlerts({ slo }: Props) {
configurationId={AlertConsumers.OBSERVABILITY}
id={ALERTS_TABLE_ID}
data-test-subj="alertTable"
featureIds={[AlertConsumers.SLO]}
featureIds={[AlertConsumers.SLO, AlertConsumers.OBSERVABILITY]}
query={{
bool: {
filter: [

View file

@ -264,6 +264,7 @@ export function SloEditForm({ slo }: Props) {
ruleTypeId={SLO_BURN_RATE_RULE_TYPE_ID}
onClose={handleCloseRuleFlyout}
onSave={handleCloseRuleFlyout}
useRuleProducer
/>
) : null}
</>

View file

@ -276,6 +276,7 @@ export function SloListItem({
onClose={() => {
setIsAddRuleFlyoutOpen(false);
}}
useRuleProducer
/>
) : null}

View file

@ -57,6 +57,7 @@ const configSchema = schema.object({
groupByPageSize: schema.number({ defaultValue: 10_000 }),
}),
enabled: schema.boolean({ defaultValue: true }),
createO11yGenericFeatureId: schema.boolean({ defaultValue: false }),
});
export const config: PluginConfigDescriptor = {

View file

@ -26,6 +26,11 @@ import { RuleRegistryPluginSetupContract } from '@kbn/rule-registry-plugin/serve
import { SharePluginSetup } from '@kbn/share-plugin/server';
import { SpacesPluginSetup } from '@kbn/spaces-plugin/server';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import {
ApmRuleType,
ES_QUERY_ID,
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
} from '@kbn/rule-data-utils';
import { ObservabilityConfig } from '.';
import { casesFeatureId, observabilityFeatureId, sloFeatureId } from '../common';
import { SLO_BURN_RATE_RULE_TYPE_ID } from '../common/constants';
@ -72,6 +77,13 @@ interface PluginStart {
const sloRuleTypes = [SLO_BURN_RATE_RULE_TYPE_ID];
const o11yRuleTypes = [
SLO_BURN_RATE_RULE_TYPE_ID,
OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
ES_QUERY_ID,
...Object.values(ApmRuleType),
];
export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
private logger: Logger;
@ -180,6 +192,58 @@ export class ObservabilityPlugin implements Plugin<ObservabilityPluginSetup> {
});
}
if (config.createO11yGenericFeatureId) {
plugins.features.registerKibanaFeature({
id: observabilityFeatureId,
name: i18n.translate('xpack.observability.nameFeatureTitle', {
defaultMessage: 'Observability',
}),
order: 1000,
category: DEFAULT_APP_CATEGORIES.observability,
app: [observabilityFeatureId],
catalogue: [observabilityFeatureId],
alerting: o11yRuleTypes,
privileges: {
all: {
app: [observabilityFeatureId],
catalogue: [observabilityFeatureId],
api: ['rac'],
savedObject: {
all: [],
read: [],
},
alerting: {
rule: {
all: o11yRuleTypes,
},
alert: {
all: o11yRuleTypes,
},
},
ui: ['read', 'write'],
},
read: {
app: [observabilityFeatureId],
catalogue: [observabilityFeatureId],
api: ['rac'],
savedObject: {
all: [],
read: [],
},
alerting: {
rule: {
read: o11yRuleTypes,
},
alert: {
read: o11yRuleTypes,
},
},
ui: ['read'],
},
},
});
}
const { ruleDataService } = plugins.ruleRegistry;
const savedObjectTypes = [SO_SLO_TYPE];

View file

@ -22,7 +22,6 @@ export class AlertingBuiltinsPlugin
public setup(core: CoreSetup<StackAlertsStartDeps>, { alerting, features }: StackAlertsDeps) {
features.registerKibanaFeature(BUILT_IN_ALERTS_FEATURE);
registerBuiltInRuleTypes({
logger: this.logger,
data: core

View file

@ -59,6 +59,7 @@ import {
RuleActionAlertsFilterProperty,
} from '@kbn/alerting-plugin/common';
import { AlertingConnectorFeatureId } from '@kbn/actions-plugin/common';
import { AlertConsumers } from '@kbn/rule-data-utils';
import { RuleReducerAction, InitialRule } from './rule_reducer';
import {
RuleTypeModel,
@ -250,11 +251,11 @@ export const RuleForm = ({
validConsumers,
})
)
.filter((item) =>
rule.consumer === ALERTS_FEATURE_ID
.filter((item) => {
return rule.consumer === ALERTS_FEATURE_ID
? !item.ruleTypeModel.requiresAppContext
: item.ruleType!.producer === rule.consumer
);
: item.ruleType!.producer === rule.consumer;
});
const availableRuleTypesResult = getAvailableRuleTypes(ruleTypes);
setAvailableRuleTypes(availableRuleTypesResult);
@ -274,9 +275,13 @@ export const RuleForm = ({
},
new Map()
);
setSolutions(
new Map([...solutionsResult.entries()].sort(([, a], [, b]) => a.localeCompare(b)))
);
const solutionsEntries = [...solutionsResult.entries()];
const isOnlyO11y =
availableRuleTypesResult.length === 1 &&
availableRuleTypesResult.every((rt) => rt.ruleType.producer === AlertConsumers.OBSERVABILITY);
if (!isOnlyO11y) {
setSolutions(new Map(solutionsEntries.sort(([, a], [, b]) => a.localeCompare(b))));
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
ruleTypes,
@ -400,6 +405,16 @@ export const RuleForm = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ruleTypeRegistry, availableRuleTypes, searchText, JSON.stringify(solutionsFilter)]);
useEffect(() => {
if (ruleTypeModel) {
const ruleType = ruleTypes.find((rt) => rt.id === ruleTypeModel.id);
if (ruleType && useRuleProducer && !MULTI_CONSUMER_RULE_TYPE_IDS.includes(ruleType.id)) {
setConsumer(ruleType.producer as RuleCreationValidConsumer);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ruleTypeModel, ruleTypes]);
const authorizedConsumers = useMemo(() => {
// If the app context provides a consumer, we assume that consumer is
// is what we set for all rules that is created in that context

View file

@ -80,4 +80,17 @@ describe('RuleFormConsumerSelectionModal', () => {
expect(mockOnChange).toHaveBeenLastCalledWith('stackAlerts');
expect(screen.queryByTestId('ruleFormConsumerSelect')).not.toBeInTheDocument();
});
it('should display nothing if observability is one of the consumer', () => {
render(
<RuleFormConsumerSelection
consumers={['logs', 'observability']}
onChange={mockOnChange}
errors={{}}
/>
);
expect(mockOnChange).toHaveBeenLastCalledWith('observability');
expect(screen.queryByTestId('ruleFormConsumerSelect')).not.toBeInTheDocument();
});
});

View file

@ -121,13 +121,15 @@ export const RuleFormConsumerSelection = (props: RuleFormConsumerSelectionProps)
}, []);
useEffect(() => {
if (formattedSelectOptions.length === 1) {
onChange(formattedSelectOptions[0].value as RuleCreationValidConsumer);
if (consumers.length === 1) {
onChange(consumers[0] as RuleCreationValidConsumer);
} else if (consumers.includes(AlertConsumers.OBSERVABILITY)) {
onChange(AlertConsumers.OBSERVABILITY as RuleCreationValidConsumer);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formattedSelectOptions]);
}, [consumers]);
if (formattedSelectOptions.length <= 1) {
if (consumers.length <= 1 || consumers.includes(AlertConsumers.OBSERVABILITY)) {
return null;
}
return (

View file

@ -838,4 +838,5 @@ export interface NotifyWhenSelectOptions {
export type RuleCreationValidConsumer =
| typeof AlertConsumers.LOGS
| typeof AlertConsumers.INFRASTRUCTURE
| typeof AlertConsumers.OBSERVABILITY
| typeof STACK_ALERTS_FEATURE_ID;

View file

@ -6,7 +6,7 @@
*/
import moment from 'moment';
import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';
import { range } from 'lodash';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { errorCountActionVariables } from '@kbn/apm-plugin/server/routes/alerts/rule_types/error_count/register_error_count_rule_type';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { getErrorGroupingKey } from '@kbn/apm-synthtrace-client/src/lib/apm/instance';

View file

@ -9,7 +9,7 @@ import { Client, errors } from '@elastic/elasticsearch';
import { ParsedTechnicalFields } from '@kbn/rule-registry-plugin/common';
import pRetry from 'p-retry';
import type { SuperTest, Test } from 'supertest';
import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { ApmRuleParamsType } from '@kbn/apm-plugin/common/rules/schema';
import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import { AggregationType, ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { AggregationType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { transactionDurationActionVariables } from '@kbn/apm-plugin/server/routes/alerts/rule_types/transaction_duration/register_transaction_duration_rule_type';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { transactionErrorRateActionVariables } from '@kbn/apm-plugin/server/routes/alerts/rule_types/transaction_error_rate/register_transaction_error_rate_rule_type';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';

View file

@ -4,7 +4,8 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { AggregationType, ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { AggregationType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import {

View file

@ -5,7 +5,8 @@
* 2.0.
*/
import expect from '@kbn/expect';
import { AggregationType, ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { AggregationType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {

View file

@ -11,7 +11,8 @@ import { LatencyAggregationType } from '@kbn/apm-plugin/common/latency_aggregati
import { ApmDocumentType, ApmTransactionDocumentType } from '@kbn/apm-plugin/common/document_type';
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { AggregationType, ApmRuleType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { AggregationType } from '@kbn/apm-plugin/common/rules/apm_rule_types';
import { ApmRuleType } from '@kbn/rule-data-utils';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import {
createApmRule,

View file

@ -12,7 +12,7 @@ import type {
import { MetricThresholdParams } from '@kbn/infra-plugin/common/alerting/metrics';
import { ThresholdParams } from '@kbn/observability-plugin/common/custom_threshold_rule/types';
import { SloBurnRateRuleParams } from './slo_api';
import { FtrProviderContext } from '../ftr_provider_context';
export function AlertingApiProvider({ getService }: FtrProviderContext) {
@ -117,7 +117,7 @@ export function AlertingApiProvider({ getService }: FtrProviderContext) {
}: {
ruleTypeId: string;
name: string;
params: MetricThresholdParams | ThresholdParams;
params: MetricThresholdParams | ThresholdParams | SloBurnRateRuleParams;
actions?: any[];
tags?: any[];
schedule?: { interval: string };
@ -140,5 +140,16 @@ export function AlertingApiProvider({ getService }: FtrProviderContext) {
});
return body;
},
async findRule(ruleId: string) {
if (!ruleId) {
throw new Error(`'ruleId' is undefined`);
}
const response = await supertest
.get('/api/alerting/rules/_find')
.set('kbn-xsrf', 'foo')
.set('x-elastic-internal-origin', 'foo');
return response.body.data.find((obj: any) => obj.id === ruleId);
},
};
}

View file

@ -13,6 +13,7 @@ import { AlertingApiProvider } from './alerting_api';
import { SamlToolsProvider } from './saml_tools';
import { DataViewApiProvider } from './data_view_api';
import { SvlCasesServiceProvider } from './svl_cases';
import { SloApiProvider } from './slo_api';
export const services = {
// deployment agnostic FTR services
@ -24,6 +25,7 @@ export const services = {
samlTools: SamlToolsProvider,
dataViewApi: DataViewApiProvider,
svlCases: SvlCasesServiceProvider,
sloApi: SloApiProvider,
};
export type InheritedFtrProviderContext = GenericFtrProviderContext<typeof services, {}>;

View file

@ -0,0 +1,89 @@
/*
* 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 { FtrProviderContext } from '../ftr_provider_context';
type DurationUnit = 'm' | 'h' | 'd' | 'w' | 'M';
interface Duration {
value: number;
unit: DurationUnit;
}
interface WindowSchema {
id: string;
burnRateThreshold: number;
maxBurnRateThreshold: number;
longWindow: Duration;
shortWindow: Duration;
actionGroup: string;
}
export interface SloBurnRateRuleParams {
sloId: string;
windows: WindowSchema[];
}
interface SloParams {
id: string;
name: string;
description: string;
indicator: {
type: 'sli.kql.custom';
params: {
index: string;
good: string;
total: string;
timestampField: string;
};
};
timeWindow: {
duration: string;
type: string;
};
budgetingMethod: string;
objective: {
target: number;
};
groupBy: string;
}
export function SloApiProvider({ getService }: FtrProviderContext) {
const supertest = getService('supertest');
const retry = getService('retry');
const requestTimeout = 30 * 1000;
const retryTimeout = 120 * 1000;
return {
async create(slo: SloParams) {
const { body } = await supertest
.post(`/api/observability/slos`)
.set('kbn-xsrf', 'foo')
.set('x-elastic-internal-origin', 'foo')
.send(slo);
return body;
},
async delete() {},
async waitForSloCreated({ sloId }: { sloId: string }) {
if (!sloId) {
throw new Error(`'sloId is undefined`);
}
return await retry.tryForTime(retryTimeout, async () => {
const response = await supertest
.get(`/api/observability/slos/${sloId}`)
.set('kbn-xsrf', 'foo')
.set('x-elastic-internal-origin', 'foo')
.timeout(requestTimeout);
return response.body;
});
},
};
}

View file

@ -164,7 +164,7 @@ export default function ({ getService }: FtrProviderContext) {
groupBy: 'all',
searchType: 'esQuery',
},
[ALERT_RULE_PRODUCER]: 'stackAlerts',
[ALERT_RULE_PRODUCER]: hits1[ALERT_RULE_PRODUCER],
[ALERT_RULE_REVISION]: 0,
[ALERT_RULE_TYPE_ID]: '.es-query',
[ALERT_RULE_TAGS]: [],

View file

@ -437,3 +437,20 @@ export async function snoozeRule({
.expect(204);
return body;
}
export async function findRule({
supertest,
ruleId,
}: {
supertest: SuperTest<Test>;
ruleId: string;
}) {
if (!ruleId) {
throw new Error(`'ruleId' is undefined`);
}
const response = await supertest
.get(`/api/alerting/rule/${ruleId}`)
.set('kbn-xsrf', 'foo')
.set('x-elastic-internal-origin', 'foo');
return response.body || {};
}

View file

@ -962,7 +962,7 @@ function validateEventLog(event: any, params: ValidateEventLogParams) {
id: params.ruleId,
license: 'basic',
category: params.ruleTypeId,
ruleset: 'stackAlerts',
ruleset: event?.rule.ruleset,
name: params.name,
});

View file

@ -190,7 +190,7 @@ export default function ({ getService }: FtrProviderContext) {
groupBy: 'all',
searchType: 'esQuery',
},
[ALERT_RULE_PRODUCER]: 'stackAlerts',
[ALERT_RULE_PRODUCER]: alertDocument[ALERT_RULE_PRODUCER],
[ALERT_RULE_REVISION]: 0,
[ALERT_RULE_TYPE_ID]: '.es-query',
[ALERT_RULE_TAGS]: [],
@ -311,7 +311,7 @@ export default function ({ getService }: FtrProviderContext) {
groupBy: 'all',
searchType: 'esQuery',
},
[ALERT_RULE_PRODUCER]: 'stackAlerts',
[ALERT_RULE_PRODUCER]: alertDocument[ALERT_RULE_PRODUCER],
[ALERT_RULE_REVISION]: 0,
[ALERT_RULE_TYPE_ID]: '.es-query',
[ALERT_RULE_TAGS]: [],
@ -520,7 +520,7 @@ export default function ({ getService }: FtrProviderContext) {
groupBy: 'all',
searchType: 'esQuery',
},
[ALERT_RULE_PRODUCER]: 'stackAlerts',
[ALERT_RULE_PRODUCER]: alertDocument[ALERT_RULE_PRODUCER],
[ALERT_RULE_REVISION]: 0,
[ALERT_RULE_TYPE_ID]: '.es-query',
[ALERT_RULE_TAGS]: [],

View file

@ -0,0 +1,194 @@
/*
* 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.
*/
/*
* 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 { cleanup, generate } from '@kbn/infra-forge';
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
export default function ({ getService }: FtrProviderContext) {
const esClient = getService('es');
const supertest = getService('supertest');
const esDeleteAllIndices = getService('esDeleteAllIndices');
const logger = getService('log');
const alertingApi = getService('alertingApi');
const dataViewApi = getService('dataViewApi');
const sloApi = getService('sloApi');
describe('Burn rate rule', () => {
const RULE_TYPE_ID = 'slo.rules.burnRate';
// DATE_VIEW should match the index template:
// x-pack/packages/kbn-infra-forge/src/data_sources/composable/template.json
const DATE_VIEW = 'kbn-data-forge-fake_hosts';
const ALERT_ACTION_INDEX = 'alert-action-slo';
const DATA_VIEW_ID = 'data-view-id';
let infraDataIndex: string;
let actionId: string;
let ruleId: string;
before(async () => {
infraDataIndex = await generate({ esClient, lookback: 'now-15m', logger });
await dataViewApi.create({
name: DATE_VIEW,
id: DATA_VIEW_ID,
title: DATE_VIEW,
});
});
after(async () => {
await supertest
.delete(`/api/alerting/rule/${ruleId}`)
.set('kbn-xsrf', 'foo')
.set('x-elastic-internal-origin', 'foo');
await supertest
.delete(`/api/actions/connector/${actionId}`)
.set('kbn-xsrf', 'foo')
.set('x-elastic-internal-origin', 'foo');
await esClient.deleteByQuery({
index: '.kibana-event-log-*',
query: { term: { 'rule.id': ruleId } },
conflicts: 'proceed',
});
await dataViewApi.delete({
id: DATA_VIEW_ID,
});
await supertest
.delete('/api/observability/slos/my-custom-id')
.set('kbn-xsrf', 'foo')
.set('x-elastic-internal-origin', 'foo');
await esDeleteAllIndices([ALERT_ACTION_INDEX, infraDataIndex]);
await cleanup({ esClient, logger });
});
describe('Rule creation', () => {
it('creates rule successfully', async () => {
actionId = await alertingApi.createIndexConnector({
name: 'Index Connector: Slo Burn rate API test',
indexName: ALERT_ACTION_INDEX,
});
await sloApi.create({
id: 'my-custom-id',
name: 'my custom name',
description: 'my custom description',
indicator: {
type: 'sli.kql.custom',
params: {
index: infraDataIndex,
good: 'system.cpu.total.norm.pct > 1',
total: 'system.cpu.total.norm.pct: *',
timestampField: '@timestamp',
},
},
timeWindow: {
duration: '7d',
type: 'rolling',
},
budgetingMethod: 'occurrences',
objective: {
target: 0.999,
},
groupBy: '*',
});
const createdRule = await alertingApi.createRule({
tags: ['observability'],
consumer: 'observability',
name: 'SLO Burn Rate rule',
ruleTypeId: RULE_TYPE_ID,
schedule: {
interval: '1m',
},
params: {
sloId: 'my-custom-id',
windows: [
{
id: '1',
actionGroup: 'slo.burnRate.alert',
burnRateThreshold: 14.4,
maxBurnRateThreshold: 720,
longWindow: {
value: 1,
unit: 'h',
},
shortWindow: {
value: 5,
unit: 'm',
},
},
{
id: '2',
actionGroup: 'slo.burnRate.high',
burnRateThreshold: 6,
maxBurnRateThreshold: 120,
longWindow: {
value: 6,
unit: 'h',
},
shortWindow: {
value: 30,
unit: 'm',
},
},
{
id: '3',
actionGroup: 'slo.burnRate.medium',
burnRateThreshold: 3,
maxBurnRateThreshold: 30,
longWindow: {
value: 24,
unit: 'h',
},
shortWindow: {
value: 120,
unit: 'm',
},
},
{
id: '4',
actionGroup: 'slo.burnRate.low',
burnRateThreshold: 1,
maxBurnRateThreshold: 10,
longWindow: {
value: 72,
unit: 'h',
},
shortWindow: {
value: 360,
unit: 'm',
},
},
],
},
actions: [],
});
ruleId = createdRule.id;
expect(ruleId).not.to.be(undefined);
});
it('should be active', async () => {
const executionStatus = await alertingApi.waitForRuleStatus({
ruleId,
expectedStatus: 'active',
});
expect(executionStatus).to.be('active');
});
it('should find the created rule with correct information about the consumer', async () => {
const match = await alertingApi.findRule(ruleId);
expect(match).not.to.be(undefined);
expect(match.consumer).to.be('observability');
});
});
});
}

View file

@ -82,7 +82,7 @@ export default function ({ getService }: FtrProviderContext) {
const createdRule = await alertingApi.createRule({
tags: ['observability'],
consumer: 'apm',
consumer: 'observability',
name: 'Threshold rule',
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
params: {
@ -139,6 +139,12 @@ export default function ({ getService }: FtrProviderContext) {
expect(executionStatus).to.be('active');
});
it('should find the created rule with correct information about the consumer', async () => {
const match = await alertingApi.findRule(ruleId);
expect(match).not.to.be(undefined);
expect(match.consumer).to.be('observability');
});
it('should set correct information in the alert document', async () => {
const resp = await alertingApi.waitForAlertInIndex({
indexName: CUSTOM_THRESHOLD_RULE_ALERT_INDEX,
@ -149,7 +155,7 @@ export default function ({ getService }: FtrProviderContext) {
'kibana.alert.rule.category',
'Custom threshold (Technical Preview)'
);
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'apm');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'observability');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0);

View file

@ -68,7 +68,7 @@ export default function ({ getService }: FtrProviderContext) {
const createdRule = await alertingApi.createRule({
tags: ['observability'],
consumer: 'apm',
consumer: 'observability',
name: 'Threshold rule',
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
params: {
@ -125,6 +125,12 @@ export default function ({ getService }: FtrProviderContext) {
expect(executionStatus).to.be('active');
});
it('should find the created rule with correct information about the consumer', async () => {
const match = await alertingApi.findRule(ruleId);
expect(match).not.to.be(undefined);
expect(match.consumer).to.be('observability');
});
it('should set correct information in the alert document', async () => {
const resp = await alertingApi.waitForAlertInIndex({
indexName: CUSTOM_THRESHOLD_RULE_ALERT_INDEX,
@ -135,7 +141,7 @@ export default function ({ getService }: FtrProviderContext) {
'kibana.alert.rule.category',
'Custom threshold (Technical Preview)'
);
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'apm');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'observability');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0);

View file

@ -84,7 +84,7 @@ export default function ({ getService }: FtrProviderContext) {
const createdRule = await alertingApi.createRule({
tags: ['observability'],
consumer: 'apm',
consumer: 'observability',
name: 'Threshold rule',
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
params: {
@ -143,6 +143,12 @@ export default function ({ getService }: FtrProviderContext) {
expect(executionStatus).to.be('active');
});
it('should find the created rule with correct information about the consumer', async () => {
const match = await alertingApi.findRule(ruleId);
expect(match).not.to.be(undefined);
expect(match.consumer).to.be('observability');
});
it('should set correct information in the alert document', async () => {
const resp = await alertingApi.waitForAlertInIndex({
indexName: CUSTOM_THRESHOLD_RULE_ALERT_INDEX,
@ -153,7 +159,7 @@ export default function ({ getService }: FtrProviderContext) {
'kibana.alert.rule.category',
'Custom threshold (Technical Preview)'
);
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'apm');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'observability');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0);

View file

@ -78,7 +78,7 @@ export default function ({ getService }: FtrProviderContext) {
const createdRule = await alertingApi.createRule({
tags: ['observability'],
consumer: 'apm',
consumer: 'observability',
name: 'Threshold rule',
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
params: {
@ -133,6 +133,12 @@ export default function ({ getService }: FtrProviderContext) {
expect(executionStatus).to.be('active');
});
it('should find the created rule with correct information about the consumer', async () => {
const match = await alertingApi.findRule(ruleId);
expect(match).not.to.be(undefined);
expect(match.consumer).to.be('observability');
});
it('should set correct information in the alert document', async () => {
const resp = await alertingApi.waitForAlertInIndex({
indexName: CUSTOM_THRESHOLD_RULE_ALERT_INDEX,
@ -143,7 +149,7 @@ export default function ({ getService }: FtrProviderContext) {
'kibana.alert.rule.category',
'Custom threshold (Technical Preview)'
);
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'apm');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'observability');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0);

View file

@ -88,7 +88,7 @@ export default function ({ getService }: FtrProviderContext) {
const createdRule = await alertingApi.createRule({
tags: ['observability'],
consumer: 'apm',
consumer: 'observability',
name: 'Threshold rule',
ruleTypeId: OBSERVABILITY_THRESHOLD_RULE_TYPE_ID,
params: {
@ -150,6 +150,12 @@ export default function ({ getService }: FtrProviderContext) {
expect(executionStatus).to.be('active');
});
it('should find the created rule with correct information about the consumer', async () => {
const match = await alertingApi.findRule(ruleId);
expect(match).not.to.be(undefined);
expect(match.consumer).to.be('observability');
});
it('should set correct information in the alert document', async () => {
const resp = await alertingApi.waitForAlertInIndex({
indexName: CUSTOM_THRESHOLD_RULE_ALERT_INDEX,
@ -162,7 +168,7 @@ export default function ({ getService }: FtrProviderContext) {
'kibana.alert.rule.category',
'Custom threshold (Technical Preview)'
);
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'apm');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.consumer', 'observability');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.name', 'Threshold rule');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.producer', 'observability');
expect(resp.hits.hits[0]._source).property('kibana.alert.rule.revision', 0);

View file

@ -0,0 +1,113 @@
/*
* 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.
*/
/*
* 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 expect from '@kbn/expect';
import { FtrProviderContext } from '../../../ftr_provider_context';
import { createEsQueryRule } from '../../common/alerting/helpers/alerting_api_helper';
export default function ({ getService }: FtrProviderContext) {
const esClient = getService('es');
const supertest = getService('supertest');
const esDeleteAllIndices = getService('esDeleteAllIndices');
const alertingApi = getService('alertingApi');
describe('ElasticSearch query rule', () => {
const RULE_TYPE_ID = '.es-query';
const ALERT_ACTION_INDEX = 'alert-action-es-query';
let actionId: string;
let ruleId: string;
after(async () => {
await supertest
.delete(`/api/alerting/rule/${ruleId}`)
.set('kbn-xsrf', 'foo')
.set('x-elastic-internal-origin', 'foo');
await supertest
.delete(`/api/actions/connector/${actionId}`)
.set('kbn-xsrf', 'foo')
.set('x-elastic-internal-origin', 'foo');
await esClient.deleteByQuery({
index: '.kibana-event-log-*',
query: { term: { 'rule.id': ruleId } },
conflicts: 'proceed',
});
await esDeleteAllIndices([ALERT_ACTION_INDEX]);
});
describe('Rule creation', () => {
it('creates rule successfully', async () => {
actionId = await alertingApi.createIndexConnector({
name: 'Index Connector: Alerting API test',
indexName: ALERT_ACTION_INDEX,
});
const createdRule = await createEsQueryRule({
supertest,
consumer: 'observability',
name: 'always fire',
ruleTypeId: RULE_TYPE_ID,
params: {
size: 100,
thresholdComparator: '>',
threshold: [-1],
index: ['alert-test-data'],
timeField: 'date',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
timeWindowSize: 20,
timeWindowUnit: 's',
},
actions: [
{
group: 'query matched',
id: actionId,
params: {
documents: [
{
ruleId: '{{rule.id}}',
ruleName: '{{rule.name}}',
ruleParams: '{{rule.params}}',
spaceId: '{{rule.spaceId}}',
tags: '{{rule.tags}}',
alertId: '{{alert.id}}',
alertActionGroup: '{{alert.actionGroup}}',
instanceContextValue: '{{context.instanceContextValue}}',
instanceStateValue: '{{state.instanceStateValue}}',
},
],
},
frequency: {
notify_when: 'onActiveAlert',
throttle: null,
summary: false,
},
},
],
});
ruleId = createdRule.id;
expect(ruleId).not.to.be(undefined);
});
it('should be active', async () => {
const executionStatus = await alertingApi.waitForRuleStatus({
ruleId,
expectedStatus: 'active',
});
expect(executionStatus).to.be('active');
});
it('should find the created rule with correct information about the consumer', async () => {
const match = await alertingApi.findRule(ruleId);
expect(match).not.to.be(undefined);
expect(match.consumer).to.be('observability');
});
});
});
}

View file

@ -16,5 +16,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./telemetry/telemetry_config'));
loadTestFile(require.resolve('./apm_api_integration/feature_flags.ts'));
loadTestFile(require.resolve('./cases'));
loadTestFile(require.resolve('./burn_rate_rule/burn_rate_rule'));
loadTestFile(require.resolve('./es_query_rule/es_query_rule'));
});
}

View file

@ -15,6 +15,7 @@ import {
createIndexConnector,
snoozeRule,
createLatencyThresholdRule,
createEsQueryRule,
} from '../../../../api_integration/test_suites/common/alerting/helpers/alerting_api_helper';
export default ({ getPageObject, getService }: FtrProviderContext) => {
@ -90,6 +91,64 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
await svlCommonPage.forceLogout();
});
it('should create an ES Query Rule and display it when consumer is observability', async () => {
const esQuery = await createEsQueryRule({
supertest,
name: 'ES Query',
consumer: 'observability',
ruleTypeId: '.es-query',
params: {
size: 100,
thresholdComparator: '>',
threshold: [-1],
index: ['alert-test-data'],
timeField: 'date',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
timeWindowSize: 20,
timeWindowUnit: 's',
},
});
ruleIdList = [esQuery.id];
await refreshRulesList();
const searchResults = await svlTriggersActionsUI.getRulesList();
expect(searchResults.length).toEqual(1);
expect(searchResults[0].name).toEqual('ES QueryElasticsearch query');
});
it('should create an ES Query rule but not display it when consumer is stackAlerts', async () => {
const esQuery = await createEsQueryRule({
supertest,
name: 'ES Query',
consumer: 'stackAlerts',
ruleTypeId: '.es-query',
params: {
size: 100,
thresholdComparator: '>',
threshold: [-1],
index: ['alert-test-data'],
timeField: 'date',
esQuery: `{\n \"query\":{\n \"match_all\" : {}\n }\n}`,
timeWindowSize: 20,
timeWindowUnit: 's',
},
});
ruleIdList = [esQuery.id];
await refreshRulesList();
await testSubjects.missingOrFail('rule-row');
});
it('should create and display an APM latency rule', async () => {
const apmLatency = await createLatencyThresholdRule({ supertest, name: 'Apm latency' });
ruleIdList = [apmLatency.id];
await refreshRulesList();
const searchResults = await svlTriggersActionsUI.getRulesList();
expect(searchResults.length).toEqual(1);
expect(searchResults[0].name).toEqual('Apm latencyLatency threshold');
});
it('should display rules in alphabetical order', async () => {
const rule1 = await createRule({
supertest,
@ -600,7 +659,9 @@ export default ({ getPageObject, getService }: FtrProviderContext) => {
await testSubjects.click('ruleTypeFilterButton');
}
expect(await (await testSubjects.find('ruleType0Group')).getVisibleText()).toEqual('Apm');
expect(await (await testSubjects.find('ruleType0Group')).getVisibleText()).toEqual(
'Observability'
);
});
await testSubjects.click('ruleTypeapm.anomalyFilterOption');