Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Logs, Alerting] [Space, Time] Add "Explain" functionality to the Alerting Framework [DO NOT MERGE] #131753

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions x-pack/plugins/alerting/common/rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -180,3 +180,12 @@ export interface RuleMonitoring extends SavedObjectAttributes {
};
};
}

export interface EsQueryExplanation {
type: 'ES_QUERY';
queries: Array<{
annotation: string;
query: string;
}>;
}
export type RuleConfigurationExplanation = EsQueryExplanation; // TODO: Could be expanded with other types, such as KQL etc.
82 changes: 82 additions & 0 deletions x-pack/plugins/alerting/server/routes/explain_rule.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* 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 { schema } from '@kbn/config-schema';
import { validateDurationSchema } from '../lib';
import { verifyAccessAndContext } from './lib';
import {
validateNotifyWhenType,
RuleTypeParams,
RuleConfigurationExplanation,
BASE_ALERTING_API_PATH,
} from '../types';
import { RouteOptions } from '.';

// NOTE: These are just copied from createRule, probably only needs params and rule_type_id really.
export const bodySchema = schema.object({
name: schema.string(),
rule_type_id: schema.string(),
enabled: schema.boolean({ defaultValue: true }),
consumer: schema.string(),
tags: schema.arrayOf(schema.string(), { defaultValue: [] }),
throttle: schema.nullable(schema.string({ validate: validateDurationSchema })),
params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
schedule: schema.object({
interval: schema.string({ validate: validateDurationSchema }),
}),
actions: schema.arrayOf(
schema.object({
group: schema.string(),
id: schema.string(),
params: schema.recordOf(schema.string(), schema.any(), { defaultValue: {} }),
}),
{ defaultValue: [] }
),
notify_when: schema.string({ validate: validateNotifyWhenType }),
});

export const explainRuleRoute = ({ router, licenseState }: RouteOptions) => {
router.post(
{
path: `${BASE_ALERTING_API_PATH}/explain_rule`,
validate: {
params: schema.maybe(
schema.object({
id: schema.maybe(schema.string()),
})
),
body: bodySchema,
},
},

router.handleLegacyErrors(
verifyAccessAndContext(licenseState, async function (context, req, res) {
const rulesClient = (await context.alerting).getRulesClient();
const {
elasticsearch: { client: esClient },
savedObjects: { client: savedObjectsClient },
} = await context.core;
const rule = req.body;

try {
const ruleExplanation: RuleConfigurationExplanation =
await rulesClient.explain<RuleTypeParams>({
ruleTypeId: rule.rule_type_id,
params: rule.params,
esClient,
savedObjectsClient,
});
return res.ok({
body: ruleExplanation,
});
} catch (e) {
throw e;
}
})
)
);
};
2 changes: 2 additions & 0 deletions x-pack/plugins/alerting/server/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { ILicenseState } from '../lib';
import { defineLegacyRoutes } from './legacy';
import { AlertingRequestHandlerContext } from '../types';
import { createRuleRoute } from './create_rule';
import { explainRuleRoute } from './explain_rule';
import { getRuleRoute, getInternalRuleRoute } from './get_rule';
import { updateRuleRoute } from './update_rule';
import { deleteRuleRoute } from './delete_rule';
Expand Down Expand Up @@ -67,4 +68,5 @@ export function defineRoutes(opts: RouteOptions) {
updateRuleApiKeyRoute(router, licenseState);
snoozeRuleRoute(router, licenseState);
unsnoozeRuleRoute(router, licenseState);
explainRuleRoute(opts);
}
31 changes: 31 additions & 0 deletions x-pack/plugins/alerting/server/rules_client/rules_client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import {
SanitizedRuleWithLegacyId,
PartialRuleWithLegacyId,
RawAlertInstance as RawAlert,
RuleConfigurationExplanation,
} from '../types';
import { validateRuleTypeParams, ruleExecutionStatusFromRaw, getRuleNotifyWhenType } from '../lib';
import { taskInstanceToAlertTaskInstance } from '../task_runner/alert_task_instance';
Expand Down Expand Up @@ -501,6 +502,36 @@ export class RulesClient {
);
}

public async explain<Params extends RuleTypeParams = never>({
ruleTypeId,
params,
esClient,
savedObjectsClient,
}: {
// TODO: Add types
ruleTypeId: any;
params: any;
esClient: any;
savedObjectsClient: any;
}): Promise<RuleConfigurationExplanation> {
console.log('Inside of explain in client');
const ruleType = this.ruleTypeRegistry.get(ruleTypeId);
if (!ruleType.configurationExplanationFunction) {
throw new Error('Rule type does not have an explain function configured');
}

try {
const result = await ruleType.configurationExplanationFunction(
params,
esClient,
savedObjectsClient
);
return result;
} catch (e) {
throw new Error(e);
}
}

public async get<Params extends RuleTypeParams = never>({
id,
includeLegacyId = false,
Expand Down
7 changes: 7 additions & 0 deletions x-pack/plugins/alerting/server/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
import type { PublicMethodsOf } from '@kbn/utility-types';
import { ISearchStartSearchSource } from '@kbn/data-plugin/common';
import { LicenseType } from '@kbn/licensing-plugin/server';
import { SavedObjectsClient } from '@kbn/core/public';
import { AlertFactoryDoneUtils, PublicAlert } from './alert';
import { RuleTypeRegistry as OrigruleTypeRegistry } from './rule_type_registry';
import { PluginSetupContract, PluginStartContract } from './plugin';
Expand All @@ -40,6 +41,7 @@ import {
SanitizedRuleConfig,
RuleMonitoring,
MappedParams,
RuleConfigurationExplanation,
} from '../common';
export type WithoutQueryAndParams<T> = Pick<T, Exclude<keyof T, 'query' | 'params'>>;
export type SpaceIdToNamespaceFunction = (spaceId?: string) => string | undefined;
Expand Down Expand Up @@ -169,6 +171,11 @@ export interface RuleType<
ruleTaskTimeout?: string;
cancelAlertsOnRuleTimeout?: boolean;
doesSetRecoveryContext?: boolean;
configurationExplanationFunction?: (
params: Params,
esClient: IScopedClusterClient,
savedObjectsClient: SavedObjectsClientContract
) => RuleConfigurationExplanation;
}
export type UntypedRuleType = RuleType<
RuleTypeParams,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ export async function executeRatioAlert(
}
}

const getESQuery = (
export const getESQuery = (
alertParams: Omit<RuleParams, 'criteria'> & { criteria: CountCriteria },
timestampField: string,
indexPattern: string,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/*
* 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 { EsQueryExplanation } from '@kbn/alerting-plugin/common/rule';
import {
RuleParams,
ruleParamsRT,
getDenominator,
getNumerator,
isRatioRuleParams,
} from '../../../../common/alerting/logs/log_threshold';
import { InfraBackendLibs } from '../../infra_types';
import { decodeOrThrow } from '../../../../common/runtime_types';
import { getESQuery } from './log_threshold_executor';

// Takes a set of alert params and generates an explanation,
// in the Log Threshold case this is an ES Query.
export const createLogThresholdExplanationFunction =
(libs: InfraBackendLibs) =>
async (ruleParams: RuleParams, scopedEsClusterClient, savedObjectsClient) => {
console.log('Inside the explain function');
const [, , { logViews }] = await libs.getStartServices();
const { indices, timestampField, runtimeMappings } = await logViews
.getClient(savedObjectsClient, scopedEsClusterClient.asCurrentUser)
.getResolvedLogView('default'); // TODO: move to params

try {
const validatedParams = decodeOrThrow(ruleParamsRT)(ruleParams);
const fakeExecutionTimestamp = Date.now(); // NOTE: This would normally be the startedAt value handed from the Alerting Framework to the Executor

const queries = [];

if (!isRatioRuleParams(validatedParams)) {
const query = getESQuery(
validatedParams,
timestampField,
indices,
runtimeMappings,
fakeExecutionTimestamp
);

queries.push({
annotation: 'Elasticsearch query', // TODO: i18n
query: JSON.stringify(query),
});
} else {
// Ratio alert params are separated out into two standard sets of alert params
// TODO: There is some crossover here with the splitting out in the executor, could be combined in to a shared function.
const numeratorParams: RuleParams = {
...validatedParams,
criteria: getNumerator(validatedParams.criteria),
};

const denominatorParams: RuleParams = {
...validatedParams,
criteria: getDenominator(validatedParams.criteria),
};

const numeratorQuery = getESQuery(
numeratorParams,
timestampField,
indices,
runtimeMappings,
fakeExecutionTimestamp
);
const denominatorQuery = getESQuery(
denominatorParams,
timestampField,
indices,
runtimeMappings,
fakeExecutionTimestamp
);

queries.push(
{
annotation: 'Numerator Elasticsearch query', // TODO: i18n
query: JSON.stringify(numeratorQuery),
},
{
annotation: 'Denominator Elasticsearch query', // TODO: i18n
query: JSON.stringify(denominatorQuery),
}
);
}

const explanation: EsQueryExplanation = {
type: 'ES_QUERY',
queries,
};

return explanation;
} catch (e) {
throw new Error(e);
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
} from '../../../../common/alerting/logs/log_threshold';
import { InfraBackendLibs } from '../../infra_types';
import { decodeOrThrow } from '../../../../common/runtime_types';
import { createLogThresholdExplanationFunction } from './log_threshold_explainer';

const timestampActionVariableDescription = i18n.translate(
'xpack.infra.logs.alerting.threshold.timestampActionVariableDescription',
Expand Down Expand Up @@ -111,6 +112,7 @@ export async function registerLogThresholdRuleType(
minimumLicenseRequired: 'basic',
isExportable: true,
executor: createLogThresholdExecutor(libs),
configurationExplanationFunction: createLogThresholdExplanationFunction(libs),
actionVariables: {
context: [
{ name: 'timestamp', description: timestampActionVariableDescription },
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* 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 { HttpSetup } from '@kbn/core/public';
import { AsApiContract, RewriteResponseCase } from '@kbn/actions-plugin/common';
import { RuleConfigurationExplanation } from '@kbn/alerting-plugin/common/rule';
import { Rule, RuleUpdates } from '../../../types';
import { BASE_ALERTING_API_PATH } from '../../constants';

type RuleExplainBody = Omit<
RuleUpdates,
'createdBy' | 'updatedBy' | 'muteAll' | 'mutedInstanceIds' | 'executionStatus'
>;

const rewriteBodyRequest: RewriteResponseCase<RuleExplainBody> = ({
ruleTypeId,
notifyWhen,
actions,
...res
}): any => ({
...res,
rule_type_id: ruleTypeId,
notify_when: notifyWhen,
actions: actions.map(({ group, id, params }) => ({
group,
id,
params,
})),
});

export async function explainRule({
http,
rule,
}: {
http: HttpSetup;
rule: RuleExplainBody;
}): Promise<RuleConfigurationExplanation> {
const res = await http.post<AsApiContract<RuleConfigurationExplanation>>(
`${BASE_ALERTING_API_PATH}/explain_rule`,
{
body: JSON.stringify(rewriteBodyRequest(rule)),
}
);
return res;
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ export { updateRule } from './update';
export { resolveRule } from './resolve_rule';
export { snoozeRule } from './snooze';
export { unsnoozeRule } from './unsnooze';
export { explainRule } from './explain_rule';
Loading